diff --git a/.travis.yml b/.travis.yml index 2938ec0..f0b6a45 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,11 +10,16 @@ addons: - redis-container php: + - "7.1" - "7.0" - "5.6" +before_install: + - echo "extension = memcached.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + - echo "extension = redis.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + install: - composer install -script: +script: - phpunit --stderr diff --git a/README.md b/README.md index 7555215..8282e1f 100644 --- a/README.md +++ b/README.md @@ -6,41 +6,55 @@ ## Description -A multi-purpose cache engine in PHP with several drivers. PSR-6 compliant. +A multi-purpose cache engine PSR-6 and PSR-16 implementation with several drivers. -## Avaible cache engines +## Cache Engine PSR-16 compliant + +PSR-16 defines a Simple Cache interface with less verbosity than PSR-6. Below a list +of engines available in this library that is PSR-16 compliant: + +| Class | Description | +|:----------------------------------------|:--------------------------------------------------------------------| +| \ByJG\Cache\Psr16\NoCacheEngine | Do nothing. Use it for disable the cache without change your code | +| \ByJG\Cache\Psr16\ArrayCacheEngine | Local cache only using array. It does not persists between requests | +| \ByJG\Cache\Psr16\FileSystemCacheEngine | Save the cache result in the local file system | +| \ByJG\Cache\Psr16\MemcachedEngine | Uses the Memcached as the cache engine | +| \ByJG\Cache\Psr16\SessionCachedEngine | uses the PHP session as cache | +| \ByJG\Cache\Psr16\ShmopCachedEngine | uses the shared memory area for cache | + +To create a new Cache Instance just create the proper cache engine and use it: -| Class | Description | -|:----------------------------------|:--------------------------------------------------------------------| -| \ByJG\Cache\NoCacheEngine | Do nothing. Use it for disable the cache without change your code | -| \ByJG\Cache\ArrayCacheEngine | Local cache only using array. It does not persists between requests | -| \ByJG\Cache\FileSystemCacheEngine | Save the cache result in the local file system | -| \ByJG\Cache\MemcachedEngine | Uses the Memcached as the cache engine | -| \ByJG\Cache\SessionCachedEngine | uses the PHP session as cache | -| \ByJG\Cache\ShmopCachedEngine | uses the shared memory area for cache | +```php +get('key'); +$cache->set('key', 'value'); +if ($cache->has('key')) { + //... +}; +``` -## Create new cache instance +## Cache Engine PSR-6 compliant -### Creating a PSR-6 compatible instance +The PSR-6 implementation use the engines defined above. PSR-6 is more verbosity and +have an extra layer do get and set the cache values. -You can set instance in the 'cacheconfig.php' setup (see below how to configure the factory) +You can use one of the factory methods to create a instance of the CachePool implementation: ```php + 524288, 'default-permission' => '0700' ]` +## Logging cache commands + +You can add a PSR Log compatible to the constructor in order to get Log of the operations + ## Install -Just type: `composer require "byjg/cache-engine=3.0.*"` +Just type: `composer require "byjg/cache-engine=4.0.*"` ## Running Unit Testes diff --git a/composer.json b/composer.json index b77187a..0b161e3 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "byjg/cache-engine", - "description": "A multi-purpose cache engine in PHP with several drivers. PSR-6 compliant.", + "description": "A multi-purpose cache engine PSR-6 and PSR-16 implementation with several drivers.", "authors": [ { "name": "João Gilberto Magalhães", @@ -15,7 +15,8 @@ "require": { "php": ">=5.4.0", "psr/cache": "1.0.*", - "psr/log": "1.0.2" + "psr/log": "1.0.2", + "psr/simple-cache": "1.0.*" }, "suggest": { "ext-memcached": "*" diff --git a/src/CacheAvailabilityInterface.php b/src/CacheAvailabilityInterface.php new file mode 100644 index 0000000..9e66d23 --- /dev/null +++ b/src/CacheAvailabilityInterface.php @@ -0,0 +1,13 @@ +logger = $logger; - if (is_null($logger)) { - $this->logger = new NullLogger(); - } - } - - /** - * @param string $key The object KEY - * @param int $ttl IGNORED IN MEMCACHED. - * @return object Description - */ - public function get($key, $ttl = 0) - { - - - if (array_key_exists($key, $this->_L1Cache)) { - $this->logger->info("[Array cache] Get '$key' from L1 Cache"); - return $this->_L1Cache[$key]; - } else { - $this->logger->info("[Array cache] Not found '$key'"); - return null; - } - } - - /** - * @param string $key The object Key - * @param object $object The object to be cached - * @param int $ttl The time to live in seconds of this objects - * @return bool If the object is successfully posted - */ - public function set($key, $object, $ttl = 0) - { - $this->logger->info("[Array cache] Set '$key' in L1 Cache"); - - $this->_L1Cache[$key] = $object; - - return true; - } - - /** - * - * @param string $key - * @param string $str - * @return bool - */ - public function append($key, $str) - { - $this->logger->info("[Array cache] Append '$key' in L1 Cache"); - - $this->_L1Cache[$key] = $this->_L1Cache[$key] . $str; - - return true; - } - - /** - * Unlock resource - * @param string $key - */ - public function release($key) - { - unset($this->_L1Cache[$key]); - } - - /** - * Lock resource before set it. - * @param string $key - */ - public function lock($key) - { - return; - } - - /** - * UnLock resource after set it - * @param string $key - */ - public function unlock($key) - { - return; - } - - public function isAvailable() - { - return true; - } -} diff --git a/src/Engine/NoCacheEngine.php b/src/Engine/NoCacheEngine.php deleted file mode 100644 index 3eb4db8..0000000 --- a/src/Engine/NoCacheEngine.php +++ /dev/null @@ -1,75 +0,0 @@ -logger = $logger; + if (is_null($logger)) { + $this->logger = new NullLogger(); + } + } + + /** + * Determines whether an item is present in the cache. + * NOTE: It is recommended that has() is only to be used for cache warming type purposes + * and not to be used within your live applications operations for get/set, as this method + * is subject to a race condition where your has() will return true and immediately after, + * another script can remove it making the state of your app out of date. + * + * @param string $key The cache item key. + * @return bool + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if the $key string is not a legal value. + */ + public function has($key) + { + if (isset($this->cache[$key])) { + if (isset($this->cache["$key.ttl"]) && time() >= $this->cache["$key.ttl"]) { + $this->delete($key); + return false; + } + + return true; + } + + return false; + } + + /** + * @param string $key The object KEY + * @param mixed $default IGNORED IN MEMCACHED. + * @return mixed Description + */ + public function get($key, $default = null) + { + if ($this->has($key)) { + $this->logger->info("[Array cache] Get '$key' from L1 Cache"); + return $this->cache[$key]; + } else { + $this->logger->info("[Array cache] Not found '$key'"); + return $default; + } + } + + /** + * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time. + * + * @param string $key The key of the item to store. + * @param mixed $value The value of the item to store, must be serializable. + * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and + * the driver supports TTL then the library may set a default value + * for it or let the driver take care of that. + * + * @return bool True on success and false on failure. + * + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if the $key string is not a legal value. + */ + public function set($key, $value, $ttl = null) + { + $this->logger->info("[Array cache] Set '$key' in L1 Cache"); + + $this->cache[$key] = $value; + if (!empty($ttl)) { + $this->cache["$key.ttl"] = $this->addToNow($ttl); + } + + return true; + } + + public function clear() + { + $this->cache = []; + } + + /** + * Unlock resource + * + * @param string $key + * @return bool + */ + public function delete($key) + { + unset($this->cache[$key]); + unset($this->cache["$key.ttl"]); + return true; + } + + public function isAvailable() + { + return true; + } +} diff --git a/src/Psr16/BaseCacheEngine.php b/src/Psr16/BaseCacheEngine.php new file mode 100644 index 0000000..2c49ef4 --- /dev/null +++ b/src/Psr16/BaseCacheEngine.php @@ -0,0 +1,53 @@ +get($key, $default); + } + return $result; + } + + public function setMultiple($values, $ttl = null) + { + foreach ($values as $key => $value) { + $this->set($key, $value, $ttl); + } + } + + public function deleteMultiple($keys) + { + foreach ($keys as $key) { + $this->delete($key); + } + } + + abstract public function isAvailable(); + + protected function addToNow($ttl) + { + if (is_numeric($ttl)) { + return strtotime("+$ttl second"); + } + + if ($ttl instanceof \DateInterval) { + $now = new \DateTime(); + $now->add($ttl); + return $now->getTimestamp(); + } + + return null; + } +} \ No newline at end of file diff --git a/src/Engine/FileSystemCacheEngine.php b/src/Psr16/FileSystemCacheEngine.php similarity index 52% rename from src/Engine/FileSystemCacheEngine.php rename to src/Psr16/FileSystemCacheEngine.php index f7f9bd7..9fd19cf 100644 --- a/src/Engine/FileSystemCacheEngine.php +++ b/src/Psr16/FileSystemCacheEngine.php @@ -1,12 +1,12 @@ logger->info("[Filesystem cache] Ignored $key because TTL=FALSE"); - return null; - } - // Check if file is Locked $fileKey = $this->fixKey($key); $lockFile = $fileKey . ".lock"; @@ -50,36 +45,37 @@ public function get($key, $ttl = 0) if (intval(time() - $lockTime) > 20) { // Wait for 10 seconds $this->logger->info("[Filesystem cache] Gave up to wait unlock. Release lock for '$key'"); $this->unlock($key); - return null; + return $default; } sleep(1); // 1 second } } // Check if file exists - if (file_exists($fileKey)) { - $fileAge = filemtime($fileKey); - - if (($ttl > 0) && (intval(time() - $fileAge) > $ttl)) { - $this->logger->info("[Filesystem cache] File too old. Ignoring '$key'"); - return null; - } else { - $this->logger->info("[Filesystem cache] Get '$key'"); - return unserialize(file_get_contents($fileKey)); - } + if ($this->has($key)) { + $this->logger->info("[Filesystem cache] Get '$key'"); + return unserialize(file_get_contents($fileKey)); } else { $this->logger->info("[Filesystem cache] Not found '$key'"); - return null; + return $default; } } /** - * @param string $key The object Key - * @param object $object The object to be cached - * @param int $ttl The time to live in seconds of this objects - * @return bool If the object is successfully posted + * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time. + * + * @param string $key The key of the item to store. + * @param mixed $value The value of the item to store, must be serializable. + * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and + * the driver supports TTL then the library may set a default value + * for it or let the driver take care of that. + * + * @return bool True on success and false on failure. + * + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if the $key string is not a legal value. */ - public function set($key, $object, $ttl = 0) + public function set($key, $value, $ttl = null) { $fileKey = $this->fixKey($key); @@ -88,16 +84,22 @@ public function set($key, $object, $ttl = 0) try { if (file_exists($fileKey)) { unlink($fileKey); + unlink("$fileKey.ttl"); } - if (is_null($object)) { + if (is_null($value)) { return false; } - if (is_string($object) && (strlen($object) === 0)) { + if (is_string($value) && (strlen($value) === 0)) { touch($fileKey); } else { - file_put_contents($fileKey, serialize($object)); + file_put_contents($fileKey, serialize($value)); + } + + $validUntil = $this->addToNow($ttl); + if (!empty($validUntil)) { + file_put_contents($fileKey . ".ttl", $validUntil); } } catch (Exception $ex) { $this->logger->warning("[Filesystem cache] I could not write to cache on file '" . basename($key) . "'. Switching to nocache=true mode."); @@ -108,33 +110,12 @@ public function set($key, $object, $ttl = 0) } /** - * Unlock resource * @param string $key + * @return bool */ - public function release($key) + public function delete($key) { $this->set($key, null); - } - - /** - * @param string $key The object Key - * @param string $content The object to be cached - * @param int $ttl The time to live in seconds of this objects - * @return bool If the object is successfully posted - */ - public function append($key, $content, $ttl = 0) - { - $fileKey = $this->fixKey($key); - - $this->logger->info("[Filesystem cache] Append '$key' in FileSystem"); - - try { - file_put_contents($fileKey, serialize($content), true); - } catch (Exception $ex) { - $this->logger->warning("[Filesystem cache] I could not write to cache on file '" . basename($key) . "'. Switching to nocache=true mode."); - return false; - } - return true; } @@ -183,4 +164,52 @@ protected function fixKey($key) . '-' . preg_replace("/[\/\\\]/", "#", $key) . '.cache'; } + + /** + * Wipes clean the entire cache's keys. + * + * @return bool True on success and false on failure. + */ + public function clear() + { + $patternKey = $this->fixKey('*'); + $list = glob($patternKey); + foreach ($list as $file) { + unlink($file); + } + return true; + } + + /** + * Determines whether an item is present in the cache. + * NOTE: It is recommended that has() is only to be used for cache warming type purposes + * and not to be used within your live applications operations for get/set, as this method + * is subject to a race condition where your has() will return true and immediately after, + * another script can remove it making the state of your app out of date. + * + * @param string $key The cache item key. + * @return bool + * @throws \Psr\SimpleCache\InvalidArgumentException + * MUST be thrown if the $key string is not a legal value. + */ + public function has($key) + { + $fileKey = $this->fixKey($key); + if (file_exists($fileKey)) { + if (file_exists("$fileKey.ttl")) { + $fileTtl = intval(file_get_contents("$fileKey.ttl")); + } + + if (!empty($fileTtl) && time() >= $fileTtl) { + $this->logger->info("[Filesystem cache] File too old. Ignoring '$key'"); + $this->delete($key); + + return false; + } + + return true; + } + + return false; + } } diff --git a/src/Engine/MemcachedEngine.php b/src/Psr16/MemcachedEngine.php similarity index 69% rename from src/Engine/MemcachedEngine.php rename to src/Psr16/MemcachedEngine.php index ef87bc9..9349ca7 100644 --- a/src/Engine/MemcachedEngine.php +++ b/src/Psr16/MemcachedEngine.php @@ -1,12 +1,11 @@ memCached)) { @@ -52,33 +55,33 @@ protected function lazyLoadMemCachedServers() /** * @param string $key The object KEY - * @param int $ttl IGNORED IN MEMCACHED. - * @return object Description + * @param int $default IGNORED IN MEMCACHED. + * @return mixed Description */ - public function get($key, $ttl = 0) + public function get($key, $default = null) { $this->lazyLoadMemCachedServers(); - $value = $this->memCached->get($key); + $value = $this->memCached->get($this->fixKey($key)); if ($this->memCached->getResultCode() !== Memcached::RES_SUCCESS) { $this->logger->info("[Memcached] Cache '$key' missed with status " . $this->memCached->getResultCode()); - return null; + return $default; } - return $value; + return unserialize($value); } /** * @param string $key The object Key - * @param object $object The object to be cached + * @param object $value The object to be cached * @param int $ttl The time to live in seconds of this objects * @return bool If the object is successfully posted */ - public function set($key, $object, $ttl = 0) + public function set($key, $value, $ttl = null) { $this->lazyLoadMemCachedServers(); - $this->memCached->set($key, $object, $ttl); + $this->memCached->set($this->fixKey($key), serialize($value), $ttl); $this->logger->info("[Memcached] Set '$key' result " . $this->memCached->getResultCode()); if ($this->memCached->getResultCode() !== Memcached::RES_SUCCESS) { $this->logger->error("[Memcached] Set '$key' failed with status " . $this->memCached->getResultCode()); @@ -88,50 +91,15 @@ public function set($key, $object, $ttl = 0) } /** - * Unlock resource - * @param string $key - */ - public function release($key) - { - $this->lazyLoadMemCachedServers(); - - $this->memCached->delete($key); - } - - /** - * * @param string $key - * @param string $str * @return bool */ - public function append($key, $str) - { - $this->lazyLoadMemCachedServers(); - - $this->logger->info("[Memcached] Append '$key' in Memcached"); - return $this->memCached->append($key, $str); - } - - /** - * Lock resource before set it. - * @param string $key - */ - public function lock($key) - { - $this->lazyLoadMemCachedServers(); - - return; - } - - /** - * UnLock resource after set it - * @param string $key - */ - public function unlock($key) + public function delete($key) { $this->lazyLoadMemCachedServers(); - return; + $this->memCached->delete($this->fixKey($key)); + return true; } public function isAvailable() @@ -147,4 +115,19 @@ public function isAvailable() return false; } } + + public function clear() + { + $this->lazyLoadMemCachedServers(); + $result = $this->memCached->flush(); + return $result; + } + + public function has($key) + { + $this->lazyLoadMemCachedServers(); + + $this->memCached->get($this->fixKey($key)); + return ($this->memCached->getResultCode() === Memcached::RES_SUCCESS); + } } diff --git a/src/Psr16/NoCacheEngine.php b/src/Psr16/NoCacheEngine.php new file mode 100644 index 0000000..d9d5c20 --- /dev/null +++ b/src/Psr16/NoCacheEngine.php @@ -0,0 +1,88 @@ +lazyLoadRedisServer(); - $value = $this->redis->get($key); + $value = $this->redis->get($this->fixKey($key)); $this->logger->info("[Redis Cache] Get '$key' result "); - return ($value === false ? null : $value); + return ($value === false ? $default : unserialize($value)); } /** * @param string $key The object Key - * @param object $object The object to be cached + * @param object $value The object to be cached * @param int $ttl The time to live in seconds of this objects * @return bool If the object is successfully posted */ - public function set($key, $object, $ttl = 0) + public function set($key, $value, $ttl = null) { $this->lazyLoadRedisServer(); - $this->redis->set($key, $object, $ttl); + $this->redis->set($this->fixKey($key), serialize($value), $ttl); $this->logger->info("[Redis Cache] Set '$key' result "); return true; } - /** - * Unlock resource - * @param string $key - */ - public function release($key) + public function delete($key) { $this->lazyLoadRedisServer(); - $this->redis->delete($key); - } - - /** - * - * @param string $key - * @param string $str - * @return bool - */ - public function append($key, $str) - { - $this->lazyLoadRedisServer(); + $this->redis->delete($this->fixKey($key)); - $this->logger->info("[Redis Cache] Append '$key' in Memcached"); - return $this->redis->append($key, $str); + return true; } - /** - * Lock resource before set it. - * @param string $key - */ - public function lock($key) + public function clear() { - $this->lazyLoadRedisServer(); - - return; + $keys = $this->redis->keys('cache:*'); + foreach ((array)$keys as $key) { + if (preg_match('/^cache\:(?.*)/', $key, $matches)) { + $this->delete($matches['key']); + } + } } - /** - * UnLock resource after set it - * @param string $key - */ - public function unlock($key) + public function has($key) { - $this->lazyLoadRedisServer(); - - return; + return $this->redis->exists($this->fixKey($key)); } public function isAvailable() diff --git a/src/Engine/SessionCacheEngine.php b/src/Psr16/SessionCacheEngine.php similarity index 59% rename from src/Engine/SessionCacheEngine.php rename to src/Psr16/SessionCacheEngine.php index e344d39..0ad0e06 100644 --- a/src/Engine/SessionCacheEngine.php +++ b/src/Psr16/SessionCacheEngine.php @@ -1,10 +1,8 @@ prefix . '-' . $key; } - public function append($key, $str) + public function get($key, $default = null) { $this->checkSession(); $keyName = $this->keyName($key); - $current = $this->get($keyName); - if ($current === false) { - $this->set($keyName, $str); + if ($this->has($key)) { + return $_SESSION[$keyName]; } else { - $this->set($keyName, $current . $str); + return $default; } } - public function get($key, $ttl = 0) + public function delete($key) { $this->checkSession(); $keyName = $this->keyName($key); if (isset($_SESSION[$keyName])) { - return $_SESSION[$keyName]; - } else { - return null; + unset($_SESSION[$keyName]); + } + if (isset($_SESSION["$keyName.ttl"])) { + unset($_SESSION["$keyName.ttl"]); } } - public function lock($key) - { - $this->checkSession(); - - // Nothing to implement here; - } - - public function release($key) + public function set($key, $value, $ttl = null) { $this->checkSession(); $keyName = $this->keyName($key); - - if (isset($_SESSION[$keyName])) { - unset($_SESSION[$keyName]); + $_SESSION[$keyName] = $value; + if (!empty($ttl)) { + $_SESSION["$keyName.ttl"] = $this->addToNow($ttl); } } - public function set($key, $object, $ttl = 0) + public function clear() { - $this->checkSession(); - - $keyName = $this->keyName($key); - $_SESSION[$keyName] = $object; + session_destroy(); } - public function unlock($key) + public function has($key) { - $this->checkSession(); + $keyName = $this->keyName($key); + + if (isset($_SESSION[$keyName])) { + if (isset($_SESSION["$keyName.ttl"]) && time() >= $_SESSION["$keyName.ttl"]) { + $this->delete($key); + return false; + } + + return true; + } - // Nothing to implement here; + return false; } public function isAvailable() diff --git a/src/Engine/ShmopCacheEngine.php b/src/Psr16/ShmopCacheEngine.php similarity index 52% rename from src/Engine/ShmopCacheEngine.php rename to src/Psr16/ShmopCacheEngine.php index 7557961..551a94e 100644 --- a/src/Engine/ShmopCacheEngine.php +++ b/src/Psr16/ShmopCacheEngine.php @@ -1,9 +1,8 @@ config['default-permission']; } - protected function getKeyId($key) + protected function getFTok($file) { - $file = $this->getFTok($key); if (!file_exists($file)) { touch($file); } @@ -71,41 +68,28 @@ protected function getKeyId($key) /** * @param string $key The object KEY - * @param int $ttl The time to live in seconds of the object. Depends on implementation. - * @return object The Object + * @param mixed $default The time to live in seconds of the object. Depends on implementation. + * @return mixed The Object */ - public function get($key, $ttl = 0) + public function get($key, $default = null) { - - - if ($ttl === false) { + if ($default === false) { $this->logger->info("[Shmop Cache] Ignored $key because TTL=FALSE"); - return null; + return $default; } - $fileKey = $this->getKeyId($key); + $file = $this->getFilenameToken($key); + $fileKey = $this->getFTok($file); // Opened $shm_id = @shmop_open($fileKey, "a", 0, 0); if (!$shm_id) { $this->logger->info("[Shmop Cache] '$key' not exists"); - return null; + return $default; } - $fileAge = filemtime($this->getFTok($key)); - - // Check - if (($ttl > 0) && (intval(time() - $fileAge) > $ttl)) { - $this->logger->info("[Shmop Cache] File too old. Ignoring '$key'"); - - // Close old descriptor - shmop_close($shm_id); - - // delete old memory segment - $shm_id = shmop_open($fileKey, "w", $this->getDefaultPermission(), $this->getMaxSize()); - shmop_delete($shm_id); - shmop_close($shm_id); - return null; + if (!$this->isValidAge($file)) { + return $default; } $this->logger->info("[Shmop Cache] Get '$key'"); @@ -116,27 +100,47 @@ public function get($key, $ttl = 0) return unserialize($serialized); } + protected function isValidAge($file) + { + if (file_exists("$file.ttl")) { + $fileTtl = intval(file_get_contents("$file.ttl")); + } + + if (!empty($fileTtl) && time() >= $fileTtl) { + $this->logger->info("[Shmop Cache] File too old. Ignoring"); + $this->deleteFromFilenameToken($file); + return false; + } + + return true; + } + /** - * @param string $key The object Key - * @param object $object The object to be cached - * @param int $ttl The time to live in seconds of the object. Depends on implementation. - * @return bool If the object is successfully posted - * @throws \Exception + * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time. + * + * @param string $key The key of the item to store. + * @param mixed $value The value of the item to store, must be serializable. + * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and + * the driver supports TTL then the library may set a default value + * for it or let the driver take care of that. + * @return bool True on success and false on failure. + * @throws InvalidArgumentException */ - public function set($key, $object, $ttl = 0) + public function set($key, $value, $ttl = null) { $this->logger->info("[Shmop Cache] set '$key'"); - $this->release($key); + $this->delete($key); - $serialized = serialize($object); + $serialized = serialize($value); $size = strlen($serialized); if ($size > $this->getMaxSize()) { - throw new \Exception('Object is greater than the max size allowed: ' . $this->getMaxSize()); + throw new \ByJG\Cache\InvalidArgumentException('Object is greater than the max size allowed: ' . $this->getMaxSize()); } - $shmKey = $this->getKeyId($key); + $file = $this->getFilenameToken($key); + $shmKey = $this->getFTok($file); $shm_id = shmop_open($shmKey, "c", 0777, $size); if (!$shm_id) { $message = "Couldn't create shared memory segment"; @@ -144,7 +148,7 @@ public function set($key, $object, $ttl = 0) if (isset($lastError['message'])) { $message = $lastError['message']; } - throw new \Exception($message); + throw new \ByJG\Cache\InvalidArgumentException($message); } $shm_bytes_written = shmop_write($shm_id, $serialized, 0); @@ -154,83 +158,81 @@ public function set($key, $object, $ttl = 0) } shmop_close($shm_id); - return true; - } - - /** - * Append only will work with strings. - * - * @param string $key - * @param string $str - * @return bool - */ - public function append($key, $str) - { - $old = $this->get($key); - if ($old === false) { - $this->set($key, $str); - } else { - $oldUn = unserialize($old); - if (is_string($oldUn)) { - $this->release($key); - $this->set($key, $oldUn . $str); - } else { - throw new InvalidArgumentException('Only is possible append string types'); - } + $validUntil = $this->addToNow($ttl); + if (!empty($validUntil)) { + file_put_contents("$file.ttl", $validUntil); } return true; } /** - * Lock resource before set it. - * @param string $key - */ - public function lock($key) - { - - } - - /** - * Unlock resource - * @param string $key - */ - public function unlock($key) - { - - } - - /** - * Release the object * @param string $key + * @return bool */ - public function release($key) + public function delete($key) { - - $this->logger->info("[Shmop Cache] release '$key'"); if ($this->get($key) === false) { $this->logger->info("[Shmop Cache] release '$key' does not exists."); - return; + return false; } - $filekey = $this->getKeyId($key); + $file = $this->getFilenameToken($key); + $this->deleteFromFilenameToken($file); + return true; + } + + private function deleteFromFilenameToken($file) + { + $filekey = $this->getFTok($file); $shm_id = @shmop_open($filekey, "w", 0, 0); - $file = $this->getFTok($key); if (file_exists($file)) { unlink($file); } + if (file_exists("$file.ttl")) { + unlink("$file.ttl"); + } + if ($shm_id) { shmop_delete($shm_id); shmop_close($shm_id); - $this->logger->info("[Shmop Cache] release '$key' confirmed."); + $this->logger->info("[Shmop Cache] release confirmed."); } } + public function clear() + { + $patternKey = sys_get_temp_dir() . '/shmop-*.cache'; + $list = glob($patternKey); + foreach ($list as $file) { + $this->deleteFromFilenameToken($file); + } + } + + public function has($key) + { + $file = $this->getFilenameToken($key); + $fileKey = $this->getFTok($file); + + // Opened + $shm_id = @shmop_open($fileKey, "a", 0, 0); + + $exists = !(!$shm_id); + + if ($exists) { + shmop_close($shm_id); + return $this->isValidAge($file); + } + + return $exists; + } + + public function isAvailable() { return function_exists('shmop_open'); diff --git a/src/Psr/CacheItem.php b/src/Psr6/CacheItem.php similarity index 95% rename from src/Psr/CacheItem.php rename to src/Psr6/CacheItem.php index 6802a09..c94ad53 100644 --- a/src/Psr/CacheItem.php +++ b/src/Psr6/CacheItem.php @@ -1,12 +1,6 @@ _cacheEngine = $_cacheEngine; $this->bufferSize = intval($bufferSize); @@ -186,7 +186,7 @@ public function deleteItem($key) public function deleteItems(array $keys) { foreach ($keys as $key) { - $this->_cacheEngine->release($key); + $this->_cacheEngine->delete($key); $this->removeElementFromBuffer($key); } diff --git a/tests/BaseCacheTest.php b/tests/BaseCacheTest.php new file mode 100644 index 0000000..87f782d --- /dev/null +++ b/tests/BaseCacheTest.php @@ -0,0 +1,62 @@ +cacheEngine->clear(); + $this->cacheEngine = null; + } + + public function CachePoolProvider() + { + $memcachedServer = ['memcached-container:11211']; + $redisCacheServer = 'redis-container:6379'; + $redisPassword = ''; + + return [ + 'Array' => [ + new \ByJG\Cache\Psr16\ArrayCacheEngine() + ], + 'FileSystem' => [ + new \ByJG\Cache\Psr16\FileSystemCacheEngine() + ], + 'ShmopCache' => [ + new \ByJG\Cache\Psr16\ShmopCacheEngine() + ], + 'SessionCache' => [ + new \ByJG\Cache\Psr16\SessionCacheEngine() + ], + 'NoCacheEngine' => [ + new \ByJG\Cache\Psr16\NoCacheEngine() + ], + 'Memcached' => [ + new \ByJG\Cache\Psr16\MemcachedEngine($memcachedServer) + ], + 'Redis' => [ + new \ByJG\Cache\Psr16\RedisCacheEngine($redisCacheServer, $redisPassword) + ] + ]; + } +} diff --git a/tests/CachePSR16Test.php b/tests/CachePSR16Test.php new file mode 100644 index 0000000..a28b5f1 --- /dev/null +++ b/tests/CachePSR16Test.php @@ -0,0 +1,194 @@ +cacheEngine = $cacheEngine; + + if ($cacheEngine->isAvailable()) { + // First time + $item = $cacheEngine->get('chave', null); + $this->assertEquals(null, $item); + $item = $cacheEngine->get('chave', 'default'); + $this->assertEquals('default', $item); + + // Set object + $cacheEngine->set('chave', 'valor'); + + // Get Object + if (!($cacheEngine instanceof \ByJG\Cache\Psr16\NoCacheEngine)) { + $item2 = $cacheEngine->get('chave', 'default'); + $this->assertEquals('valor', $item2); + } + + // Remove + $cacheEngine->delete('chave'); + + // Check Removed + $item = $cacheEngine->get('chave'); + $this->assertEquals(null, $item); + } else { + $this->markTestIncomplete('Object is not fully functional'); + } + } + + /** + * @dataProvider CachePoolProvider + * @param \ByJG\Cache\Psr16\BaseCacheEngine $cacheEngine + */ + public function testGetMultipleItems(\ByJG\Cache\Psr16\BaseCacheEngine $cacheEngine) + { + $this->cacheEngine = $cacheEngine; + + if ($cacheEngine->isAvailable()) { + // First time + $items = $cacheEngine->getMultiple(['chave1', 'chave2']); + $this->assertEquals(null, $items['chave1']); + $this->assertEquals(null, $items['chave2']); + $items = $cacheEngine->getMultiple(['chave1', 'chave2'], 'default'); + $this->assertEquals('default', $items['chave1']); + $this->assertEquals('default', $items['chave2']); + + // Set object + $cacheEngine->set('chave1', 'valor1'); + $cacheEngine->set('chave2', 'valor2'); + + // Get Object + if (!($cacheEngine instanceof \ByJG\Cache\Psr16\NoCacheEngine)) { + $item2 = $cacheEngine->getMultiple(['chave1', 'chave2']); + $this->assertEquals('valor1', $item2['chave1']); + $this->assertEquals('valor2', $item2['chave2']); + } + + // Remove + $cacheEngine->deleteMultiple(['chave1', 'chave2']); + + // Check Removed + $items = $cacheEngine->getMultiple(['chave1', 'chave2']); + $this->assertEquals(null, $items['chave1']); + $this->assertEquals(null, $items['chave2']); + } else { + $this->markTestIncomplete('Object is not fully functional'); + } + } + + /** + * @dataProvider CachePoolProvider + * @param \ByJG\Cache\Psr16\BaseCacheEngine $cacheEngine + */ + public function testTtl(\ByJG\Cache\Psr16\BaseCacheEngine $cacheEngine) + { + $this->cacheEngine = $cacheEngine; + + if ($cacheEngine->isAvailable()) { + // First time + $item = $cacheEngine->get('chave'); + $this->assertEquals(null, $item); + $this->assertFalse($cacheEngine->has('chave')); + $item2 = $cacheEngine->get('chave2'); + $this->assertEquals(null, $item2); + $this->assertFalse($cacheEngine->has('chave2')); + + // Set object + $cacheEngine->set('chave', 'valor', 2); + $cacheEngine->set('chave2', 'valor2', 2); + + // Get Object + if (!($cacheEngine instanceof \ByJG\Cache\Psr16\NoCacheEngine)) { + $item2 = $cacheEngine->get('chave'); + $this->assertEquals('valor', $item2); + $this->assertTrue($cacheEngine->has('chave2')); + sleep(3); + $item2 = $cacheEngine->get('chave'); + $this->assertEquals(null, $item2); + $this->assertFalse($cacheEngine->has('chave2')); + } + } else { + $this->markTestIncomplete('Object is not fully functional'); + } + } + + /** + * @dataProvider CachePoolProvider + * @param \ByJG\Cache\Psr16\BaseCacheEngine $cacheEngine + */ + public function testCacheObject(\ByJG\Cache\Psr16\BaseCacheEngine $cacheEngine) + { + $this->cacheEngine = $cacheEngine; + + if ($cacheEngine->isAvailable()) { + // First time + $item = $cacheEngine->get('chave'); + $this->assertEquals(null, $item); + + // Set object + $model = new Model(10, 20); + $cacheEngine->set('chave', $model); + + // Get Object + if (!($cacheEngine instanceof \ByJG\Cache\Psr16\NoCacheEngine)) { + $item2 = $cacheEngine->get('chave'); + $this->assertEquals($model, $item2); + } + + // Delete + $cacheEngine->delete('chave'); + $item = $cacheEngine->get('chave'); + $this->assertEquals(null, $item); + } else { + $this->markTestIncomplete('Object is not fully functional'); + } + } + + /** + * @dataProvider CachePoolProvider + * @param \ByJG\Cache\Psr16\BaseCacheEngine $cacheEngine + */ + public function testClear(\ByJG\Cache\Psr16\BaseCacheEngine $cacheEngine) + { + $this->cacheEngine = $cacheEngine; + + if ($cacheEngine->isAvailable()) { + // Values + $empty = [ + 'chave' => null, + 'chave2' => null, + 'chave3' => null + ]; + $set = [ + 'chave' => 'val', + 'chave2' => 'val2', + 'chave3' => 'val3' + ]; + + // First time + $item = $cacheEngine->getMultiple(['chave', 'chave2', 'chave3']); + $this->assertEquals($empty, $item); + + // Set and Check + $cacheEngine->setMultiple($set); + if (!($cacheEngine instanceof NoCacheEngine)) { + $item = $cacheEngine->getMultiple(['chave', 'chave2', 'chave3']); + $this->assertEquals($set, $item); + } + + // Clear and Check + $cacheEngine->clear(); + $item = $cacheEngine->getMultiple(['chave', 'chave2', 'chave3']); + $this->assertEquals($empty, $item); + } else { + $this->markTestIncomplete('Object is not fully functional'); + } + } +} diff --git a/tests/CachePSR6Test.php b/tests/CachePSR6Test.php new file mode 100644 index 0000000..aa956ab --- /dev/null +++ b/tests/CachePSR6Test.php @@ -0,0 +1,164 @@ +cacheEngine = $cacheEngine; + + $object = new CachePool($cacheEngine); + if ($object->isAvailable()) { + // First time + $item = $object->getItem('chave'); + $this->assertFalse($item->isHit()); + + // Set object + $item->set('valor'); + $object->save($item); + $this->assertTrue($item->isHit()); + + // Get Object + $item2 = $object->getItem('chave'); + $this->assertTrue($item2->isHit()); + $this->assertEquals('valor', $item2->get()); + + // Remove + $object->deleteItem('chave'); + + // Check Removed + $item = $object->getItem('chave'); + $this->assertFalse($item->isHit()); + } else { + $this->markTestIncomplete('Object is not fully functional'); + } + } + + /** + * @dataProvider CachePoolProvider + * @param \ByJG\Cache\Psr16\BaseCacheEngine $cacheEngine + */ + public function testGetMultipleItems(\ByJG\Cache\Psr16\BaseCacheEngine $cacheEngine) + { + $this->cacheEngine = $cacheEngine; + + $object = new CachePool($cacheEngine); + if ($object->isAvailable()) { + // First time + $items = $object->getItems(['chave1', 'chave2']); + $this->assertFalse($items[0]->isHit()); + $this->assertFalse($items[1]->isHit()); + + // Set object + $items[0]->set('valor1'); + $items[1]->set('valor2'); + $object->saveDeferred($items[0]); + $object->saveDeferred($items[1]); + $object->commit(); + $this->assertTrue($items[0]->isHit()); + $this->assertTrue($items[1]->isHit()); + + // Get Object + $item2 = $object->getItems(['chave1', 'chave2']); + $this->assertTrue($item2[0]->isHit()); + $this->assertTrue($item2[1]->isHit()); + $this->assertEquals('valor1', $item2[0]->get()); + $this->assertEquals('valor2', $item2[1]->get()); + + // Remove + $object->deleteItems(['chave1', 'chave2']); + + // Check Removed + $items = $object->getItems(['chave1', 'chave2']); + $this->assertFalse($items[0]->isHit()); + $this->assertFalse($items[1]->isHit()); + } else { + $this->markTestIncomplete('Object is not fully functional'); + } + } + + /** + * @dataProvider CachePoolProvider + * @param \ByJG\Cache\Psr16\BaseCacheEngine $cacheEngine + */ + public function testTtl(\ByJG\Cache\Psr16\BaseCacheEngine $cacheEngine) + { + $this->cacheEngine = $cacheEngine; + + $object = new CachePool($cacheEngine); + if ($object->isAvailable()) { + // First time + $item = $object->getItem('chave'); + $this->assertFalse($item->isHit()); + + // Set object + $item->set('valor'); + $item->expiresAfter(2); + $object->save($item); + $this->assertTrue($item->isHit()); + + // Get Object + $item2 = $object->getItem('chave'); + $this->assertTrue($item2->isHit()); + $this->assertEquals('valor', $item2->get()); + sleep(3); + $item3 = $object->getItem('chave'); + $this->assertFalse($item3->isHit()); + $this->assertEquals(null, $item3->get()); + + // Remove + $object->deleteItem('chave'); + + // Check Removed + $item = $object->getItem('chave'); + $this->assertFalse($item->isHit()); + } else { + $this->markTestIncomplete('Object is not fully functional'); + } + } + + /** + * @dataProvider CachePoolProvider + * @param \ByJG\Cache\Psr16\BaseCacheEngine $cacheEngine + */ + public function testCacheObject(\ByJG\Cache\Psr16\BaseCacheEngine $cacheEngine) + { + $this->cacheEngine = $cacheEngine; + + $object = new CachePool($cacheEngine); + if ($object->isAvailable()) { + // First time + $item = $object->getItem('chave'); + $this->assertFalse($item->isHit()); + + // Set object + $model = new Model(10, 20); + $item->set($model); + $object->save($item); + $this->assertTrue($item->isHit()); + + // Get Object + $item2 = $object->getItem('chave'); + $this->assertTrue($item2->isHit()); + $this->assertEquals($model, $item2->get()); + + // Remove + $object->deleteItem('chave'); + + // Check Removed + $item = $object->getItem('chave'); + $this->assertFalse($item->isHit()); + } else { + $this->markTestIncomplete('Object is not fully functional'); + } + } +} diff --git a/tests/CachePoolTest.php b/tests/CachePoolTest.php deleted file mode 100644 index 76f0402..0000000 --- a/tests/CachePoolTest.php +++ /dev/null @@ -1,129 +0,0 @@ -isAvailable()) { - // First time - $item = $object->getItem('chave'); - $this->assertFalse($item->isHit()); - - // Set object - $item->set('valor'); - $object->save($item); - $this->assertTrue($item->isHit()); - - // Get Object - $item2 = $object->getItem('chave'); - $this->assertTrue($item2->isHit()); - $this->assertEquals('valor', $item2->get()); - - // Remove - $object->deleteItem('chave'); - - // Check Removed - $item = $object->getItem('chave'); - $this->assertFalse($item->isHit()); - } else { - $this->markTestIncomplete('Object is not fully functional'); - } - } - - /** - * @dataProvider CachePoolProvider - * @param CachePool $object - */ - public function testGetMultipleItems($object) - { - if ($object->isAvailable()) { - - // First time - $items = $object->getItems(['chave1', 'chave2']); - $this->assertFalse($items[0]->isHit()); - $this->assertFalse($items[1]->isHit()); - - // Set object - $items[0]->set('valor1'); - $items[1]->set('valor2'); - $object->saveDeferred($items[0]); - $object->saveDeferred($items[1]); - $object->commit(); - $this->assertTrue($items[0]->isHit()); - $this->assertTrue($items[1]->isHit()); - - // Get Object - $item2 = $object->getItems(['chave1', 'chave2']); - $this->assertTrue($item2[0]->isHit()); - $this->assertTrue($item2[1]->isHit()); - $this->assertEquals('valor1', $item2[0]->get()); - $this->assertEquals('valor2', $item2[1]->get()); - - // Remove - $object->deleteItems(['chave1', 'chave2']); - - // Check Removed - $items = $object->getItems(['chave1', 'chave2']); - $this->assertFalse($items[0]->isHit()); - $this->assertFalse($items[1]->isHit()); - } else { - $this->markTestIncomplete('Object is not fully functional'); - } - } - -} diff --git a/tests/Model.php b/tests/Model.php new file mode 100644 index 0000000..8c3becb --- /dev/null +++ b/tests/Model.php @@ -0,0 +1,64 @@ +prop1 = $prop1; + $this->prop2 = $prop2; + } + + /** + * @return mixed + */ + public function getProp1() + { + return $this->prop1; + } + + /** + * @param mixed $prop1 + */ + public function setProp1($prop1) + { + $this->prop1 = $prop1; + } + + /** + * @return mixed + */ + public function getProp2() + { + return $this->prop2; + } + + /** + * @param mixed $prop2 + */ + public function setProp2($prop2) + { + $this->prop2 = $prop2; + } + + public function sum() + { + return $this->prop1 + $this->prop2; + } +}