msd Backup hinzugefügt

This commit is contained in:
aschwarz
2023-07-25 19:16:12 +02:00
parent 50297c1d25
commit 1d3ed789b5
680 changed files with 103120 additions and 0 deletions

View File

@ -0,0 +1,296 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache;
use Desarrolla2\Cache\Option\PrefixTrait as PrefixOption;
use Desarrolla2\Cache\Option\TtlTrait as TtlOption;
use Desarrolla2\Cache\Packer\PackingTrait as Packing;
use Desarrolla2\Cache\Exception\InvalidArgumentException;
use DateTimeImmutable;
use DateInterval;
use Traversable;
/**
* AbstractAdapter
*/
abstract class AbstractCache implements CacheInterface
{
use PrefixOption;
use TtlOption;
use Packing;
/**
* Make a clone of this object.
*
* @return static
*/
protected function cloneSelf(): self
{
return clone $this;
}
/**
* {@inheritdoc}
*/
public function withOption(string $key, $value): self
{
return $this->withOptions([$key => $value]);
}
/**
* {@inheritdoc}
*/
public function withOptions(array $options): self
{
$cache = $this->cloneSelf();
foreach ($options as $key => $value) {
$method = "set" . str_replace('-', '', $key) . "Option";
if (empty($key) || !method_exists($cache, $method)) {
throw new InvalidArgumentException("unknown option '$key'");
}
$cache->$method($value);
}
return $cache;
}
/**
* {@inheritdoc}
*/
public function getOption($key)
{
$method = "get" . str_replace('-', '', $key) . "Option";
if (empty($key) || !method_exists($this, $method)) {
throw new InvalidArgumentException("unknown option '$key'");
}
return $this->$method();
}
/**
* Validate the key
*
* @param string $key
* @return void
* @throws InvalidArgumentException
*/
protected function assertKey($key): void
{
if (!is_string($key)) {
$type = (is_object($key) ? get_class($key) . ' ' : '') . gettype($key);
throw new InvalidArgumentException("Expected key to be a string, not $type");
}
if ($key === '' || preg_match('~[{}()/\\\\@:]~', $key)) {
throw new InvalidArgumentException("Invalid key '$key'");
}
}
/**
* Assert that the keys are an array or traversable
*
* @param iterable $subject
* @param string $msg
* @return void
* @throws InvalidArgumentException if subject are not iterable
*/
protected function assertIterable($subject, $msg): void
{
$iterable = function_exists('is_iterable')
? is_iterable($subject)
: is_array($subject) || $subject instanceof Traversable;
if (!$iterable) {
throw new InvalidArgumentException($msg);
}
}
/**
* Turn the key into a cache identifier
*
* @param string $key
* @return string
* @throws InvalidArgumentException
*/
protected function keyToId($key): string
{
$this->assertKey($key);
return sprintf('%s%s', $this->prefix, $key);
}
/**
* Create a map with keys and ids
*
* @param iterable $keys
* @return array
* @throws InvalidArgumentException
*/
protected function mapKeysToIds($keys): array
{
$this->assertIterable($keys, 'keys not iterable');
$map = [];
foreach ($keys as $key) {
$id = $this->keyToId($key);
$map[$id] = $key;
}
return $map;
}
/**
* Pack all values and turn keys into ids
*
* @param iterable $values
* @return array
*/
protected function packValues(iterable $values): array
{
$packed = [];
foreach ($values as $key => $value) {
$id = $this->keyToId(is_int($key) ? (string)$key : $key);
$packed[$id] = $this->pack($value);
}
return $packed;
}
/**
* {@inheritdoc}
*/
public function getMultiple($keys, $default = null)
{
$this->assertIterable($keys, 'keys not iterable');
$result = [];
foreach ($keys as $key) {
$result[$key] = $this->get($key, $default);
}
return $result;
}
/**
* {@inheritdoc}
*/
public function setMultiple($values, $ttl = null)
{
$this->assertIterable($values, 'values not iterable');
$success = true;
foreach ($values as $key => $value) {
$success = $this->set(is_int($key) ? (string)$key : $key, $value, $ttl) && $success;
}
return $success;
}
/**
* {@inheritdoc}
*/
public function deleteMultiple($keys)
{
$this->assertIterable($keys, 'keys not iterable');
$success = true;
foreach ($keys as $key) {
$success = $this->delete($key) && $success;
}
return $success;
}
/**
* Get the current time.
*
* @return int
*/
protected function currentTimestamp(): int
{
return time();
}
/**
* Convert TTL to seconds from now
*
* @param null|int|DateInterval $ttl
* @return int|null
* @throws InvalidArgumentException
*/
protected function ttlToSeconds($ttl): ?int
{
if (!isset($ttl)) {
return $this->ttl;
}
if ($ttl instanceof DateInterval) {
$reference = new DateTimeImmutable();
$endTime = $reference->add($ttl);
$ttl = $endTime->getTimestamp() - $reference->getTimestamp();
}
if (!is_int($ttl)) {
$type = (is_object($ttl) ? get_class($ttl) . ' ' : '') . gettype($ttl);
throw new InvalidArgumentException("ttl should be of type int or DateInterval, not $type");
}
return isset($this->ttl) ? min($ttl, $this->ttl) : $ttl;
}
/**
* Convert TTL to epoch timestamp
*
* @param null|int|DateInterval $ttl
* @return int|null
* @throws InvalidArgumentException
*/
protected function ttlToTimestamp($ttl): ?int
{
if (!isset($ttl)) {
return isset($this->ttl) ? time() + $this->ttl : null;
}
if (is_int($ttl)) {
return time() + (isset($this->ttl) ? min($ttl, $this->ttl) : $ttl);
}
if ($ttl instanceof DateInterval) {
$timestamp = (new DateTimeImmutable())->add($ttl)->getTimestamp();
return isset($this->ttl) ? min($timestamp, time() + $this->ttl) : $timestamp;
}
$type = (is_object($ttl) ? get_class($ttl) . ' ' : '') . gettype($ttl);
throw new InvalidArgumentException("ttl should be of type int or DateInterval, not $type");
}
}

View File

@ -0,0 +1,216 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
namespace Desarrolla2\Cache;
use Desarrolla2\Cache\Exception\InvalidArgumentException;
use Desarrolla2\Cache\Option\FilenameTrait as FilenameOption;
/**
* Abstract class for using files as cache.
*
* @package Desarrolla2\Cache
*/
abstract class AbstractFile extends AbstractCache
{
use FilenameOption;
/**
* @var string
*/
protected $cacheDir;
/**
* Class constructor
*
* @param string|null $cacheDir
*/
public function __construct(?string $cacheDir = null)
{
if (!$cacheDir) {
$cacheDir = realpath(sys_get_temp_dir()) . DIRECTORY_SEPARATOR . 'cache';
if(!is_dir($cacheDir)) {
mkdir($cacheDir, 0777, true);
}
}
$this->cacheDir = rtrim($cacheDir, '/');
}
/**
* Validate the key
*
* @param string $key
* @return void
* @throws InvalidArgumentException
*/
protected function assertKey($key): void
{
parent::assertKey($key);
if (strpos($key, '*')) {
throw new InvalidArgumentException("Key may not contain the character '*'");
}
}
/**
* Get the contents of the cache file.
*
* @param string $cacheFile
* @return string
*/
protected function readFile(string $cacheFile): string
{
return file_get_contents($cacheFile);
}
/**
* Read the first line of the cache file.
*
* @param string $cacheFile
* @return string
*/
protected function readLine(string $cacheFile): string
{
$fp = fopen($cacheFile, 'r');
$line = fgets($fp);
fclose($fp);
return $line;
}
/**
* Create a cache file
*
* @param string $cacheFile
* @param string $contents
* @return bool
*/
protected function writeFile(string $cacheFile, string $contents): bool
{
$dir = dirname($cacheFile);
if ($dir !== $this->cacheDir && !is_dir($dir)) {
mkdir($dir, 0775, true);
}
return (bool)file_put_contents($cacheFile, $contents);
}
/**
* Delete a cache file
*
* @param string $file
* @return bool
*/
protected function deleteFile(string $file): bool
{
return !is_file($file) || unlink($file);
}
/**
* Remove all files from a directory.
*/
protected function removeFiles(string $dir): bool
{
$success = true;
$generator = $this->getFilenameOption();
$objects = $this->streamSafeGlob($dir, $generator('*'));
foreach ($objects as $object) {
$success = $this->deleteFile($object) && $success;
}
return $success;
}
/**
* Recursive delete an empty directory.
*
* @param string $dir
*/
protected function removeRecursively(string $dir): bool
{
$success = $this->removeFiles($dir);
$objects = $this->streamSafeGlob($dir, '*');
foreach ($objects as $object) {
if (!is_dir($object)) {
continue;
}
if (is_link($object)) {
unlink($object);
} else {
$success = $this->removeRecursively($object) && $success;
rmdir($object);
}
}
return $success;
}
/**
* {@inheritdoc}
*/
public function delete($key)
{
$cacheFile = $this->getFilename($key);
return $this->deleteFile($cacheFile);
}
/**
* Delete cache directory.
*
* {@inheritdoc}
*/
public function clear()
{
$this->removeRecursively($this->cacheDir);
return true;
}
/**
* Glob that is safe with streams (vfs for example)
*
* @param string $directory
* @param string $filePattern
* @return array
*/
protected function streamSafeGlob(string $directory, string $filePattern): array
{
$filePattern = basename($filePattern);
$files = scandir($directory);
$found = [];
foreach ($files as $filename) {
if (in_array($filename, ['.', '..'])) {
continue;
}
if (fnmatch($filePattern, $filename) || fnmatch($filePattern . '.ttl', $filename)) {
$found[] = "{$directory}/{$filename}";
}
}
return $found;
}
}

View File

@ -0,0 +1,88 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache;
use Desarrolla2\Cache\Exception\CacheException;
use Desarrolla2\Cache\Packer\PackerInterface;
use Desarrolla2\Cache\Packer\NopPacker;
/**
* Apcu
*/
class Apcu extends AbstractCache
{
/**
* Create the default packer for this cache implementation
*
* @return PackerInterface
*/
protected static function createDefaultPacker(): PackerInterface
{
return new NopPacker();
}
/**
* {@inheritdoc}
*/
public function set($key, $value, $ttl = null)
{
$ttlSeconds = $this->ttlToSeconds($ttl);
if (isset($ttlSeconds) && $ttlSeconds <= 0) {
return $this->delete($key);
}
return apcu_store($this->keyToId($key), $this->pack($value), $ttlSeconds ?? 0);
}
/**
* {@inheritdoc}
*/
public function get($key, $default = null)
{
$packed = apcu_fetch($this->keyToId($key), $success);
return $success ? $this->unpack($packed) : $default;
}
/**
* {@inheritdoc}
*/
public function has($key)
{
return apcu_exists($this->keyToId($key));
}
/**
* {@inheritdoc}
*/
public function delete($key)
{
$id = $this->keyToId($key);
return apcu_delete($id) || !apcu_exists($id);
}
/**
* {@inheritdoc}
*/
public function clear()
{
return apcu_clear_cache();
}
}

View File

@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
namespace Desarrolla2\Cache;
use Psr\SimpleCache\CacheInterface as PsrCacheInterface;
use Desarrolla2\Cache\Packer\PackerInterface;
use Desarrolla2\Cache\KeyMaker\KeyMakerInterface;
/**
* CacheInterface
*/
interface CacheInterface extends PsrCacheInterface
{
/**
* Set option for cache
*
* @param string $key
* @param mixed $value
* @return static
*/
public function withOption(string $key, $value);
/**
* Set multiple options for cache
*
* @param array $options
* @return static
*/
public function withOptions(array $options);
/**
* Get option for cache
*
* @param string $key
* @return mixed
*/
public function getOption($key);
/**
* Set the packer
*
* @param PackerInterface $packer
* @return static
*/
public function withPacker(PackerInterface $packer);
}

View File

@ -0,0 +1,192 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
namespace Desarrolla2\Cache;
use Desarrolla2\Cache\Packer\NopPacker;
use Desarrolla2\Cache\Packer\PackerInterface;
use Desarrolla2\Cache\Exception\InvalidArgumentException;
/**
* Use multiple cache adapters.
*/
class Chain extends AbstractCache
{
/**
* @var CacheInterface[]
*/
protected $adapters;
/**
* Create the default packer for this cache implementation
*
* @return PackerInterface
*/
protected static function createDefaultPacker(): PackerInterface
{
return new NopPacker();
}
/**
* Chain constructor.
*
* @param CacheInterface[] $adapters Fastest to slowest
*/
public function __construct(array $adapters)
{
foreach ($adapters as $adapter) {
if (!$adapter instanceof CacheInterface) {
throw new InvalidArgumentException("All adapters should be a cache implementation");
}
}
$this->adapters = $adapters;
}
/**
* {@inheritdoc}
*/
public function set($key, $value, $ttl = null)
{
$success = true;
foreach ($this->adapters as $adapter) {
$success = $adapter->set($key, $value, $ttl) && $success;
}
return $success;
}
/**
* {@inheritdoc}
*/
public function setMultiple($values, $ttl = null)
{
$success = true;
foreach ($this->adapters as $adapter) {
$success = $adapter->setMultiple($values, $ttl) && $success;
}
return $success;
}
/**
* {@inheritdoc}
*/
public function get($key, $default = null)
{
foreach ($this->adapters as $adapter) {
$result = $adapter->get($key); // Not using $default as we want to get null if the adapter doesn't have it
if (isset($result)) {
return $result;
}
}
return $default;
}
/**
* {@inheritdoc}
*/
public function getMultiple($keys, $default = null)
{
$this->assertIterable($keys, 'keys are not iterable');
$missing = [];
$values = [];
foreach ($keys as $key) {
$this->assertKey($key);
$missing[] = $key;
$values[$key] = $default;
}
foreach ($this->adapters as $adapter) {
if (empty($missing)) {
break;
}
$found = [];
foreach ($adapter->getMultiple($missing) as $key => $value) {
if (isset($value)) {
$found[$key] = $value;
}
}
$values = array_merge($values, $found);
$missing = array_values(array_diff($missing, array_keys($found)));
}
return $values;
}
/**
* {@inheritdoc}
*/
public function has($key)
{
foreach ($this->adapters as $adapter) {
if ($adapter->has($key)) {
return true;
}
}
return false;
}
/**
* {@inheritdoc}
*/
public function delete($key)
{
$success = true;
foreach ($this->adapters as $adapter) {
$success = $adapter->delete($key) && $success;
}
return $success;
}
/**
* {@inheritdoc}
*/
public function deleteMultiple($keys)
{
$success = true;
foreach ($this->adapters as $adapter) {
$success = $adapter->deleteMultiple($keys) && $success;
}
return $success;
}
/**
* {@inheritdoc}
*/
public function clear()
{
$success = true;
foreach ($this->adapters as $adapter) {
$success = $adapter->clear() && $success;
}
return $success;
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache\Exception;
use Psr\SimpleCache\CacheException as PsrCacheException;
/**
* Exception bad method calls
*/
class BadMethodCallException extends \BadMethodCallException implements PsrCacheException
{
}

View File

@ -0,0 +1,25 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache\Exception;
use Psr\SimpleCache\CacheException as PsrCacheException;
/**
* Interface used for all types of exceptions thrown by the implementing library.
*/
class CacheException extends \RuntimeException implements PsrCacheException
{
}

View File

@ -0,0 +1,25 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache\Exception;
use Psr\SimpleCache\InvalidArgumentException as PsrInvalidArgumentException;
/**
* Exception for invalid cache arguments.
*/
class InvalidArgumentException extends \InvalidArgumentException implements PsrInvalidArgumentException
{
}

View File

@ -0,0 +1,25 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache\Exception;
use Psr\SimpleCache\CacheException as PsrCacheException;
/**
* Exception for unexpected values when reading from cache.
*/
class UnexpectedValueException extends \UnexpectedValueException implements PsrCacheException
{
}

View File

@ -0,0 +1,175 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache;
use Desarrolla2\Cache\Exception\InvalidArgumentException;
use Desarrolla2\Cache\Exception\UnexpectedValueException;
use Desarrolla2\Cache\Packer\PackerInterface;
use Desarrolla2\Cache\Packer\SerializePacker;
/**
* Cache file.
*/
class File extends AbstractFile
{
/**
* @var string 'embed', 'file', 'mtime'
*/
protected $ttlStrategy = 'embed';
/**
* Create the default packer for this cache implementation
*
* @return PackerInterface
*/
protected static function createDefaultPacker(): PackerInterface
{
return new SerializePacker();
}
/**
* Set TTL strategy
*
* @param string $strategy
*/
protected function setTtlStrategyOption($strategy)
{
if (!in_array($strategy, ['embed', 'file', 'mtime'])) {
throw new InvalidArgumentException("Unknown strategy '$strategy', should be 'embed', 'file' or 'mtime'");
}
$this->ttlStrategy = $strategy;
}
/**
* Get TTL strategy
*
* @return string
*/
protected function getTtlStrategyOption(): string
{
return $this->ttlStrategy;
}
/**
* Get the TTL using one of the strategies
*
* @param string $cacheFile
* @return int
*/
protected function getTtl(string $cacheFile)
{
switch ($this->ttlStrategy) {
case 'embed':
return (int)$this->readLine($cacheFile);
case 'file':
return file_exists("$cacheFile.ttl")
? (int)file_get_contents("$cacheFile.ttl")
: PHP_INT_MAX;
case 'mtime':
return $this->getTtl($cacheFile) > 0 ? filemtime($cacheFile) + $this->ttl : PHP_INT_MAX;
}
throw new \InvalidArgumentException("Invalid TTL strategy '{$this->ttlStrategy}'");
}
/**
* Set the TTL using one of the strategies
*
* @param int|null $expiration
* @param string $contents
* @param string $cacheFile
* @return string The (modified) contents
*/
protected function setTtl($expiration, $contents, $cacheFile)
{
switch ($this->ttlStrategy) {
case 'embed':
$contents = ($expiration ?? PHP_INT_MAX) . "\n" . $contents;
break;
case 'file':
if ($expiration !== null) {
file_put_contents("$cacheFile.ttl", $expiration);
}
break;
case 'mtime':
// nothing
break;
}
return $contents;
}
/**
* {@inheritdoc}
*/
public function get($key, $default = null)
{
if (!$this->has($key)) {
return $default;
}
$cacheFile = $this->getFilename($key);
$packed = $this->readFile($cacheFile);
if ($this->ttlStrategy === 'embed') {
$packed = substr($packed, strpos($packed, "\n") + 1);
}
return $this->unpack($packed);
}
/**
* {@inheritdoc}
*/
public function has($key)
{
$cacheFile = $this->getFilename($key);
if (!file_exists($cacheFile)) {
return false;
}
$ttl = $this->getTtl($cacheFile);
if ($ttl <= time()) {
$this->deleteFile($cacheFile);
return false;
}
return true;
}
/**
* {@inheritdoc}
*/
public function set($key, $value, $ttl = null)
{
$cacheFile = $this->getFilename($key);
$packed = $this->pack($value);
if (!is_string($packed)) {
throw new UnexpectedValueException("Packer must create a string for the data to be cached to file");
}
$contents = $this->setTtl($this->ttlToTimestamp($ttl), $packed, $cacheFile);
return $this->writeFile($cacheFile, $contents);
}
}

View File

@ -0,0 +1,68 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache\File;
/**
* Create a path for a key
*/
class BasicFilename
{
/**
* @var string
*/
protected $format;
/**
* BasicFilename constructor.
*
* @param string $format
*/
public function __construct(string $format)
{
$this->format = $format;
}
/**
* Get the format
*
* @return string
*/
public function getFormat(): string
{
return $this->format;
}
/**
* Create the path for a key
*
* @param string $key
* @return string
*/
public function __invoke(string $key): string
{
return sprintf($this->format, $key ?: '*');
}
/**
* Cast to string
*
* @return string
*/
public function __toString(): string
{
return $this->getFormat();
}
}

View File

@ -0,0 +1,121 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache\File;
/**
* Create a path for a key as prefix tree directory structure.
*
* @see https://en.wikipedia.org/wiki/Trie
*/
class TrieFilename
{
/**
* @var string
*/
protected $format;
/**
* @var int
*/
protected $levels;
/**
* @var bool
*/
protected $hash;
/**
* TrieFilename constructor.
*
* @param string $format
* @param int $levels The depth of the structure
* @param bool $hash MD5 hash the key to get a better spread
*/
public function __construct(string $format, int $levels = 1, bool $hash = false)
{
$this->format = $format;
$this->levels = $levels;
$this->hash = $hash;
}
/**
* Get the format
*
* @return string
*/
public function getFormat(): string
{
return $this->format;
}
/**
* Get the depth of the structure
*
* @return int
*/
public function getLevels(): int
{
return $this->levels;
}
/**
* Will the key be hashed to create the trie.
*
* @return bool
*/
public function isHashed(): bool
{
return $this->hash;
}
/**
* Create the path for a key
*
* @param string $key
* @return string
*/
public function __invoke(string $key): string
{
if (empty($key)) {
return $this->wildcardPath();
}
$dirname = $this->hash ? base_convert(md5($key), 16, 36) : $key;
$filename = sprintf($this->format, $key);
$path = '';
for ($length = 1; $length <= $this->levels; $length++) {
$path .= substr($dirname, 0, $length) . DIRECTORY_SEPARATOR;
}
return $path . $filename;
}
/**
* Get a path for all files (using glob)
*
* @return string
*/
protected function wildcardPath(): string
{
$filename = sprintf($this->format, '*');
return str_repeat('*' . DIRECTORY_SEPARATOR, $this->levels) . $filename;
}
}

View File

@ -0,0 +1,218 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache;
use Desarrolla2\Cache\Exception\InvalidArgumentException;
use Desarrolla2\Cache\Packer\PackerInterface;
use Desarrolla2\Cache\Packer\NopPacker;
use Memcached as MemcachedServer;
/**
* Memcached
*/
class Memcached extends AbstractCache
{
/**
* @var MemcachedServer
*/
protected $server;
/**
* @param MemcachedServer $server
*/
public function __construct(MemcachedServer $server)
{
$this->server = $server;
}
/**
* Create the default packer for this cache implementation
*
* @return PackerInterface
*/
protected static function createDefaultPacker(): PackerInterface
{
return new NopPacker();
}
/**
* Validate the key
*
* @param string $key
* @return void
* @throws InvalidArgumentException
*/
protected function assertKey($key): void
{
parent::assertKey($key);
if (strlen($key) > 250) {
throw new InvalidArgumentException("Key to long, max 250 characters");
}
}
/**
* Pack all values and turn keys into ids
*
* @param iterable $values
* @return array
*/
protected function packValues(iterable $values): array
{
$packed = [];
foreach ($values as $key => $value) {
$this->assertKey(is_int($key) ? (string)$key : $key);
$packed[$key] = $this->pack($value);
}
return $packed;
}
/**
* {@inheritdoc}
*/
public function get($key, $default = null)
{
$this->assertKey($key);
$data = $this->server->get($key);
if ($this->server->getResultCode() !== MemcachedServer::RES_SUCCESS) {
return $default;
}
return $this->unpack($data);
}
/**
* {@inheritdoc}
*/
public function has($key)
{
$this->assertKey($key);
$this->server->get($key);
$result = $this->server->getResultCode();
return $result === MemcachedServer::RES_SUCCESS;
}
/**
* {@inheritdoc}
*/
public function set($key, $value, $ttl = null)
{
$this->assertKey($key);
$packed = $this->pack($value);
$ttlTime = $this->ttlToMemcachedTime($ttl);
if ($ttlTime === false) {
return $this->delete($key);
}
$success = $this->server->set($key, $packed, $ttlTime);
return $success;
}
/**
* {@inheritdoc}
*/
public function delete($key)
{
$this->server->delete($this->keyToId($key));
$result = $this->server->getResultCode();
return $result === MemcachedServer::RES_SUCCESS || $result === MemcachedServer::RES_NOTFOUND;
}
/**
* {@inheritdoc}
*/
public function getMultiple($keys, $default = null)
{
$this->assertIterable($keys, 'keys not iterable');
$keysArr = is_array($keys) ? $keys : iterator_to_array($keys, false);
array_walk($keysArr, [$this, 'assertKey']);
$result = $this->server->getMulti($keysArr);
if ($result === false) {
return false;
}
$items = array_fill_keys($keysArr, $default);
foreach ($result as $key => $value) {
$items[$key] = $this->unpack($value);
}
return $items;
}
/**
* {@inheritdoc}
*/
public function setMultiple($values, $ttl = null)
{
$this->assertIterable($values, 'values not iterable');
$packed = $this->packValues($values);
$ttlTime = $this->ttlToMemcachedTime($ttl);
if ($ttlTime === false) {
return $this->server->deleteMulti(array_keys($packed));
}
return $this->server->setMulti($packed, $ttlTime);
}
/**
* {@inheritdoc}
*/
public function clear()
{
return $this->server->flush();
}
/**
* Convert ttl to timestamp or seconds.
*
* @see http://php.net/manual/en/memcached.expiration.php
*
* @param null|int|\DateInterval $ttl
* @return int|null
* @throws InvalidArgumentException
*/
protected function ttlToMemcachedTime($ttl)
{
$seconds = $this->ttlToSeconds($ttl);
if ($seconds <= 0) {
return isset($seconds) ? false : 0;
}
/* 2592000 seconds = 30 days */
return $seconds <= 2592000 ? $seconds : $this->ttlToTimestamp($ttl);
}
}

View File

@ -0,0 +1,165 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache;
use Desarrolla2\Cache\Packer\PackerInterface;
use Desarrolla2\Cache\Packer\SerializePacker;
/**
* Memory
*/
class Memory extends AbstractCache
{
/**
* Limit the amount of entries
* @var int
*/
protected $limit = PHP_INT_MAX;
/**
* @var array
*/
protected $cache = [];
/**
* @var array
*/
protected $cacheTtl = [];
/**
* Create the default packer for this cache implementation.
* {@internal NopPacker might fail PSR-16, as cached objects would change}
*
* @return PackerInterface
*/
protected static function createDefaultPacker(): PackerInterface
{
return new SerializePacker();
}
/**
* Make a clone of this object.
* Set by cache reference, thus using the same pool.
*
* @return static
*/
protected function cloneSelf(): AbstractCache
{
$clone = clone $this;
$clone->cache =& $this->cache;
$clone->cacheTtl =& $this->cacheTtl;
return $clone;
}
/**
* Set the max number of items
*
* @param int $limit
*/
protected function setLimitOption($limit)
{
$this->limit = (int)$limit ?: PHP_INT_MAX;
}
/**
* Get the max number of items
*
* @return int
*/
protected function getLimitOption()
{
return $this->limit;
}
/**
* {@inheritdoc}
*/
public function get($key, $default = null)
{
if (!$this->has($key)) {
return $default;
}
$id = $this->keyToId($key);
return $this->unpack($this->cache[$id]);
}
/**
* {@inheritdoc}
*/
public function has($key)
{
$id = $this->keyToId($key);
if (!isset($this->cacheTtl[$id])) {
return false;
}
if ($this->cacheTtl[$id] <= time()) {
unset($this->cache[$id], $this->cacheTtl[$id]);
return false;
}
return true;
}
/**
* {@inheritdoc}
*/
public function set($key, $value, $ttl = null)
{
if (count($this->cache) >= $this->limit) {
$deleteKey = key($this->cache);
unset($this->cache[$deleteKey], $this->cacheTtl[$deleteKey]);
}
$id = $this->keyToId($key);
$this->cache[$id] = $this->pack($value);
$this->cacheTtl[$id] = $this->ttlToTimestamp($ttl) ?? PHP_INT_MAX;
return true;
}
/**
* {@inheritdoc}
*/
public function delete($key)
{
$id = $this->keyToId($key);
unset($this->cache[$id], $this->cacheTtl[$id]);
return true;
}
/**
* {@inheritdoc}
*/
public function clear()
{
$this->cache = [];
$this->cacheTtl = [];
return true;
}
}

View File

@ -0,0 +1,273 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache;
use Desarrolla2\Cache\Packer\PackerInterface;
use Desarrolla2\Cache\Packer\MongoDBBinaryPacker;
use Desarrolla2\Cache\Option\InitializeTrait as InitializeOption;
use MongoDB\Collection;
use MongoDB\BSON\UTCDatetime as BSONUTCDateTime;
use MongoDB\Driver\Exception\RuntimeException as MongoDBRuntimeException;
/**
* MongoDB cache implementation
*/
class MongoDB extends AbstractCache
{
use InitializeOption;
/**
* @var Collection
*/
protected $collection;
/**
* Class constructor
*
* @param Collection $collection
*/
public function __construct(Collection $collection)
{
$this->collection = $collection;
}
/**
* Initialize the DB collection.
* Set TTL index.
*/
protected function initialize(): void
{
$this->collection->createIndex(['ttl' => 1], ['expireAfterSeconds' => 0]);
}
/**
* Create the default packer for this cache implementation.
*
* @return PackerInterface
*/
protected static function createDefaultPacker(): PackerInterface
{
return new MongoDBBinaryPacker();
}
/**
* Get filter for key and ttl.
*
* @param string|iterable $key
* @return array
*/
protected function filter($key)
{
if (is_array($key)) {
$key = ['$in' => $key];
}
return [
'_id' => $key,
'$or' => [
['ttl' => ['$gt' => new BSONUTCDateTime($this->currentTimestamp() * 1000)]],
['ttl' => null]
]
];
}
/**
* {@inheritdoc }
*/
public function get($key, $default = null)
{
$filter = $this->filter($this->keyToId($key));
try {
$data = $this->collection->findOne($filter);
} catch (MongoDBRuntimeException $e) {
return $default;
}
return isset($data) ? $this->unpack($data['value']) : $default;
}
/**
* {@inheritdoc}
*/
public function getMultiple($keys, $default = null)
{
$idKeyPairs = $this->mapKeysToIds($keys);
if (empty($idKeyPairs)) {
return [];
}
$filter = $this->filter(array_keys($idKeyPairs));
$items = array_fill_keys(array_values($idKeyPairs), $default);
try {
$rows = $this->collection->find($filter);
} catch (MongoDBRuntimeException $e) {
return $items;
}
foreach ($rows as $row) {
$id = $row['_id'];
$key = $idKeyPairs[$id];
$items[$key] = $this->unpack($row['value']);
}
return $items;
}
/**
* {@inheritdoc }
*/
public function has($key)
{
$filter = $this->filter($this->keyToId($key));
try {
$count = $this->collection->count($filter);
} catch (MongoDBRuntimeException $e) {
return false;
}
return $count > 0;
}
/**
* {@inheritdoc }
*/
public function set($key, $value, $ttl = null)
{
$id = $this->keyToId($key);
$item = [
'_id' => $id,
'ttl' => $this->getTtlBSON($ttl),
'value' => $this->pack($value)
];
try {
$this->collection->replaceOne(['_id' => $id], $item, ['upsert' => true]);
} catch (MongoDBRuntimeException $e) {
return false;
}
return true;
}
/**
* {@inheritdoc}
*/
public function setMultiple($values, $ttl = null)
{
$this->assertIterable($values, 'values not iterable');
if (empty($values)) {
return true;
}
$bsonTtl = $this->getTtlBSON($ttl);
$items = [];
foreach ($values as $key => $value) {
$id = $this->keyToId(is_int($key) ? (string)$key : $key);
$items[] = [
'replaceOne' => [
['_id' => $id],
[
'_id' => $id,
'ttl' => $bsonTtl,
'value' => $this->pack($value)
],
[ 'upsert' => true ]
]
];
}
try {
$this->collection->bulkWrite($items);
} catch (MongoDBRuntimeException $e) {
return false;
}
return true;
}
/**
* {@inheritdoc}
*/
public function delete($key)
{
$id = $this->keyToId($key);
try {
$this->collection->deleteOne(['_id' => $id]);
} catch (MongoDBRuntimeException $e) {
return false;
}
return true;
}
/**
* {@inheritdoc}
*/
public function deleteMultiple($keys)
{
$idKeyPairs = $this->mapKeysToIds($keys);
try {
if (!empty($idKeyPairs)) {
$this->collection->deleteMany(['_id' => ['$in' => array_keys($idKeyPairs)]]);
}
} catch (MongoDBRuntimeException $e) {
return false;
}
return true;
}
/**
* {@inheritdoc}
*/
public function clear()
{
try {
$this->collection->drop();
} catch (MongoDBRuntimeException $e) {
return false;
}
$this->requireInitialization();
return true;
}
/**
* Get TTL as Date type BSON object
*
* @param null|int|\DateInterval $ttl
* @return BSONUTCDatetime|null
*/
protected function getTtlBSON($ttl): ?BSONUTCDatetime
{
return isset($ttl) ? new BSONUTCDateTime($this->ttlToTimestamp($ttl) * 1000) : null;
}
}

View File

@ -0,0 +1,312 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache;
use Desarrolla2\Cache\Option\InitializeTrait;
use mysqli as Server;
use Desarrolla2\Cache\Packer\PackerInterface;
use Desarrolla2\Cache\Packer\SerializePacker;
/**
* Mysqli cache adapter.
*
* Errors are silently ignored but exceptions are **not** caught. Beware when using `mysqli_report()` to throw a
* `mysqli_sql_exception` on error.
*/
class Mysqli extends AbstractCache
{
use InitializeTrait;
/**
* @var Server
*/
protected $server;
/**
* @var string
*/
protected $table = 'cache';
/**
* Class constructor
*
* @param Server $server
*/
public function __construct(Server $server)
{
$this->server = $server;
}
/**
* Initialize table.
* Automatically delete old cache.
*/
protected function initialize(): void
{
if ($this->initialized !== false) {
return;
}
$this->query(
"CREATE TABLE IF NOT EXISTS `{table}` "
. "( `key` VARCHAR(255), `value` BLOB, `ttl` BIGINT UNSIGNED, PRIMARY KEY (`key`) )"
);
$this->query(
"CREATE EVENT IF NOT EXISTS `apply_ttl_{$this->table}` ON SCHEDULE EVERY 1 HOUR DO BEGIN"
. " DELETE FROM {table} WHERE `ttl` < UNIX_TIMESTAMP();"
. " END"
);
$this->initialized = true;
}
/**
* Create the default packer for this cache implementation.
*
* @return PackerInterface
*/
protected static function createDefaultPacker(): PackerInterface
{
return new SerializePacker();
}
/**
* Set the table name
*
* @param string $table
*/
public function setTableOption(string $table)
{
$this->table = $table;
$this->requireInitialization();
}
/**
* Get the table name
*
* @return string
*/
public function getTableOption(): string
{
return $this->table;
}
/**
* {@inheritdoc}
*/
public function get($key, $default = null)
{
$this->initialize();
$result = $this->query(
'SELECT `value` FROM {table} WHERE `key` = ? AND (`ttl` > ? OR `ttl` IS NULL) LIMIT 1',
'si',
$this->keyToId($key),
$this->currentTimestamp()
);
$row = $result !== false ? $result->fetch_row() : null;
return $row ? $this->unpack($row[0]) : $default;
}
/**
* {@inheritdoc}
*/
public function getMultiple($keys, $default = null)
{
$idKeyPairs = $this->mapKeysToIds($keys);
if (empty($idKeyPairs)) {
return [];
}
$this->initialize();
$values = array_fill_keys(array_values($idKeyPairs), $default);
$placeholders = rtrim(str_repeat('?, ', count($idKeyPairs)), ', ');
$paramTypes = str_repeat('s', count($idKeyPairs)) . 'i';
$params = array_keys($idKeyPairs);
$params[] = $this->currentTimestamp();
$result = $this->query(
"SELECT `key`, `value` FROM {table} WHERE `key` IN ($placeholders) AND (`ttl` > ? OR `ttl` IS NULL)",
$paramTypes,
...$params
);
while (([$id, $value] = $result->fetch_row())) {
$key = $idKeyPairs[$id];
$values[$key] = $this->unpack($value);
}
return $values;
}
/**
* {@inheritdoc}
*/
public function has($key)
{
$this->initialize();
$result = $this->query(
'SELECT COUNT(`key`) FROM {table} WHERE `key` = ? AND (`ttl` > ? OR `ttl` IS NULL) LIMIT 1',
'si',
$this->keyToId($key),
$this->currentTimestamp()
);
[$count] = $result ? $result->fetch_row() : [null];
return isset($count) && $count > 0;
}
/**
* {@inheritdoc}
*/
public function set($key, $value, $ttl = null)
{
$this->initialize();
$result = $this->query(
'REPLACE INTO {table} (`key`, `value`, `ttl`) VALUES (?, ?, ?)',
'ssi',
$this->keyToId($key),
$this->pack($value),
$this->ttlToTimestamp($ttl)
);
return $result !== false;
}
/**
* {@inheritdoc}
*/
public function setMultiple($values, $ttl = null)
{
$this->assertIterable($values, 'values not iterable');
if (empty($values)) {
return true;
}
$this->initialize();
$count = 0;
$params = [];
$timeTtl = $this->ttlToTimestamp($ttl);
foreach ($values as $key => $value) {
$count++;
$params[] = $this->keyToId(is_int($key) ? (string)$key : $key);
$params[] = $this->pack($value);
$params[] = $timeTtl;
}
$query = 'REPLACE INTO {table} (`key`, `value`, `ttl`) VALUES '
. rtrim(str_repeat('(?, ?, ?), ', $count), ', ');
return (bool)$this->query($query, str_repeat('ssi', $count), ...$params);
}
/**
* {@inheritdoc}
*/
public function delete($key)
{
$this->initialize();
return (bool)$this->query(
'DELETE FROM {table} WHERE `key` = ?',
's',
$this->keyToId($key)
);
}
/**
* {@inheritdoc}
*/
public function deleteMultiple($keys)
{
$idKeyPairs = $this->mapKeysToIds($keys);
if (empty($idKeyPairs)) {
return true;
}
$this->initialize();
$placeholders = rtrim(str_repeat('?, ', count($idKeyPairs)), ', ');
$paramTypes = str_repeat('s', count($idKeyPairs));
return (bool)$this->query(
"DELETE FROM {table} WHERE `key` IN ($placeholders)",
$paramTypes,
...array_keys($idKeyPairs)
);
}
/**
* {@inheritdoc}
*/
public function clear()
{
$this->initialize();
return (bool)$this->query('TRUNCATE {table}');
}
/**
* Query the MySQL server
*
* @param string $query
* @param string $types
* @param mixed[] $params
* @return \mysqli_result|bool
*/
protected function query($query, $types = '', ...$params)
{
$sql = str_replace('{table}', $this->table, $query);
if ($params === []) {
$ret = $this->server->query($sql);
} else {
$statement = $this->server->prepare($sql);
if ($statement !== false) {
$statement->bind_param($types, ...$params);
$ret = $statement->execute();
$ret = $ret ? ($statement->get_result() ?: true) : false;
} else {
$ret = false;
}
}
if ($this->server->error) {
trigger_error($this->server->error . " $sql", E_USER_NOTICE);
}
return $ret;
}
}

View File

@ -0,0 +1,93 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache;
use Desarrolla2\Cache\AbstractCache;
use Desarrolla2\Cache\Packer\PackerInterface;
use Desarrolla2\Cache\Packer\NopPacker;
/**
* Dummy cache handler
*/
class NotCache extends AbstractCache
{
/**
* Create the default packer for this cache implementation.
*
* @return PackerInterface
*/
protected static function createDefaultPacker(): PackerInterface
{
return new NopPacker();
}
/**
* {@inheritdoc}
*/
public function delete($key)
{
return true;
}
/**
* {@inheritdoc}
*/
public function get($key, $default = null)
{
return false;
}
/**
* {@inheritdoc}
*/
public function getMultiple($keys, $default = null)
{
return false;
}
/**
* {@inheritdoc}
*/
public function has($key)
{
return false;
}
/**
* {@inheritdoc}
*/
public function set($key, $value, $ttl = null)
{
return false;
}
/**
* {@inheritdoc}
*/
public function setMultiple($values, $ttl = null)
{
return false;
}
/**
* {@inheritdoc}
*/
public function clear()
{
return true;
}
}

View File

@ -0,0 +1,91 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache\Option;
use TypeError;
use Desarrolla2\Cache\File\BasicFilename;
/**
* Use filename generator
*/
trait FilenameTrait
{
/**
* @var callable
*/
protected $filename;
/**
* Filename format or callable.
* The filename format will be applied using sprintf, replacing `%s` with the key.
*
* @param string|callable $filename
* @return void
*/
protected function setFilenameOption($filename): void
{
if (is_string($filename)) {
$filename = new BasicFilename($filename);
}
if (!is_callable($filename)) {
throw new TypeError("Filename should be a string or callable");
}
$this->filename = $filename;
}
/**
* Get the filename callable
*
* @return callable
*/
protected function getFilenameOption(): callable
{
if (!isset($this->filename)) {
$this->filename = new BasicFilename('%s.' . $this->getPacker()->getType());
}
return $this->filename;
}
/**
* Create a filename based on the key
*
* @param string|mixed $key
* @return string
*/
protected function getFilename($key): string
{
$id = $this->keyToId($key);
$generator = $this->getFilenameOption();
return $this->cacheDir . DIRECTORY_SEPARATOR . $generator($id);
}
/**
* Get a wildcard for all files
*
* @return string
*/
protected function getWildcard(): string
{
$generator = $this->getFilenameOption();
return $this->cacheDir . DIRECTORY_SEPARATOR . $generator('');
}
}

View File

@ -0,0 +1,65 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache\Option;
/**
* Auto initialize the cache
*/
trait InitializeTrait
{
/**
* Is cache initialized
* @var bool|null
*/
protected $initialized = false;
/**
* Enable/disable initialization
*
* @param bool $enabled
*/
public function setInitializeOption(bool $enabled)
{
$this->initialized = $enabled ? (bool)$this->initialized : null;
}
/**
* Should initialize
*
* @return bool
*/
protected function getInitializeOption(): bool
{
return $this->initialized !== null;
}
/**
* Mark as initialization required (if enabled)
*/
protected function requireInitialization()
{
$this->initialized = isset($this->initialized) ? false : null;
}
/**
* Initialize
*
* @return void
*/
abstract protected function initialize(): void;
}

View File

@ -0,0 +1,49 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache\Option;
/**
* Prefix option
*/
trait PrefixTrait
{
/**
* @var string
*/
protected $prefix = '';
/**
* Set the key prefix
*
* @param string $prefix
* @return void
*/
protected function setPrefixOption(string $prefix): void
{
$this->prefix = $prefix;
}
/**
* Get the key prefix
*
* @return string
*/
protected function getPrefixOption(): string
{
return $this->prefix;
}
}

View File

@ -0,0 +1,54 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache\Option;
use Desarrolla2\Cache\Exception\InvalidArgumentException;
/**
* TTL option
*/
trait TtlTrait
{
/**
* @var int|null
*/
protected $ttl = null;
/**
* Set the maximum time to live (ttl)
*
* @param int|null $value Seconds or null to live forever
* @throws InvalidArgumentException
*/
protected function setTtlOption(?int $value): void
{
if (isset($value) && $value < 1) {
throw new InvalidArgumentException('ttl cant be lower than 1');
}
$this->ttl = $value;
}
/**
* Get the maximum time to live (ttl)
*
* @return int|null
*/
protected function getTtlOption(): ?int
{
return $this->ttl;
}
}

View File

@ -0,0 +1,69 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache\Packer;
use Desarrolla2\Cache\Packer\PackerInterface;
use Desarrolla2\Cache\Exception\InvalidArgumentException;
/**
* Pack value through serialization
*/
class JsonPacker implements PackerInterface
{
/**
* Get cache type (might be used as file extension)
*
* @return string
*/
public function getType()
{
return 'json';
}
/**
* Pack the value
*
* @param mixed $value
* @return string
*/
public function pack($value)
{
return json_encode($value);
}
/**
* Unpack the value
*
* @param string $packed
* @return mixed
* @throws InvalidArgumentException
*/
public function unpack($packed)
{
if (!is_string($packed)) {
throw new InvalidArgumentException("packed value should be a string");
}
$ret = json_decode($packed);
if (!isset($ret) && json_last_error()) {
throw new \UnexpectedValueException("packed value is not a valid JSON string");
}
return $ret;
}
}

View File

@ -0,0 +1,77 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
namespace Desarrolla2\Cache\Packer;
use Desarrolla2\Cache\Packer\PackerInterface;
use MongoDB\BSON\Binary;
/**
* Pack as BSON binary
*
* @todo Don't use serialize when packer chain is here.
*/
class MongoDBBinaryPacker implements PackerInterface
{
/**
* @var array
*/
protected $options;
/**
* SerializePacker constructor
*
* @param array $options Any options to be provided to unserialize()
*/
public function __construct(array $options = ['allowed_classes' => true])
{
$this->options = $options;
}
/**
* Get cache type (might be used as file extension)
*
* @return string
*/
public function getType()
{
return 'bson';
}
/**
* Pack the value
*
* @param mixed $value
* @return string
*/
public function pack($value)
{
return new Binary(serialize($value), Binary::TYPE_GENERIC);
}
/**
* Unpack the value
*
* @param string $packed
* @return string
* @throws \UnexpectedValueException if he value can't be unpacked
*/
public function unpack($packed)
{
if (!$packed instanceof Binary) {
throw new \InvalidArgumentException("packed value should be BSON binary");
}
return unserialize((string)$packed, $this->options);
}
}

View File

@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache\Packer;
use Desarrolla2\Cache\Packer\PackerInterface;
/**
* Don't pack, just straight passthrough
*/
class NopPacker implements PackerInterface
{
/**
* Get cache type (might be used as file extension)
*
* @return string
*/
public function getType()
{
return 'data';
}
/**
* Pack the value
*
* @param mixed $value
* @return mixed
*/
public function pack($value)
{
return $value;
}
/**
* Unpack the value
*
* @param mixed $packed
* @return mixed
*/
public function unpack($packed)
{
return $packed;
}
}

View File

@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache\Packer;
/**
* Interface for packer / unpacker
*/
interface PackerInterface
{
/**
* Get cache type (might be used as file extension)
*
* @return string
*/
public function getType();
/**
* Pack the value
*
* @param mixed $value
* @return string|mixed
*/
public function pack($value);
/**
* Unpack the value
*
* @param string|mixed $packed
* @return string
* @throws \UnexpectedValueException if the value can't be unpacked
*/
public function unpack($packed);
}

View File

@ -0,0 +1,86 @@
<?php
/**
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache\Packer;
/**
* Support packing for Caching adapter
*/
trait PackingTrait
{
/**
* @var PackerInterface
*/
protected $packer;
/**
* Create the default packer for this cache implementation
*
* @return PackerInterface
*/
abstract protected static function createDefaultPacker(): PackerInterface;
/**
* Set a packer to pack (serialialize) and unpack (unserialize) the data.
*
* @param PackerInterface $packer
* @return static
*/
public function withPacker(PackerInterface $packer)
{
$cache = $this->cloneSelf();
$cache->packer = $packer;
return $cache;
}
/**
* Get the packer
*
* @return PackerInterface
*/
protected function getPacker(): PackerInterface
{
if (!isset($this->packer)) {
$this->packer = static::createDefaultPacker();
}
return $this->packer;
}
/**
* Pack the value
*
* @param mixed $value
* @return string|mixed
*/
protected function pack($value)
{
return $this->getPacker()->pack($value);
}
/**
* Unpack the data to retrieve the value
*
* @param string|mixed $packed
* @return mixed
* @throws \UnexpectedValueException
*/
protected function unpack($packed)
{
return $this->getPacker()->unpack($packed);
}
}

View File

@ -0,0 +1,78 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache\Packer;
use Desarrolla2\Cache\Packer\PackerInterface;
use Desarrolla2\Cache\Exception\InvalidArgumentException;
/**
* Pack value through serialization
*/
class SerializePacker implements PackerInterface
{
/**
* @var array
*/
protected $options;
/**
* SerializePacker constructor
*
* @param array $options Any options to be provided to unserialize()
*/
public function __construct(array $options = ['allowed_classes' => true])
{
$this->options = $options;
}
/**
* Get cache type (might be used as file extension)
*
* @return string
*/
public function getType()
{
return 'php.cache';
}
/**
* Pack the value
*
* @param mixed $value
* @return string
*/
public function pack($value)
{
return serialize($value);
}
/**
* Unpack the value
*
* @param string $packed
* @return string
* @throws \UnexpectedValueException if he value can't be unpacked
*/
public function unpack($packed)
{
if (!is_string($packed)) {
throw new InvalidArgumentException("packed value should be a string");
}
return unserialize($packed, $this->options);
}
}

View File

@ -0,0 +1,111 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache;
use Desarrolla2\Cache\AbstractFile;
use Desarrolla2\Cache\Packer\PackerInterface;
use Desarrolla2\Cache\Packer\SerializePacker;
use Desarrolla2\Cache\File\BasicFilename;
/**
* Cache file as PHP script.
*/
class PhpFile extends AbstractFile
{
/**
* Create the default packer for this cache implementation.
*
* @return PackerInterface
*/
protected static function createDefaultPacker(): PackerInterface
{
return new SerializePacker();
}
/**
* Get the filename callable
*
* @return callable
*/
protected function getFilenameOption(): callable
{
if (!isset($this->filename)) {
$this->filename = new BasicFilename('%s.php');
}
return $this->filename;
}
/**
* Create a PHP script returning the cached value
*
* @param mixed $value
* @param int|null $ttl
* @return string
*/
public function createScript($value, ?int $ttl): string
{
$macro = var_export($value, true);
if (strpos($macro, 'stdClass::__set_state') !== false) {
$macro = preg_replace_callback("/('([^'\\\\]++|''\\.)')|stdClass::__set_state/", $macro, function($match) {
return empty($match[1]) ? '(object)' : $match[1];
});
}
return $ttl !== null
? "<?php return time() < {$ttl} ? {$macro} : false;"
: "<?php return {$macro};";
}
/**
* {@inheritdoc}
*/
public function get($key, $default = null)
{
$cacheFile = $this->getFilename($key);
if (!file_exists($cacheFile)) {
return $default;
}
$packed = include $cacheFile;
return $packed === false ? $default : $this->unpack($packed);
}
/**
* {@inheritdoc}
*/
public function has($key)
{
return $this->get($key) !== null;
}
/**
* {@inheritdoc}
*/
public function set($key, $value, $ttl = null)
{
$cacheFile = $this->getFilename($key);
$packed = $this->pack($value);
$script = $this->createScript($packed, $this->ttlToTimestamp($ttl));
return $this->writeFile($cacheFile, $script);
}
}

View File

@ -0,0 +1,245 @@
<?php
/*
* This file is part of the Cache package.
*
* Copyright (c) Daniel González
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @author Daniel González <daniel@desarrolla2.com>
* @author Arnold Daniels <arnold@jasny.net>
*/
declare(strict_types=1);
namespace Desarrolla2\Cache;
use Desarrolla2\Cache\AbstractCache;
use Desarrolla2\Cache\Exception\UnexpectedValueException;
use Desarrolla2\Cache\Packer\PackerInterface;
use Desarrolla2\Cache\Packer\SerializePacker;
use Predis\Client;
use Predis\Response\ServerException;
use Predis\Response\Status;
use Predis\Response\ErrorInterface;
/**
* Predis cache adapter.
*
* Errors are silently ignored but ServerExceptions are **not** caught. To PSR-16 compliant disable the `exception`
* option.
*/
class Predis extends AbstractCache
{
/**
* @var Client
*/
protected $predis;
/**
* Class constructor
* @see predis documentation about how know your configuration https://github.com/nrk/predis
*
* @param Client $client
*/
public function __construct(Client $client)
{
$this->predis = $client;
}
/**
* Create the default packer for this cache implementation.
*
* @return PackerInterface
*/
protected static function createDefaultPacker(): PackerInterface
{
return new SerializePacker();
}
/**
* Run a predis command.
*
* @param string $cmd
* @param mixed ...$args
* @return mixed|bool
*/
protected function execCommand(string $cmd, ...$args)
{
$command = $this->predis->createCommand($cmd, $args);
$response = $this->predis->executeCommand($command);
if ($response instanceof ErrorInterface) {
return false;
}
if ($response instanceof Status) {
return $response->getPayload() === 'OK';
}
return $response;
}
/**
* Set multiple (mset) with expire
*
* @param array $dictionary
* @param int|null $ttlSeconds
* @return bool
*/
protected function msetExpire(array $dictionary, ?int $ttlSeconds): bool
{
if (empty($dictionary)) {
return true;
}
if (!isset($ttlSeconds)) {
return $this->execCommand('MSET', $dictionary);
}
$transaction = $this->predis->transaction();
foreach ($dictionary as $key => $value) {
$transaction->set($key, $value, 'EX', $ttlSeconds);
}
try {
$responses = $transaction->execute();
} catch (ServerException $e) {
return false;
}
$ok = array_reduce($responses, function($ok, $response) {
return $ok && $response instanceof Status && $response->getPayload() === 'OK';
}, true);
return $ok;
}
/**
* {@inheritdoc}
*/
public function get($key, $default = null)
{
$id = $this->keyToId($key);
$response = $this->execCommand('GET', $id);
return !empty($response) ? $this->unpack($response) : $default;
}
/**
* {@inheritdoc}
*/
public function getMultiple($keys, $default = null)
{
$idKeyPairs = $this->mapKeysToIds($keys);
$ids = array_keys($idKeyPairs);
$response = $this->execCommand('MGET', $ids);
if ($response === false) {
return false;
}
$items = [];
$packedItems = array_combine(array_values($idKeyPairs), $response);
foreach ($packedItems as $key => $packed) {
$items[$key] = isset($packed) ? $this->unpack($packed) : $default;
}
return $items;
}
/**
* {@inheritdoc}
*/
public function has($key)
{
return $this->execCommand('EXISTS', $this->keyToId($key));
}
/**
* {@inheritdoc}
*/
public function set($key, $value, $ttl = null)
{
$id = $this->keyToId($key);
$packed = $this->pack($value);
if (!is_string($packed)) {
throw new UnexpectedValueException("Packer must create a string for the data");
}
$ttlSeconds = $this->ttlToSeconds($ttl);
if (isset($ttlSeconds) && $ttlSeconds <= 0) {
return $this->execCommand('DEL', [$id]);
}
return !isset($ttlSeconds)
? $this->execCommand('SET', $id, $packed)
: $this->execCommand('SETEX', $id, $ttlSeconds, $packed);
}
/**
* {@inheritdoc}
*/
public function setMultiple($values, $ttl = null)
{
$this->assertIterable($values, 'values not iterable');
$dictionary = [];
foreach ($values as $key => $value) {
$id = $this->keyToId(is_int($key) ? (string)$key : $key);
$packed = $this->pack($value);
if (!is_string($packed)) {
throw new UnexpectedValueException("Packer must create a string for the data");
}
$dictionary[$id] = $packed;
}
$ttlSeconds = $this->ttlToSeconds($ttl);
if (isset($ttlSeconds) && $ttlSeconds <= 0) {
return $this->execCommand('DEL', array_keys($dictionary));
}
return $this->msetExpire($dictionary, $ttlSeconds);
}
/**
* {@inheritdoc}
*/
public function delete($key)
{
$id = $this->keyToId($key);
return $this->execCommand('DEL', [$id]) !== false;
}
/**
* {@inheritdoc}
*/
public function deleteMultiple($keys)
{
$ids = array_keys($this->mapKeysToIds($keys));
return empty($ids) || $this->execCommand('DEL', $ids) !== false;
}
/**
* {@inheritdoc}
*/
public function clear()
{
return $this->execCommand('FLUSHDB');
}
}