From 1b1d0000fb7aa463024ceb8dfa9430fce95f68a6 Mon Sep 17 00:00:00 2001 From: Michel Hunziker Date: Fri, 31 Aug 2012 17:30:38 +0200 Subject: [PATCH 1/5] Add missing @throws annotations --- src/Pattern/CallbackCache.php | 8 +++++--- src/Pattern/CaptureCache.php | 7 ++++++- src/Pattern/OutputCache.php | 4 ++-- src/PatternFactory.php | 2 +- src/Storage/Adapter/AdapterOptions.php | 1 + src/Storage/Adapter/Filesystem.php | 5 +++++ src/Storage/Adapter/MemoryOptions.php | 1 + src/Storage/Adapter/ZendServerDisk.php | 4 +++- src/Storage/Adapter/ZendServerShm.php | 2 +- src/Storage/Plugin/PluginOptions.php | 4 +++- 10 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/Pattern/CallbackCache.php b/src/Pattern/CallbackCache.php index e5fe0b0fd..b77e641e7 100644 --- a/src/Pattern/CallbackCache.php +++ b/src/Pattern/CallbackCache.php @@ -44,7 +44,8 @@ public function setOptions(PatternOptions $options) * @param callable $callback A valid callback * @param array $args Callback arguments * @return mixed Result - * @throws Exception + * @throws Exception\RuntimeException if invalid cached data + * @throws \Exception */ public function call($callback, array $args = array()) { @@ -127,8 +128,9 @@ public function generateKey($callback, array $args = array()) * * @param callable $callback A valid callback * @param array $args Callback arguments + * @throws Exception\RuntimeException if callback not serializable + * @throws Exception\InvalidArgumentException if invalid callback * @return string - * @throws Exception */ protected function generateCallbackKey($callback, array $args) { @@ -173,8 +175,8 @@ protected function generateCallbackKey($callback, array $args) * Generate a unique key of the argument part. * * @param array $args + * @throws Exception\RuntimeException * @return string - * @throws Exception */ protected function generateArgumentsKey(array $args) { diff --git a/src/Pattern/CaptureCache.php b/src/Pattern/CaptureCache.php index b8b2fbce6..1d51337cc 100644 --- a/src/Pattern/CaptureCache.php +++ b/src/Pattern/CaptureCache.php @@ -49,7 +49,7 @@ public function start($pageId = null) * * @param string $content * @param null|string $pageId - * @throws Exception\RuntimeException + * @throws Exception\LogicException */ public function set($content, $pageId = null) { @@ -74,6 +74,7 @@ public function set($content, $pageId = null) * * @param null|string $pageId * @return bool|string + * @throws Exception\LogicException * @throws Exception\RuntimeException */ public function get($pageId = null) @@ -108,6 +109,7 @@ public function get($pageId = null) * Checks if a cache with given id exists * * @param null|string $pageId + * @throws Exception\LogicException * @return boolean */ public function has($pageId = null) @@ -132,6 +134,7 @@ public function has($pageId = null) * Remove from cache * * @param null|string $pageId + * @throws Exception\LogicException * @throws Exception\RuntimeException * @return boolean */ @@ -169,6 +172,7 @@ public function remove($pageId = null) * Clear cached pages matching glob pattern * * @param string $pattern + * @throws Exception\LogicException */ public function clearByGlob($pattern = '**') { @@ -191,6 +195,7 @@ public function clearByGlob($pattern = '**') /** * Determine the page to save from the request * + * @throws Exception\RuntimeException * @return string */ protected function detectPageId() diff --git a/src/Pattern/OutputCache.php b/src/Pattern/OutputCache.php index a55f09cf4..f9aac0a77 100644 --- a/src/Pattern/OutputCache.php +++ b/src/Pattern/OutputCache.php @@ -50,8 +50,8 @@ public function setOptions(PatternOptions $options) * else start buffering output until end() is called or the script ends. * * @param string $key Key + * @throws Exception\MissingKeyException if key is missing * @return boolean - * @throws Exception */ public function start($key) { @@ -76,8 +76,8 @@ public function start($key) * Stops buffering output, write buffered data to cache using the given key on start() * and displays the buffer. * + * @throws Exception\RuntimeException if output cache not started or buffering not active * @return boolean TRUE on success, FALSE on failure writing to cache - * @throws Exception */ public function end() { diff --git a/src/PatternFactory.php b/src/PatternFactory.php index b39efc9fe..5a9de1bc9 100644 --- a/src/PatternFactory.php +++ b/src/PatternFactory.php @@ -32,7 +32,7 @@ class PatternFactory * @param string|Pattern\PatternInterface $patternName * @param array|Traversable|Pattern\PatternOptions $options * @return Pattern\PatternInterface - * @throws Exception\RuntimeException + * @throws Exception\InvalidArgumentException */ public static function factory($patternName, $options = array()) { diff --git a/src/Storage/Adapter/AdapterOptions.php b/src/Storage/Adapter/AdapterOptions.php index 70adb9d5b..f5c0896b4 100644 --- a/src/Storage/Adapter/AdapterOptions.php +++ b/src/Storage/Adapter/AdapterOptions.php @@ -86,6 +86,7 @@ public function setAdapter(StorageInterface $adapter = null) * Set key pattern * * @param null|string $keyPattern + * @throws Exception\InvalidArgumentException * @return AdapterOptions */ public function setKeyPattern($keyPattern) diff --git a/src/Storage/Adapter/Filesystem.php b/src/Storage/Adapter/Filesystem.php index 505cf84a1..a87257a9a 100644 --- a/src/Storage/Adapter/Filesystem.php +++ b/src/Storage/Adapter/Filesystem.php @@ -103,6 +103,7 @@ public function getOptions() /** * Flush the whole storage * + * @throws Exception\RuntimeException * @return boolean */ public function flush() @@ -179,6 +180,7 @@ public function clearExpired() * Remove items by given namespace * * @param string $namespace + * @throws Exception\RuntimeException * @return boolean */ public function clearByNamespace($namespace) @@ -212,6 +214,7 @@ public function clearByNamespace($namespace) * Remove items matching given prefix * * @param string $prefix + * @throws Exception\RuntimeException * @return boolean */ public function clearByPrefix($prefix) @@ -383,6 +386,7 @@ public function optimize() /** * Get total space in bytes * + * @throws Exception\RuntimeException * @return int|float */ public function getTotalSpace() @@ -419,6 +423,7 @@ public function getTotalSpace() /** * Get available space in bytes * + * @throws Exception\RuntimeException * @return int|float */ public function getAvailableSpace() diff --git a/src/Storage/Adapter/MemoryOptions.php b/src/Storage/Adapter/MemoryOptions.php index ce871fc12..21bb4cc9f 100644 --- a/src/Storage/Adapter/MemoryOptions.php +++ b/src/Storage/Adapter/MemoryOptions.php @@ -80,6 +80,7 @@ public function getMemoryLimit() * Normalized a given value of memory limit into the number of bytes * * @param string|int $value + * @throws Exception\InvalidArgumentException * @return int */ protected function normalizeMemoryLimit($value) diff --git a/src/Storage/Adapter/ZendServerDisk.php b/src/Storage/Adapter/ZendServerDisk.php index e9fe81baa..82c09feb5 100644 --- a/src/Storage/Adapter/ZendServerDisk.php +++ b/src/Storage/Adapter/ZendServerDisk.php @@ -41,7 +41,7 @@ class ZendServerDisk extends AbstractZendServer implements * Constructor * * @param null|array|\Traversable|AdapterOptions $options - * @throws Exception\ExceptionInterface + * @throws Exception\ExtensionNotLoadedException */ public function __construct($options = array()) { @@ -84,6 +84,7 @@ public function clearByNamespace($namespace) /** * Get total space in bytes * + * @throws Exception\RuntimeException * @return int|float */ public function getTotalSpace() @@ -106,6 +107,7 @@ public function getTotalSpace() /** * Get available space in bytes * + * @throws Exception\RuntimeException * @return int|float */ public function getAvailableSpace() diff --git a/src/Storage/Adapter/ZendServerShm.php b/src/Storage/Adapter/ZendServerShm.php index 63759e085..d20b6c995 100644 --- a/src/Storage/Adapter/ZendServerShm.php +++ b/src/Storage/Adapter/ZendServerShm.php @@ -31,7 +31,7 @@ class ZendServerShm extends AbstractZendServer implements * Constructor * * @param null|array|\Traversable|AdapterOptions $options - * @throws Exception\ExceptionInterface + * @throws Exception\ExtensionNotLoadedException */ public function __construct($options = array()) { diff --git a/src/Storage/Plugin/PluginOptions.php b/src/Storage/Plugin/PluginOptions.php index 9ca1b41dc..5bf20b74b 100644 --- a/src/Storage/Plugin/PluginOptions.php +++ b/src/Storage/Plugin/PluginOptions.php @@ -105,7 +105,8 @@ public function getClearingFactor() * Used by: * - ExceptionHandler * - * @param callable ExceptionCallback + * @param callable $exceptionCallback + * @throws Exception\InvalidArgumentException * @return PluginOptions */ public function setExceptionCallback($exceptionCallback) @@ -187,6 +188,7 @@ public function getOptimizingFactor() * - Serializer * * @param string|SerializerAdapter $serializer + * @throws Exception\InvalidArgumentException * @return Serializer */ public function setSerializer($serializer) From d365e1eb48a30adcc9da20d88b2cdf32294060d5 Mon Sep 17 00:00:00 2001 From: Michel Hunziker Date: Fri, 31 Aug 2012 19:52:32 +0200 Subject: [PATCH 2/5] Resolve undefined classes in phpDoc --- src/Pattern/CallbackCache.php | 6 ++++-- src/Pattern/ClassCache.php | 10 ++++++---- src/Pattern/ObjectCache.php | 10 ++++++---- src/Pattern/PatternInterface.php | 2 +- src/Pattern/PatternOptions.php | 4 +++- src/Storage/Adapter/AbstractAdapter.php | 1 + src/Storage/Adapter/Dba.php | 1 + src/Storage/Adapter/DbaOptions.php | 2 +- src/Storage/Adapter/FilesystemIterator.php | 2 +- src/Storage/Adapter/FilesystemOptions.php | 1 + src/Storage/Adapter/WinCache.php | 3 ++- src/Storage/Plugin/ClearExpiredByFactor.php | 4 ++-- src/Storage/StorageInterface.php | 2 ++ 13 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/Pattern/CallbackCache.php b/src/Pattern/CallbackCache.php index b77e641e7..fe26293d7 100644 --- a/src/Pattern/CallbackCache.php +++ b/src/Pattern/CallbackCache.php @@ -101,7 +101,8 @@ public function call($callback, array $args = array()) * @param string $function Function name to call * @param array $args Function arguments * @return mixed - * @throws Exception + * @throws Exception\RuntimeException + * @throws \Exception */ public function __call($function, array $args) { @@ -115,7 +116,8 @@ public function __call($function, array $args) * @param callable $callback A valid callback * @param array $args Callback arguments * @return string - * @throws Exception + * @throws Exception\RuntimeException + * @throws Exception\InvalidArgumentException */ public function generateKey($callback, array $args = array()) { diff --git a/src/Pattern/ClassCache.php b/src/Pattern/ClassCache.php index 6bd7fbee9..3935c0088 100644 --- a/src/Pattern/ClassCache.php +++ b/src/Pattern/ClassCache.php @@ -44,7 +44,8 @@ public function setOptions(PatternOptions $options) * @param string $method Method name to call * @param array $args Method arguments * @return mixed - * @throws Exception + * @throws Exception\RuntimeException + * @throws \Exception */ public function call($method, array $args = array()) { @@ -78,7 +79,7 @@ public function call($method, array $args = array()) * @param string $method The method * @param array $args Callback arguments * @return string - * @throws Exception + * @throws Exception\RuntimeException */ public function generateKey($method, array $args = array()) { @@ -95,7 +96,7 @@ public function generateKey($method, array $args = array()) * @param callable $callback A valid callback * @param array $args Callback arguments * @return string - * @throws Exception + * @throws Exception\RuntimeException */ protected function generateCallbackKey($callback, array $args) { @@ -110,7 +111,8 @@ protected function generateCallbackKey($callback, array $args) * @param string $method Method name to call * @param array $args Method arguments * @return mixed - * @throws Exception + * @throws Exception\RuntimeException + * @throws \Exception */ public function __call($method, array $args) { diff --git a/src/Pattern/ObjectCache.php b/src/Pattern/ObjectCache.php index e8ab316a7..bff731f50 100644 --- a/src/Pattern/ObjectCache.php +++ b/src/Pattern/ObjectCache.php @@ -43,7 +43,8 @@ public function setOptions(PatternOptions $options) * @param string $method Method name to call * @param array $args Method arguments * @return mixed - * @throws Exception + * @throws Exception\RuntimeException + * @throws \Exception */ public function call($method, array $args = array()) { @@ -158,7 +159,7 @@ public function call($method, array $args = array()) * @param string $method The method * @param array $args Callback arguments * @return string - * @throws Exception + * @throws Exception\RuntimeException */ public function generateKey($method, array $args = array()) { @@ -175,7 +176,7 @@ public function generateKey($method, array $args = array()) * @param callable $callback A valid callback * @param array $args Callback arguments * @return string - * @throws Exception + * @throws Exception\RuntimeException */ protected function generateCallbackKey($callback, array $args = array()) { @@ -190,7 +191,8 @@ protected function generateCallbackKey($callback, array $args = array()) * @param string $method Method name to call * @param array $args Method arguments * @return mixed - * @throws Exception + * @throws Exception\RuntimeException + * @throws \Exception */ public function __call($method, array $args) { diff --git a/src/Pattern/PatternInterface.php b/src/Pattern/PatternInterface.php index 934ccbc53..af39a1e67 100644 --- a/src/Pattern/PatternInterface.php +++ b/src/Pattern/PatternInterface.php @@ -21,7 +21,7 @@ interface PatternInterface * Set pattern options * * @param PatternOptions $options - * @return Pattern + * @return PatternInterface */ public function setOptions(PatternOptions $options); diff --git a/src/Pattern/PatternOptions.php b/src/Pattern/PatternOptions.php index 9478f1e81..3090e4b79 100644 --- a/src/Pattern/PatternOptions.php +++ b/src/Pattern/PatternOptions.php @@ -10,6 +10,7 @@ namespace Zend\Cache\Pattern; +use Traversable; use Zend\Cache\Exception; use Zend\Cache\StorageFactory; use Zend\Cache\Storage\StorageInterface as Storage; @@ -742,7 +743,8 @@ protected function normalizeObjectMethods(array $methods) * Create a storage object from a given specification * * @param array|string|Storage $storage - * @return StorageAdapter + * @throws Exception\InvalidArgumentException + * @return Storage */ protected function storageFactory($storage) { diff --git a/src/Storage/Adapter/AbstractAdapter.php b/src/Storage/Adapter/AbstractAdapter.php index f78755110..9e0e7e53f 100644 --- a/src/Storage/Adapter/AbstractAdapter.php +++ b/src/Storage/Adapter/AbstractAdapter.php @@ -22,6 +22,7 @@ use Zend\Cache\Storage\PostEvent; use Zend\Cache\Storage\StorageInterface; use Zend\EventManager\EventManager; +use Zend\EventManager\EventManagerInterface; use Zend\EventManager\EventsCapableInterface; /** diff --git a/src/Storage/Adapter/Dba.php b/src/Storage/Adapter/Dba.php index 902498eb1..ef4f2cee8 100644 --- a/src/Storage/Adapter/Dba.php +++ b/src/Storage/Adapter/Dba.php @@ -11,6 +11,7 @@ namespace Zend\Cache\Storage\Adapter; use stdClass; +use Traversable; use Zend\Cache\Exception; use Zend\Cache\Storage\AvailableSpaceCapableInterface; use Zend\Cache\Storage\Capabilities; diff --git a/src/Storage/Adapter/DbaOptions.php b/src/Storage/Adapter/DbaOptions.php index bcf78379a..5fe3340c2 100644 --- a/src/Storage/Adapter/DbaOptions.php +++ b/src/Storage/Adapter/DbaOptions.php @@ -99,7 +99,7 @@ public function getPathname() /** * * - * @param unknown_type $mode + * @param string $mode * @return \Zend\Cache\Storage\Adapter\DbaOptions */ public function setMode($mode) diff --git a/src/Storage/Adapter/FilesystemIterator.php b/src/Storage/Adapter/FilesystemIterator.php index 142b680f2..b138b2d4b 100644 --- a/src/Storage/Adapter/FilesystemIterator.php +++ b/src/Storage/Adapter/FilesystemIterator.php @@ -74,7 +74,7 @@ public function __construct(Filesystem $storage, $path, $prefix) /** * Get storage instance * - * @return StorageInterface + * @return Filesystem */ public function getStorage() { diff --git a/src/Storage/Adapter/FilesystemOptions.php b/src/Storage/Adapter/FilesystemOptions.php index 1914561c8..e27de8578 100644 --- a/src/Storage/Adapter/FilesystemOptions.php +++ b/src/Storage/Adapter/FilesystemOptions.php @@ -10,6 +10,7 @@ namespace Zend\Cache\Storage\Adapter; +use Traversable; use Zend\Cache\Exception; /** diff --git a/src/Storage/Adapter/WinCache.php b/src/Storage/Adapter/WinCache.php index aa78b4c1c..bfe8bf9af 100644 --- a/src/Storage/Adapter/WinCache.php +++ b/src/Storage/Adapter/WinCache.php @@ -12,6 +12,7 @@ use ArrayObject; use stdClass; +use Traversable; use Zend\Cache\Exception; use Zend\Cache\Storage\AvailableSpaceCapableInterface; use Zend\Cache\Storage\Capabilities; @@ -60,7 +61,7 @@ public function __construct($options = null) /** * Set options. * - * @param array|\Traversable|WinCacheOptions $options + * @param array|Traversable|WinCacheOptions $options * @return WinCache * @see getOptions() */ diff --git a/src/Storage/Plugin/ClearExpiredByFactor.php b/src/Storage/Plugin/ClearExpiredByFactor.php index 4b4d60a50..0fdbec2ea 100644 --- a/src/Storage/Plugin/ClearExpiredByFactor.php +++ b/src/Storage/Plugin/ClearExpiredByFactor.php @@ -36,7 +36,7 @@ class ClearExpiredByFactor extends AbstractPlugin * * @param EventManagerInterface $events * @param int $priority - * @return ClearByFactor + * @return ClearExpiredByFactor * @throws Exception\LogicException */ public function attach(EventManagerInterface $events, $priority = 1) @@ -62,7 +62,7 @@ public function attach(EventManagerInterface $events, $priority = 1) * Detach * * @param EventManagerInterface $events - * @return ClearByFactor + * @return ClearExpiredByFactor * @throws Exception\LogicException */ public function detach(EventManagerInterface $events) diff --git a/src/Storage/StorageInterface.php b/src/Storage/StorageInterface.php index 8a5df0bcb..3c4f5ec00 100644 --- a/src/Storage/StorageInterface.php +++ b/src/Storage/StorageInterface.php @@ -10,6 +10,8 @@ namespace Zend\Cache\Storage; +use Traversable; + /** * @category Zend * @package Zend_Cache From b506606b1c694549a4429843d7784452b1debddc Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 31 Aug 2012 14:44:59 -0500 Subject: [PATCH 3/5] [zendframework/zf2#2284][ZF2-507] Updated README - Notice about Date header --- .coveralls.yml | 3 + .gitattributes | 6 + .gitignore | 14 + .php_cs | 43 + .travis.yml | 35 + CONTRIBUTING.md | 229 +++ LICENSE.txt | 27 + README.md | 9 + composer.json | 50 + phpunit.xml.dist | 62 + phpunit.xml.travis | 64 + src/Exception/BadMethodCallException.php | 20 + src/Exception/ExceptionInterface.php | 20 + src/Exception/ExtensionNotLoadedException.php | 19 + src/Exception/InvalidArgumentException.php | 20 + src/Exception/LogicException.php | 19 + src/Exception/MissingDependencyException.php | 19 + src/Exception/MissingKeyException.php | 19 + src/Exception/OutOfSpaceException.php | 19 + src/Exception/RuntimeException.php | 19 + src/Exception/UnexpectedValueException.php | 20 + .../UnsupportedMethodCallException.php | 20 + src/Pattern/AbstractPattern.php | 57 + src/Pattern/CallbackCache.php | 205 +++ src/Pattern/CaptureCache.php | 381 ++++ src/Pattern/ClassCache.php | 170 ++ src/Pattern/ObjectCache.php | 288 +++ src/Pattern/OutputCache.php | 96 + src/Pattern/PatternInterface.php | 34 + src/Pattern/PatternOptions.php | 763 ++++++++ src/PatternFactory.php | 97 + src/PatternPluginManager.php | 70 + src/Storage/Adapter/AbstractAdapter.php | 1586 ++++++++++++++++ src/Storage/Adapter/AbstractZendServer.php | 267 +++ src/Storage/Adapter/AdapterOptions.php | 269 +++ src/Storage/Adapter/Apc.php | 696 +++++++ src/Storage/Adapter/ApcIterator.php | 164 ++ src/Storage/Adapter/ApcOptions.php | 52 + src/Storage/Adapter/Dba.php | 524 ++++++ src/Storage/Adapter/DbaIterator.php | 196 ++ src/Storage/Adapter/DbaOptions.php | 134 ++ src/Storage/Adapter/Filesystem.php | 1604 +++++++++++++++++ src/Storage/Adapter/FilesystemIterator.php | 179 ++ src/Storage/Adapter/FilesystemOptions.php | 462 +++++ src/Storage/Adapter/KeyListIterator.php | 176 ++ src/Storage/Adapter/Memcached.php | 618 +++++++ src/Storage/Adapter/MemcachedOptions.php | 210 +++ src/Storage/Adapter/Memory.php | 747 ++++++++ src/Storage/Adapter/MemoryOptions.php | 117 ++ src/Storage/Adapter/WinCache.php | 511 ++++++ src/Storage/Adapter/WinCacheOptions.php | 54 + src/Storage/Adapter/ZendServerDisk.php | 185 ++ src/Storage/Adapter/ZendServerShm.php | 144 ++ src/Storage/AdapterPluginManager.php | 78 + .../AvailableSpaceCapableInterface.php | 26 + src/Storage/Capabilities.php | 541 ++++++ src/Storage/ClearByNamespaceInterface.php | 27 + src/Storage/ClearByPrefixInterface.php | 27 + src/Storage/ClearExpiredInterface.php | 26 + src/Storage/Event.php | 71 + src/Storage/ExceptionEvent.php | 97 + src/Storage/FlushableInterface.php | 26 + src/Storage/IterableInterface.php | 24 + src/Storage/IteratorInterface.php | 49 + src/Storage/OptimizableInterface.php | 26 + src/Storage/Plugin/AbstractPlugin.php | 49 + src/Storage/Plugin/ClearExpiredByFactor.php | 104 ++ src/Storage/Plugin/ExceptionHandler.php | 129 ++ src/Storage/Plugin/IgnoreUserAbort.php | 171 ++ src/Storage/Plugin/OptimizeByFactor.php | 101 ++ src/Storage/Plugin/PluginInterface.php | 36 + src/Storage/Plugin/PluginOptions.php | 304 ++++ src/Storage/Plugin/Serializer.php | 315 ++++ src/Storage/PluginManager.php | 71 + src/Storage/PostEvent.php | 66 + src/Storage/StorageInterface.php | 250 +++ src/Storage/TaggableInterface.php | 49 + src/Storage/TotalSpaceCapableInterface.php | 26 + src/StorageFactory.php | 244 +++ test/Pattern/CallbackCacheTest.php | 168 ++ test/Pattern/CaptureCacheTest.php | 132 ++ test/Pattern/ClassCacheTest.php | 133 ++ test/Pattern/CommonPatternTest.php | 55 + test/Pattern/ObjectCacheTest.php | 173 ++ test/Pattern/OutputCacheTest.php | 104 ++ test/PatternFactoryTest.php | 57 + test/Storage/Adapter/AbstractAdapterTest.php | 763 ++++++++ test/Storage/Adapter/AbstractDbaTest.php | 69 + .../Adapter/AbstractZendServerTest.php | 111 ++ test/Storage/Adapter/ApcTest.php | 82 + test/Storage/Adapter/CommonAdapterTest.php | 1036 +++++++++++ test/Storage/Adapter/DbaDb2Test.php | 24 + test/Storage/Adapter/DbaDb3Test.php | 24 + test/Storage/Adapter/DbaDb4Test.php | 24 + test/Storage/Adapter/DbaFlatfileTest.php | 24 + test/Storage/Adapter/DbaGdbmTest.php | 24 + test/Storage/Adapter/DbaInifileTest.php | 24 + test/Storage/Adapter/DbaQdbmTest.php | 24 + test/Storage/Adapter/FilesystemTest.php | 288 +++ test/Storage/Adapter/MemcachedTest.php | 97 + test/Storage/Adapter/MemoryTest.php | 41 + test/Storage/Adapter/WinCacheTest.php | 61 + test/Storage/Adapter/ZendServerDiskTest.php | 53 + test/Storage/Adapter/ZendServerShmTest.php | 58 + test/Storage/CapabilitiesTest.php | 99 + .../Plugin/ClearExpiredByFactorTest.php | 95 + test/Storage/Plugin/CommonPluginTest.php | 45 + test/Storage/Plugin/ExceptionHandlerTest.php | 138 ++ test/Storage/Plugin/IgnoreUserAbortTest.php | 116 ++ test/Storage/Plugin/OptimizeByFactorTest.php | 90 + test/Storage/Plugin/SerializerTest.php | 123 ++ .../TestAsset/ClearExpiredMockAdapter.php | 20 + test/Storage/TestAsset/MockAdapter.php | 29 + test/Storage/TestAsset/MockPlugin.php | 97 + .../TestAsset/OptimizableMockAdapter.php | 20 + test/StorageFactoryTest.php | 187 ++ test/TestAsset/DummyPattern.php | 30 + test/TestAsset/DummyStorageAdapter.php | 17 + test/TestAsset/DummyStoragePlugin.php | 25 + test/bootstrap.php | 34 + 120 files changed, 19559 insertions(+) create mode 100644 .coveralls.yml create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .php_cs create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 phpunit.xml.travis create mode 100644 src/Exception/BadMethodCallException.php create mode 100644 src/Exception/ExceptionInterface.php create mode 100644 src/Exception/ExtensionNotLoadedException.php create mode 100644 src/Exception/InvalidArgumentException.php create mode 100644 src/Exception/LogicException.php create mode 100644 src/Exception/MissingDependencyException.php create mode 100644 src/Exception/MissingKeyException.php create mode 100644 src/Exception/OutOfSpaceException.php create mode 100644 src/Exception/RuntimeException.php create mode 100644 src/Exception/UnexpectedValueException.php create mode 100644 src/Exception/UnsupportedMethodCallException.php create mode 100644 src/Pattern/AbstractPattern.php create mode 100644 src/Pattern/CallbackCache.php create mode 100644 src/Pattern/CaptureCache.php create mode 100644 src/Pattern/ClassCache.php create mode 100644 src/Pattern/ObjectCache.php create mode 100644 src/Pattern/OutputCache.php create mode 100644 src/Pattern/PatternInterface.php create mode 100644 src/Pattern/PatternOptions.php create mode 100644 src/PatternFactory.php create mode 100644 src/PatternPluginManager.php create mode 100644 src/Storage/Adapter/AbstractAdapter.php create mode 100644 src/Storage/Adapter/AbstractZendServer.php create mode 100644 src/Storage/Adapter/AdapterOptions.php create mode 100644 src/Storage/Adapter/Apc.php create mode 100644 src/Storage/Adapter/ApcIterator.php create mode 100644 src/Storage/Adapter/ApcOptions.php create mode 100644 src/Storage/Adapter/Dba.php create mode 100644 src/Storage/Adapter/DbaIterator.php create mode 100644 src/Storage/Adapter/DbaOptions.php create mode 100644 src/Storage/Adapter/Filesystem.php create mode 100644 src/Storage/Adapter/FilesystemIterator.php create mode 100644 src/Storage/Adapter/FilesystemOptions.php create mode 100644 src/Storage/Adapter/KeyListIterator.php create mode 100644 src/Storage/Adapter/Memcached.php create mode 100644 src/Storage/Adapter/MemcachedOptions.php create mode 100644 src/Storage/Adapter/Memory.php create mode 100644 src/Storage/Adapter/MemoryOptions.php create mode 100644 src/Storage/Adapter/WinCache.php create mode 100644 src/Storage/Adapter/WinCacheOptions.php create mode 100644 src/Storage/Adapter/ZendServerDisk.php create mode 100644 src/Storage/Adapter/ZendServerShm.php create mode 100644 src/Storage/AdapterPluginManager.php create mode 100644 src/Storage/AvailableSpaceCapableInterface.php create mode 100644 src/Storage/Capabilities.php create mode 100644 src/Storage/ClearByNamespaceInterface.php create mode 100644 src/Storage/ClearByPrefixInterface.php create mode 100644 src/Storage/ClearExpiredInterface.php create mode 100644 src/Storage/Event.php create mode 100644 src/Storage/ExceptionEvent.php create mode 100644 src/Storage/FlushableInterface.php create mode 100644 src/Storage/IterableInterface.php create mode 100644 src/Storage/IteratorInterface.php create mode 100644 src/Storage/OptimizableInterface.php create mode 100644 src/Storage/Plugin/AbstractPlugin.php create mode 100644 src/Storage/Plugin/ClearExpiredByFactor.php create mode 100644 src/Storage/Plugin/ExceptionHandler.php create mode 100644 src/Storage/Plugin/IgnoreUserAbort.php create mode 100644 src/Storage/Plugin/OptimizeByFactor.php create mode 100644 src/Storage/Plugin/PluginInterface.php create mode 100644 src/Storage/Plugin/PluginOptions.php create mode 100644 src/Storage/Plugin/Serializer.php create mode 100644 src/Storage/PluginManager.php create mode 100644 src/Storage/PostEvent.php create mode 100644 src/Storage/StorageInterface.php create mode 100644 src/Storage/TaggableInterface.php create mode 100644 src/Storage/TotalSpaceCapableInterface.php create mode 100644 src/StorageFactory.php create mode 100644 test/Pattern/CallbackCacheTest.php create mode 100644 test/Pattern/CaptureCacheTest.php create mode 100644 test/Pattern/ClassCacheTest.php create mode 100644 test/Pattern/CommonPatternTest.php create mode 100644 test/Pattern/ObjectCacheTest.php create mode 100644 test/Pattern/OutputCacheTest.php create mode 100644 test/PatternFactoryTest.php create mode 100644 test/Storage/Adapter/AbstractAdapterTest.php create mode 100644 test/Storage/Adapter/AbstractDbaTest.php create mode 100644 test/Storage/Adapter/AbstractZendServerTest.php create mode 100644 test/Storage/Adapter/ApcTest.php create mode 100644 test/Storage/Adapter/CommonAdapterTest.php create mode 100644 test/Storage/Adapter/DbaDb2Test.php create mode 100644 test/Storage/Adapter/DbaDb3Test.php create mode 100644 test/Storage/Adapter/DbaDb4Test.php create mode 100644 test/Storage/Adapter/DbaFlatfileTest.php create mode 100644 test/Storage/Adapter/DbaGdbmTest.php create mode 100644 test/Storage/Adapter/DbaInifileTest.php create mode 100644 test/Storage/Adapter/DbaQdbmTest.php create mode 100644 test/Storage/Adapter/FilesystemTest.php create mode 100644 test/Storage/Adapter/MemcachedTest.php create mode 100644 test/Storage/Adapter/MemoryTest.php create mode 100644 test/Storage/Adapter/WinCacheTest.php create mode 100644 test/Storage/Adapter/ZendServerDiskTest.php create mode 100644 test/Storage/Adapter/ZendServerShmTest.php create mode 100644 test/Storage/CapabilitiesTest.php create mode 100644 test/Storage/Plugin/ClearExpiredByFactorTest.php create mode 100644 test/Storage/Plugin/CommonPluginTest.php create mode 100644 test/Storage/Plugin/ExceptionHandlerTest.php create mode 100644 test/Storage/Plugin/IgnoreUserAbortTest.php create mode 100644 test/Storage/Plugin/OptimizeByFactorTest.php create mode 100644 test/Storage/Plugin/SerializerTest.php create mode 100644 test/Storage/TestAsset/ClearExpiredMockAdapter.php create mode 100644 test/Storage/TestAsset/MockAdapter.php create mode 100644 test/Storage/TestAsset/MockPlugin.php create mode 100644 test/Storage/TestAsset/OptimizableMockAdapter.php create mode 100644 test/StorageFactoryTest.php create mode 100644 test/TestAsset/DummyPattern.php create mode 100644 test/TestAsset/DummyStorageAdapter.php create mode 100644 test/TestAsset/DummyStoragePlugin.php create mode 100644 test/bootstrap.php diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 000000000..53bda829c --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,3 @@ +coverage_clover: clover.xml +json_path: coveralls-upload.json +src_dir: src diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..85dc9a8c8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +/test export-ignore +/vendor export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +.php_cs export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..4cac0a218 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.buildpath +.DS_Store +.idea +.project +.settings/ +.*.sw* +.*.un~ +nbproject +tmp/ + +clover.xml +coveralls-upload.json +phpunit.xml +vendor diff --git a/.php_cs b/.php_cs new file mode 100644 index 000000000..bf4b799f3 --- /dev/null +++ b/.php_cs @@ -0,0 +1,43 @@ +notPath('TestAsset') + ->notPath('_files') + ->filter(function (SplFileInfo $file) { + if (strstr($file->getPath(), 'compatibility')) { + return false; + } + }); +$config = Symfony\CS\Config\Config::create(); +$config->level(null); +$config->fixers( + array( + 'braces', + 'duplicate_semicolon', + 'elseif', + 'empty_return', + 'encoding', + 'eof_ending', + 'function_call_space', + 'function_declaration', + 'indentation', + 'join_function', + 'line_after_namespace', + 'linefeed', + 'lowercase_keywords', + 'parenthesis', + 'multiple_use', + 'method_argument_space', + 'object_operator', + 'php_closing_tag', + 'psr0', + 'remove_lines_between_uses', + 'short_tag', + 'standardize_not_equal', + 'trailing_spaces', + 'unused_use', + 'visibility', + 'whitespacy_lines', + ) +); +$config->finder($finder); +return $config; diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..fe909ecb1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,35 @@ +sudo: false + +language: php + +matrix: + fast_finish: true + include: + - php: 5.5 + - php: 5.6 + env: + - EXECUTE_TEST_COVERALLS=true + - EXECUTE_CS_CHECK=true + - php: 7 + - php: hhvm + allow_failures: + - php: 7 + - php: hhvm + +notifications: + irc: "irc.freenode.org#zftalk.dev" + email: false + +before_install: + - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi + +install: + - composer install --no-interaction --prefer-source + +script: + - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then ./vendor/bin/phpunit -c phpunit.xml.travis --coverage-clover clover.xml ; fi + - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then ./vendor/bin/phpunit -c phpunit.xml.travis ; fi + - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/php-cs-fixer fix -v --diff --dry-run --config-file=.php_cs ; fi + +after_script: + - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then ./vendor/bin/coveralls ; fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..77852065b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,229 @@ +# CONTRIBUTING + +## RESOURCES + +If you wish to contribute to Zend Framework, please be sure to +read/subscribe to the following resources: + + - [Coding Standards](https://github.com/zendframework/zf2/wiki/Coding-Standards) + - [Contributor's Guide](http://framework.zend.com/participate/contributor-guide) + - ZF Contributor's mailing list: + Archives: http://zend-framework-community.634137.n4.nabble.com/ZF-Contributor-f680267.html + Subscribe: zf-contributors-subscribe@lists.zend.com + - ZF Contributor's IRC channel: + #zftalk.dev on Freenode.net + +If you are working on new features or refactoring [create a proposal](https://github.com/zendframework/zend-cache/issues/new). + +## Reporting Potential Security Issues + +If you have encountered a potential security vulnerability, please **DO NOT** report it on the public +issue tracker: send it to us at [zf-security@zend.com](mailto:zf-security@zend.com) instead. +We will work with you to verify the vulnerability and patch it as soon as possible. + +When reporting issues, please provide the following information: + +- Component(s) affected +- A description indicating how to reproduce the issue +- A summary of the security vulnerability and impact + +We request that you contact us via the email address above and give the project +contributors a chance to resolve the vulnerability and issue a new release prior +to any public exposure; this helps protect users and provides them with a chance +to upgrade and/or update in order to protect their applications. + +For sensitive email communications, please use [our PGP key](http://framework.zend.com/zf-security-pgp-key.asc). + +## RUNNING TESTS + +> ### Note: testing versions prior to 2.4 +> +> This component originates with Zend Framework 2. During the lifetime of ZF2, +> testing infrastructure migrated from PHPUnit 3 to PHPUnit 4. In most cases, no +> changes were necessary. However, due to the migration, tests may not run on +> versions < 2.4. As such, you may need to change the PHPUnit dependency if +> attempting a fix on such a version. + +To run tests: + +- Clone the repository: + + ```console + $ git clone git@github.com:zendframework/zend-cache.git + $ cd + ``` + +- Install dependencies via composer: + + ```console + $ curl -sS https://getcomposer.org/installer | php -- + $ ./composer.phar install + ``` + + If you don't have `curl` installed, you can also download `composer.phar` from https://getcomposer.org/ + +- Run the tests via `phpunit` and the provided PHPUnit config, like in this example: + + ```console + $ ./vendor/bin/phpunit + ``` + +You can turn on conditional tests with the phpunit.xml file. +To do so: + + - Copy `phpunit.xml.dist` file to `phpunit.xml` + - Edit `phpunit.xml` to enable any specific functionality you + want to test, as well as to provide test values to utilize. + +## Running Coding Standards Checks + +This component uses [php-cs-fixer](http://cs.sensiolabs.org/) for coding +standards checks, and provides configuration for our selected checks. +`php-cs-fixer` is installed by default via Composer. + +To run checks only: + +```console +$ ./vendor/bin/php-cs-fixer fix . -v --diff --dry-run --config-file=.php_cs +``` + +To have `php-cs-fixer` attempt to fix problems for you, omit the `--dry-run` +flag: + +```console +$ ./vendor/bin/php-cs-fixer fix . -v --diff --config-file=.php_cs +``` + +If you allow php-cs-fixer to fix CS issues, please re-run the tests to ensure +they pass, and make sure you add and commit the changes after verification. + +## Recommended Workflow for Contributions + +Your first step is to establish a public repository from which we can +pull your work into the master repository. We recommend using +[GitHub](https://github.com), as that is where the component is already hosted. + +1. Setup a [GitHub account](http://github.com/), if you haven't yet +2. Fork the repository (http://github.com/zendframework/zend-cache) +3. Clone the canonical repository locally and enter it. + + ```console + $ git clone git://github.com:zendframework/zend-cache.git + $ cd zend-cache + ``` + +4. Add a remote to your fork; substitute your GitHub username in the command + below. + + ```console + $ git remote add {username} git@github.com:{username}/zend-cache.git + $ git fetch {username} + ``` + +### Keeping Up-to-Date + +Periodically, you should update your fork or personal repository to +match the canonical ZF repository. Assuming you have setup your local repository +per the instructions above, you can do the following: + + +```console +$ git checkout master +$ git fetch origin +$ git rebase origin/master +# OPTIONALLY, to keep your remote up-to-date - +$ git push {username} master:master +``` + +If you're tracking other branches -- for example, the "develop" branch, where +new feature development occurs -- you'll want to do the same operations for that +branch; simply substitute "develop" for "master". + +### Working on a patch + +We recommend you do each new feature or bugfix in a new branch. This simplifies +the task of code review as well as the task of merging your changes into the +canonical repository. + +A typical workflow will then consist of the following: + +1. Create a new local branch based off either your master or develop branch. +2. Switch to your new local branch. (This step can be combined with the + previous step with the use of `git checkout -b`.) +3. Do some work, commit, repeat as necessary. +4. Push the local branch to your remote repository. +5. Send a pull request. + +The mechanics of this process are actually quite trivial. Below, we will +create a branch for fixing an issue in the tracker. + +```console +$ git checkout -b hotfix/9295 +Switched to a new branch 'hotfix/9295' +``` + +... do some work ... + + +```console +$ git commit +``` + +... write your log message ... + + +```console +$ git push {username} hotfix/9295:hotfix/9295 +Counting objects: 38, done. +Delta compression using up to 2 threads. +Compression objects: 100% (18/18), done. +Writing objects: 100% (20/20), 8.19KiB, done. +Total 20 (delta 12), reused 0 (delta 0) +To ssh://git@github.com/{username}/zend-cache.git + b5583aa..4f51698 HEAD -> master +``` + +To send a pull request, you have two options. + +If using GitHub, you can do the pull request from there. Navigate to +your repository, select the branch you just created, and then select the +"Pull Request" button in the upper right. Select the user/organization +"zendframework" as the recipient. + +If using your own repository - or even if using GitHub - you can use `git +format-patch` to create a patchset for us to apply; in fact, this is +**recommended** for security-related patches. If you use `format-patch`, please +send the patches as attachments to: + +- zf-devteam@zend.com for patches without security implications +- zf-security@zend.com for security patches + +#### What branch to issue the pull request against? + +Which branch should you issue a pull request against? + +- For fixes against the stable release, issue the pull request against the + "master" branch. +- For new features, or fixes that introduce new elements to the public API (such + as new public methods or properties), issue the pull request against the + "develop" branch. + +### Branch Cleanup + +As you might imagine, if you are a frequent contributor, you'll start to +get a ton of branches both locally and on your remote. + +Once you know that your changes have been accepted to the master +repository, we suggest doing some cleanup of these branches. + +- Local branch cleanup + + ```console + $ git branch -d + ``` + +- Remote branch removal + + ```console + $ git push {username} : + ``` diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..6eab5aa14 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,27 @@ +Copyright (c) 2005-2015, Zend Technologies USA, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of Zend Technologies USA, Inc. nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 000000000..46d27859d --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# zend-cache + +`Zend\Cache` provides a general cache system for PHP. The `Zend\Cache` component +is able to cache different patterns (class, object, output, etc) using different +storage adapters (DB, File, Memcache, etc). + + +- File issues at https://github.com/zendframework/zend-cache/issues +- Documentation is at http://framework.zend.com/manual/current/en/index.html#zend-cache diff --git a/composer.json b/composer.json new file mode 100644 index 000000000..aac6bd8a7 --- /dev/null +++ b/composer.json @@ -0,0 +1,50 @@ +{ + "name": "zendframework/zend-cache", + "description": "provides a generic way to cache any data", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "cache" + ], + "homepage": "https://github.com/zendframework/zend-cache", + "autoload": { + "psr-4": { + "Zend\\Cache": "src/" + } + }, + "require": { + "php": ">=5.3.3", + "zendframework/zend-stdlib": "self.version", + "zendframework/zend-servicemanager": "self.version", + "zendframework/zend-serializer": "self.version", + "zendframework/zend-eventmanager": "self.version" + }, + "require-dev": { + "zendframework/zend-session": "self.version", + "fabpot/php-cs-fixer": "1.7.*", + "satooshi/php-coveralls": "dev-master", + "phpunit/PHPUnit": "~4.0" + }, + "suggest": { + "zendframework/zend-serializer": "Zend\\Serializer component", + "zendframework/zend-session": "Zend\\Session component", + "ext-apc": "APC >= 3.1.6 to use the APC storage adapter", + "ext-dba": "DBA, to use the DBA storage adapter", + "ext-memcached": "Memcached >= 1.0.0 to use the Memcached storage adapter", + "ext-mongo": "Mongo, to use MongoDb storage adapter", + "ext-wincache": "WinCache, to use the WinCache storage adapter", + "mongofill/mongofill": "Alternative to ext-mongo - a pure PHP implementation designed as a drop in replacement", + "zendframework/zend-log": "Zend\\Log component" + }, + "extra": { + "branch-alias": { + "dev-master": "2.4-dev", + "dev-develop": "2.5-dev" + } + }, + "autoload-dev": { + "psr-4": { + "ZendTest\\Cache\\": "test/" + } + } +} \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 000000000..9fd3f75d3 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,62 @@ + + + + + ./test/ + + + + + + disable + + + + + + ./src + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpunit.xml.travis b/phpunit.xml.travis new file mode 100644 index 000000000..8b5e0202e --- /dev/null +++ b/phpunit.xml.travis @@ -0,0 +1,64 @@ + + + + + ./test/ + + + + + + disable + + + + + + ./src + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Exception/BadMethodCallException.php b/src/Exception/BadMethodCallException.php new file mode 100644 index 000000000..b29a56e91 --- /dev/null +++ b/src/Exception/BadMethodCallException.php @@ -0,0 +1,20 @@ +options = $options; + return $this; + } + + /** + * Get all pattern options + * + * @return PatternOptions + */ + public function getOptions() + { + if (null === $this->options) { + $this->setOptions(new PatternOptions()); + } + return $this->options; + } +} diff --git a/src/Pattern/CallbackCache.php b/src/Pattern/CallbackCache.php new file mode 100644 index 000000000..e5fe0b0fd --- /dev/null +++ b/src/Pattern/CallbackCache.php @@ -0,0 +1,205 @@ +getStorage()) { + throw new Exception\InvalidArgumentException("Missing option 'storage'"); + } + return $this; + } + + /** + * Call the specified callback or get the result from cache + * + * @param callable $callback A valid callback + * @param array $args Callback arguments + * @return mixed Result + * @throws Exception + */ + public function call($callback, array $args = array()) + { + $options = $this->getOptions(); + $storage = $options->getStorage(); + $success = null; + $key = $this->generateCallbackKey($callback, $args); + $result = $storage->getItem($key, $success); + if ($success) { + if (!isset($result[0])) { + throw new Exception\RuntimeException("Invalid cached data for key '{$key}'"); + } + + echo isset($result[1]) ? $result[1] : ''; + return $result[0]; + } + + $cacheOutput = $options->getCacheOutput(); + if ($cacheOutput) { + ob_start(); + ob_implicit_flush(false); + } + + // TODO: do not cache on errors using [set|restore]_error_handler + + try { + if ($args) { + $ret = call_user_func_array($callback, $args); + } else { + $ret = call_user_func($callback); + } + } catch (\Exception $e) { + if ($cacheOutput) { + ob_end_flush(); + } + throw $e; + } + + if ($cacheOutput) { + $data = array($ret, ob_get_flush()); + } else { + $data = array($ret); + } + + $storage->setItem($key, $data); + + return $ret; + } + + /** + * function call handler + * + * @param string $function Function name to call + * @param array $args Function arguments + * @return mixed + * @throws Exception + */ + public function __call($function, array $args) + { + return $this->call($function, $args); + } + + /** + * Generate a unique key in base of a key representing the callback part + * and a key representing the arguments part. + * + * @param callable $callback A valid callback + * @param array $args Callback arguments + * @return string + * @throws Exception + */ + public function generateKey($callback, array $args = array()) + { + return $this->generateCallbackKey($callback, $args); + } + + /** + * Generate a unique key in base of a key representing the callback part + * and a key representing the arguments part. + * + * @param callable $callback A valid callback + * @param array $args Callback arguments + * @return string + * @throws Exception + */ + protected function generateCallbackKey($callback, array $args) + { + if (!is_callable($callback, false, $callbackKey)) { + throw new Exception\InvalidArgumentException('Invalid callback'); + } + + // functions, methods and classnames are case-insensitive + $callbackKey = strtolower($callbackKey); + + // generate a unique key of object callbacks + if (is_object($callback)) { // Closures & __invoke + $object = $callback; + } elseif (isset($callback[0])) { // array($object, 'method') + $object = $callback[0]; + } + if (isset($object)) { + ErrorHandler::start(); + try { + $serializedObject = serialize($object); + } catch (\Exception $e) { + ErrorHandler::stop(); + throw new Exception\RuntimeException( + "Can't serialize callback: see previous exception", 0, $e + ); + } + $error = ErrorHandler::stop(); + + if (!$serializedObject) { + throw new Exception\RuntimeException(sprintf( + 'Cannot serialize callback%s', + ($error ? ': ' . $error->getMessage() : '') + ), 0, $error); + } + $callbackKey.= $serializedObject; + } + + return md5($callbackKey) . $this->generateArgumentsKey($args); + } + + /** + * Generate a unique key of the argument part. + * + * @param array $args + * @return string + * @throws Exception + */ + protected function generateArgumentsKey(array $args) + { + if (!$args) { + return ''; + } + + ErrorHandler::start(); + try { + $serializedArgs = serialize(array_values($args)); + } catch (\Exception $e) { + ErrorHandler::stop(); + throw new Exception\RuntimeException( + "Can't serialize arguments: see previous exception" + , 0, $e); + } + $error = ErrorHandler::stop(); + + if (!$serializedArgs) { + throw new Exception\RuntimeException(sprintf( + 'Cannot serialize arguments%s', + ($error ? ': ' . $error->getMessage() : '') + ), 0, $error); + } + + return md5($serializedArgs); + } +} diff --git a/src/Pattern/CaptureCache.php b/src/Pattern/CaptureCache.php new file mode 100644 index 000000000..b8b2fbce6 --- /dev/null +++ b/src/Pattern/CaptureCache.php @@ -0,0 +1,381 @@ +detectPageId(); + } + + $that = $this; + ob_start(function ($content) use ($that, $pageId) { + $that->set($content, $pageId); + + // http://php.net/manual/function.ob-start.php + // -> If output_callback returns FALSE original input is sent to the browser. + return false; + }); + + ob_implicit_flush(false); + } + + /** + * Write content to page identity + * + * @param string $content + * @param null|string $pageId + * @throws Exception\RuntimeException + */ + public function set($content, $pageId = null) + { + $publicDir = $this->getOptions()->getPublicDir(); + if ($publicDir === null) { + throw new Exception\LogicException("Option 'public_dir' no set"); + } + + if ($pageId === null) { + $pageId = $this->detectPageId(); + } + + $path = $this->pageId2Path($pageId); + $file = $path . \DIRECTORY_SEPARATOR . $this->pageId2Filename($pageId); + + $this->createDirectoryStructure($publicDir . \DIRECTORY_SEPARATOR . $path); + $this->putFileContent($publicDir . \DIRECTORY_SEPARATOR . $file, $content); + } + + /** + * Get from cache + * + * @param null|string $pageId + * @return bool|string + * @throws Exception\RuntimeException + */ + public function get($pageId = null) + { + $publicDir = $this->getOptions()->getPublicDir(); + if ($publicDir === null) { + throw new Exception\LogicException("Option 'public_dir' no set"); + } + + if ($pageId === null) { + $pageId = $this->detectPageId(); + } + + $file = $publicDir + . \DIRECTORY_SEPARATOR . $this->pageId2Path($pageId) + . \DIRECTORY_SEPARATOR . $this->pageId2Filename($pageId); + + if (file_exists($file)) { + ErrorHandler::start(); + $content = file_get_contents($file); + $error = ErrorHandler::stop(); + if ($content === false) { + throw new Exception\RuntimeException( + "Failed to read cached pageId '{$pageId}'", 0, $error + ); + } + return $content; + } + } + + /** + * Checks if a cache with given id exists + * + * @param null|string $pageId + * @return boolean + */ + public function has($pageId = null) + { + $publicDir = $this->getOptions()->getPublicDir(); + if ($publicDir === null) { + throw new Exception\LogicException("Option 'public_dir' no set"); + } + + if ($pageId === null) { + $pageId = $this->detectPageId(); + } + + $file = $publicDir + . \DIRECTORY_SEPARATOR . $this->pageId2Path($pageId) + . \DIRECTORY_SEPARATOR . $this->pageId2Filename($pageId); + + return file_exists($file); + } + + /** + * Remove from cache + * + * @param null|string $pageId + * @throws Exception\RuntimeException + * @return boolean + */ + public function remove($pageId = null) + { + $publicDir = $this->getOptions()->getPublicDir(); + if ($publicDir === null) { + throw new Exception\LogicException("Option 'public_dir' no set"); + } + + if ($pageId === null) { + $pageId = $this->detectPageId(); + } + + $file = $publicDir + . \DIRECTORY_SEPARATOR . $this->pageId2Path($pageId) + . \DIRECTORY_SEPARATOR . $this->pageId2Filename($pageId); + + if (file_exists($file)) { + ErrorHandler::start(); + $res = unlink($file); + $err = ErrorHandler::stop(); + if (!$res) { + throw new Exception\RuntimeException( + "Failed to remove cached pageId '{$pageId}'", 0, $err + ); + } + return true; + } + + return false; + } + + /** + * Clear cached pages matching glob pattern + * + * @param string $pattern + */ + public function clearByGlob($pattern = '**') + { + $publicDir = $this->getOptions()->getPublicDir(); + if ($publicDir === null) { + throw new Exception\LogicException("Option 'public_dir' no set"); + } + + $it = new \GlobIterator( + $publicDir . '/' . $pattern, + \GlobIterator::CURRENT_AS_SELF | \GlobIterator::SKIP_DOTS | \GlobIterator::UNIX_PATHS + ); + foreach ($it as $pathname => $entry) { + if ($entry->isFile()) { + unlink($pathname); + } + } + } + + /** + * Determine the page to save from the request + * + * @return string + */ + protected function detectPageId() + { + if (!isset($_SERVER['REQUEST_URI'])) { + throw new Exception\RuntimeException("Can't auto-detect current page identity"); + } + + return $_SERVER['REQUEST_URI']; + } + + /** + * Get filename for page id + * + * @param string $pageId + * @return string + */ + protected function pageId2Filename($pageId) + { + if (substr($pageId, -1) === '/') { + return $this->getOptions()->getIndexFilename(); + } + + return basename($pageId); + } + + /** + * Get path for page id + * + * @param string $pageId + * @return string + */ + protected function pageId2Path($pageId) + { + if (substr($pageId, -1) == '/') { + $path = rtrim($pageId, '/'); + } else { + $path = dirname($pageId); + } + + // convert requested "/" to the valid local directory separator + if ('/' != \DIRECTORY_SEPARATOR) { + $path = str_replace('/', \DIRECTORY_SEPARATOR, $path); + } + + return $path; + } + + /** + * Write content to a file + * + * @param string $file File complete path + * @param string $data Data to write + * @return void + * @throws Exception\RuntimeException + */ + protected function putFileContent($file, $data) + { + $options = $this->getOptions(); + $locking = $options->getFileLocking(); + $perm = $options->getFilePermission(); + $umask = $options->getUmask(); + if ($umask !== false && $perm !== false) { + $perm = $perm & ~$umask; + } + + ErrorHandler::start(); + + $umask = ($umask !== false) ? umask($umask) : false; + $rs = file_put_contents($file, $data, $locking ? \LOCK_EX : 0); + if ($umask) { + umask($umask); + } + + if ($rs === false) { + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException( + "Error writing file '{$file}'", 0, $err + ); + } + + if ($perm !== false && !chmod($file, $perm)) { + $oct = decoct($perm); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException("chmod('{$file}', 0{$oct}) failed", 0, $err); + } + + ErrorHandler::stop(); + } + + /** + * Creates directory if not already done. + * + * @param string $pathname + * @return void + * @throws Exception\RuntimeException + */ + protected function createDirectoryStructure($pathname) + { + // Directory structure already exists + if (file_exists($pathname)) { + return; + } + + $options = $this->getOptions(); + $perm = $options->getDirPermission(); + $umask = $options->getUmask(); + if ($umask !== false && $perm !== false) { + $perm = $perm & ~$umask; + } + + ErrorHandler::start(); + + if ($perm === false) { + // build-in mkdir function is enough + + $umask = ($umask !== false) ? umask($umask) : false; + $res = mkdir($pathname, ($perm !== false) ? $perm : 0777, true); + + if ($umask !== false) { + umask($umask); + } + + if (!$res) { + $oct = ($perm === false) ? '777' : decoct($perm); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException( + "mkdir('{$pathname}', 0{$oct}, true) failed", 0, $err + ); + } + + if ($perm !== false && !chmod($pathname, $perm)) { + $oct = decoct($perm); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException( + "chmod('{$pathname}', 0{$oct}) failed", 0, $err + ); + } + + } else { + // build-in mkdir function sets permission together with current umask + // which doesn't work well on multo threaded webservers + // -> create directories one by one and set permissions + + // find existing path and missing path parts + $parts = array(); + $path = $pathname; + while (!file_exists($path)) { + array_unshift($parts, basename($path)); + $nextPath = dirname($path); + if ($nextPath === $path) { + break; + } + $path = $nextPath; + } + + // make all missing path parts + foreach ($parts as $part) { + $path.= \DIRECTORY_SEPARATOR . $part; + + // create a single directory, set and reset umask immediately + $umask = ($umask !== false) ? umask($umask) : false; + $res = mkdir($path, ($perm === false) ? 0777 : $perm, false); + if ($umask !== false) { + umask($umask); + } + + if (!$res) { + $oct = ($perm === false) ? '777' : decoct($perm); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException( + "mkdir('{$path}', 0{$oct}, false) failed" + ); + } + + if ($perm !== false && !chmod($path, $perm)) { + $oct = decoct($perm); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException( + "chmod('{$path}', 0{$oct}) failed" + ); + } + } + } + + ErrorHandler::stop(); + } +} diff --git a/src/Pattern/ClassCache.php b/src/Pattern/ClassCache.php new file mode 100644 index 000000000..6bd7fbee9 --- /dev/null +++ b/src/Pattern/ClassCache.php @@ -0,0 +1,170 @@ +getClass()) { + throw new Exception\InvalidArgumentException("Missing option 'class'"); + } elseif (!$options->getStorage()) { + throw new Exception\InvalidArgumentException("Missing option 'storage'"); + } + return $this; + } + + /** + * Call and cache a class method + * + * @param string $method Method name to call + * @param array $args Method arguments + * @return mixed + * @throws Exception + */ + public function call($method, array $args = array()) + { + $options = $this->getOptions(); + $classname = $options->getClass(); + $method = strtolower($method); + $callback = $classname . '::' . $method; + + $cache = $options->getCacheByDefault(); + if ($cache) { + $cache = !in_array($method, $options->getClassNonCacheMethods()); + } else { + $cache = in_array($method, $options->getClassCacheMethods()); + } + + if (!$cache) { + if ($args) { + return call_user_func_array($callback, $args); + } else { + return $classname::$method(); + } + } + + return parent::call($callback, $args); + } + + /** + * Generate a unique key in base of a key representing the callback part + * and a key representing the arguments part. + * + * @param string $method The method + * @param array $args Callback arguments + * @return string + * @throws Exception + */ + public function generateKey($method, array $args = array()) + { + return $this->generateCallbackKey( + $this->getOptions()->getClass() . '::' . $method, + $args + ); + } + + /** + * Generate a unique key in base of a key representing the callback part + * and a key representing the arguments part. + * + * @param callable $callback A valid callback + * @param array $args Callback arguments + * @return string + * @throws Exception + */ + protected function generateCallbackKey($callback, array $args) + { + $callbackKey = md5(strtolower($callback)); + $argumentKey = $this->generateArgumentsKey($args); + return $callbackKey . $argumentKey; + } + + /** + * Calling a method of the entity. + * + * @param string $method Method name to call + * @param array $args Method arguments + * @return mixed + * @throws Exception + */ + public function __call($method, array $args) + { + return $this->call($method, $args); + } + + /** + * Set a static property + * + * @param string $name + * @param mixed $value + * @return void + * @see http://php.net/manual/language.oop5.overloading.php#language.oop5.overloading.members + */ + public function __set($name, $value) + { + $class = $this->getOptions()->getClass(); + $class::$name = $value; + } + + /** + * Get a static property + * + * @param string $name + * @return mixed + * @see http://php.net/manual/language.oop5.overloading.php#language.oop5.overloading.members + */ + public function __get($name) + { + $class = $this->getOptions()->getClass(); + return $class::$name; + } + + /** + * Is a static property exists. + * + * @param string $name + * @return bool + */ + public function __isset($name) + { + $class = $this->getOptions()->getClass(); + return isset($class::$name); + } + + /** + * Unset a static property + * + * @param string $name + * @return void + */ + public function __unset($name) + { + $class = $this->getOptions()->getClass(); + unset($class::$name); + } +} diff --git a/src/Pattern/ObjectCache.php b/src/Pattern/ObjectCache.php new file mode 100644 index 000000000..e8ab316a7 --- /dev/null +++ b/src/Pattern/ObjectCache.php @@ -0,0 +1,288 @@ +getObject()) { + throw new Exception\InvalidArgumentException("Missing option 'object'"); + } elseif (!$options->getStorage()) { + throw new Exception\InvalidArgumentException("Missing option 'storage'"); + } + } + + /** + * Call and cache a class method + * + * @param string $method Method name to call + * @param array $args Method arguments + * @return mixed + * @throws Exception + */ + public function call($method, array $args = array()) + { + $options = $this->getOptions(); + $object = $options->getObject(); + $method = strtolower($method); + + // handle magic methods + switch ($method) { + case '__set': + $property = array_shift($args); + $value = array_shift($args); + + $object->{$property} = $value; + + if (!$options->getObjectCacheMagicProperties() + || property_exists($object, $property) + ) { + // no caching if property isn't magic + // or caching magic properties is disabled + return; + } + + // remove cached __get and __isset + $removeKeys = null; + if (method_exists($object, '__get')) { + $removeKeys[] = $this->generateKey('__get', array($property)); + } + if (method_exists($object, '__isset')) { + $removeKeys[] = $this->generateKey('__isset', array($property)); + } + if ($removeKeys) { + $options->getStorage()->removeItems($removeKeys); + } + return; + + case '__get': + $property = array_shift($args); + + if (!$options->getObjectCacheMagicProperties() + || property_exists($object, $property) + ) { + // no caching if property isn't magic + // or caching magic properties is disabled + return $object->{$property}; + } + + array_unshift($args, $property); + return parent::call(array($object, '__get'), $args); + + case '__isset': + $property = array_shift($args); + + if (!$options->getObjectCacheMagicProperties() + || property_exists($object, $property) + ) { + // no caching if property isn't magic + // or caching magic properties is disabled + return isset($object->{$property}); + } + + return parent::call(array($object, '__isset'), array($property)); + + case '__unset': + $property = array_shift($args); + + unset($object->{$property}); + + if (!$options->getObjectCacheMagicProperties() + || property_exists($object, $property) + ) { + // no caching if property isn't magic + // or caching magic properties is disabled + return; + } + + // remove previous cached __get and __isset calls + $removeKeys = null; + if (method_exists($object, '__get')) { + $removeKeys[] = $this->generateKey('__get', array($property)); + } + if (method_exists($object, '__isset')) { + $removeKeys[] = $this->generateKey('__isset', array($property)); + } + if ($removeKeys) { + $options->getStorage()->removeItems($removeKeys); + } + return; + } + + $cache = $options->getCacheByDefault(); + if ($cache) { + $cache = !in_array($method, $options->getObjectNonCacheMethods()); + } else { + $cache = in_array($method, $options->getObjectCacheMethods()); + } + + if (!$cache) { + if ($args) { + return call_user_func_array(array($object, $method), $args); + } + return $object->{$method}(); + } + + return parent::call(array($object, $method), $args); + } + + /** + * Generate a unique key in base of a key representing the callback part + * and a key representing the arguments part. + * + * @param string $method The method + * @param array $args Callback arguments + * @return string + * @throws Exception + */ + public function generateKey($method, array $args = array()) + { + return $this->generateCallbackKey( + array($this->getOptions()->getObject(), $method), + $args + ); + } + + /** + * Generate a unique key in base of a key representing the callback part + * and a key representing the arguments part. + * + * @param callable $callback A valid callback + * @param array $args Callback arguments + * @return string + * @throws Exception + */ + protected function generateCallbackKey($callback, array $args = array()) + { + $callbackKey = md5($this->getOptions()->getObjectKey() . '::' . strtolower($callback[1])); + $argumentKey = $this->generateArgumentsKey($args); + return $callbackKey . $argumentKey; + } + + /** + * Class method call handler + * + * @param string $method Method name to call + * @param array $args Method arguments + * @return mixed + * @throws Exception + */ + public function __call($method, array $args) + { + return $this->call($method, $args); + } + + /** + * Writing data to properties. + * + * NOTE: + * Magic properties will be cached too if the option cacheMagicProperties + * is enabled and the property doesn't exist in real. If so it calls __set + * and removes cached data of previous __get and __isset calls. + * + * @param string $name + * @param mixed $value + * @return void + * @see http://php.net/manual/language.oop5.overloading.php#language.oop5.overloading.members + */ + public function __set($name, $value) + { + return $this->call('__set', array($name, $value)); + } + + /** + * Reading data from properties. + * + * NOTE: + * Magic properties will be cached too if the option cacheMagicProperties + * is enabled and the property doesn't exist in real. If so it calls __get. + * + * @param string $name + * @return mixed + * @see http://php.net/manual/language.oop5.overloading.php#language.oop5.overloading.members + */ + public function __get($name) + { + return $this->call('__get', array($name)); + } + + /** + * Checking existing properties. + * + * NOTE: + * Magic properties will be cached too if the option cacheMagicProperties + * is enabled and the property doesn't exist in real. If so it calls __get. + * + * @param string $name + * @return bool + * @see http://php.net/manual/language.oop5.overloading.php#language.oop5.overloading.members + */ + public function __isset($name) + { + return $this->call('__isset', array($name)); + } + + /** + * Unseting a property. + * + * NOTE: + * Magic properties will be cached too if the option cacheMagicProperties + * is enabled and the property doesn't exist in real. If so it removes + * previous cached __isset and __get calls. + * + * @param string $name + * @return void + * @see http://php.net/manual/language.oop5.overloading.php#language.oop5.overloading.members + */ + public function __unset($name) + { + return $this->call('__unset', array($name)); + } + + /** + * Handle casting to string + * + * @return string + * @see http://php.net/manual/language.oop5.magic.php#language.oop5.magic.tostring + */ + public function __toString() + { + return $this->call('__toString'); + } + + /** + * Handle invoke calls + * + * @return mixed + * @see http://php.net/manual/language.oop5.magic.php#language.oop5.magic.invoke + */ + public function __invoke() + { + return $this->call('__invoke', func_get_args()); + } +} diff --git a/src/Pattern/OutputCache.php b/src/Pattern/OutputCache.php new file mode 100644 index 000000000..a55f09cf4 --- /dev/null +++ b/src/Pattern/OutputCache.php @@ -0,0 +1,96 @@ +getStorage()) { + throw new Exception\InvalidArgumentException("Missing option 'storage'"); + } + + return $this; + } + + /** + * if there is a cached item with the given key display it's data and return true + * else start buffering output until end() is called or the script ends. + * + * @param string $key Key + * @return boolean + * @throws Exception + */ + public function start($key) + { + if (($key = (string) $key) === '') { + throw new Exception\MissingKeyException('Missing key to read/write output from cache'); + } + + $success = null; + $data = $this->getOptions()->getStorage()->getItem($key, $success); + if ($success) { + echo $data; + return true; + } + + ob_start(); + ob_implicit_flush(false); + $this->keyStack[] = $key; + return false; + } + + /** + * Stops buffering output, write buffered data to cache using the given key on start() + * and displays the buffer. + * + * @return boolean TRUE on success, FALSE on failure writing to cache + * @throws Exception + */ + public function end() + { + $key = array_pop($this->keyStack); + if ($key === null) { + throw new Exception\RuntimeException('Output cache not started'); + } + + $output = ob_end_flush(); + if ($output === false) { + throw new Exception\RuntimeException('Output buffering not active'); + } + + return $this->getOptions()->getStorage()->setItem($key, $output); + } +} diff --git a/src/Pattern/PatternInterface.php b/src/Pattern/PatternInterface.php new file mode 100644 index 000000000..934ccbc53 --- /dev/null +++ b/src/Pattern/PatternInterface.php @@ -0,0 +1,34 @@ +filePermission = false; + $this->dirPermission = false; + } + + parent::__construct($options); + } + + /** + * Set flag indicating whether or not to cache by default + * + * Used by: + * - ClassCache + * - ObjectCache + * + * @param bool $cacheByDefault + * @return PatternOptions + */ + public function setCacheByDefault($cacheByDefault) + { + $this->cacheByDefault = $cacheByDefault; + return $this; + } + + /** + * Do we cache by default? + * + * Used by: + * - ClassCache + * - ObjectCache + * + * @return bool + */ + public function getCacheByDefault() + { + return $this->cacheByDefault; + } + + /** + * Set whether or not to cache output + * + * Used by: + * - CallbackCache + * - ClassCache + * - ObjectCache + * + * @param bool $cacheOutput + * @return PatternOptions + */ + public function setCacheOutput($cacheOutput) + { + $this->cacheOutput = (bool) $cacheOutput; + return $this; + } + + /** + * Will we cache output? + * + * Used by: + * - CallbackCache + * - ClassCache + * - ObjectCache + * + * @return bool + */ + public function getCacheOutput() + { + return $this->cacheOutput; + } + + /** + * Set class name + * + * Used by: + * - ClassCache + * + * @param string $class + * @return PatternOptions + */ + public function setClass($class) + { + if (!is_string($class)) { + throw new Exception\InvalidArgumentException('Invalid classname provided; must be a string'); + } + $this->class = $class; + return $this; + } + + /** + * Get class name + * + * Used by: + * - ClassCache + * + * @return null|string + */ + public function getClass() + { + return $this->class; + } + + /** + * Set list of method return values to cache + * + * Used by: + * - ClassCache + * + * @param array $classCacheMethods + * @return PatternOptions + */ + public function setClassCacheMethods(array $classCacheMethods) + { + $this->classCacheMethods = $this->recursiveStrtolower($classCacheMethods); + return $this; + } + + /** + * Get list of methods from which to cache return values + * + * Used by: + * - ClassCache + * + * @return array + */ + public function getClassCacheMethods() + { + return $this->classCacheMethods; + } + + /** + * Set list of method return values NOT to cache + * + * Used by: + * - ClassCache + * + * @param array $classNonCacheMethods + * @return PatternOptions + */ + public function setClassNonCacheMethods(array $classNonCacheMethods) + { + $this->classNonCacheMethods = $this->recursiveStrtolower($classNonCacheMethods); + return $this; + } + + /** + * Get list of methods from which NOT to cache return values + * + * Used by: + * - ClassCache + * + * @return array + */ + public function getClassNonCacheMethods() + { + return $this->classNonCacheMethods; + } + + /** + * Set directory permission + * + * @param false|int $dirPermission + * @return PatternOptions + */ + public function setDirPermission($dirPermission) + { + if ($dirPermission !== false) { + if (is_string($dirPermission)) { + $dirPermission = octdec($dirPermission); + } else { + $dirPermission = (int) $dirPermission; + } + + // validate + if (($dirPermission & 0700) != 0700) { + throw new Exception\InvalidArgumentException( + 'Invalid directory permission: need permission to execute, read and write by owner' + ); + } + } + + $this->dirPermission = $dirPermission; + return $this; + } + + /** + * Gets directory permission + * + * @return false|int + */ + public function getDirPermission() + { + return $this->dirPermission; + } + + /** + * Set umask + * + * Used by: + * - CaptureCache + * + * @param false|int $umask + * @return PatternOptions + */ + public function setUmask($umask) + { + if ($umask !== false) { + if (is_string($umask)) { + $umask = octdec($umask); + } else { + $umask = (int) $umask; + } + + // validate + if ($umask & 0700) { + throw new Exception\InvalidArgumentException( + 'Invalid umask: need permission to execute, read and write by owner' + ); + } + + // normalize + $umask = $umask & 0777; + } + + $this->umask = $umask; + return $this; + } + + /** + * Get umask + * + * Used by: + * - CaptureCache + * + * @return false|int + */ + public function getUmask() + { + return $this->umask; + } + + /** + * Set whether or not file locking should be used + * + * Used by: + * - CaptureCache + * + * @param bool $fileLocking + * @return PatternOptions + */ + public function setFileLocking($fileLocking) + { + $this->fileLocking = (bool) $fileLocking; + return $this; + } + + /** + * Is file locking enabled? + * + * Used by: + * - CaptureCache + * + * @return bool + */ + public function getFileLocking() + { + return $this->fileLocking; + } + + /** + * Set file permission + * + * @param false|int $filePermission + * @return PatternOptions + */ + public function setFilePermission($filePermission) + { + if ($filePermission !== false) { + if (is_string($filePermission)) { + $filePermission = octdec($filePermission); + } else { + $filePermission = (int) $filePermission; + } + + // validate + if (($filePermission & 0600) != 0600) { + throw new Exception\InvalidArgumentException( + 'Invalid file permission: need permission to read and write by owner' + ); + } elseif ($filePermission & 0111) { + throw new Exception\InvalidArgumentException( + "Invalid file permission: Files shoudn't be executable" + ); + } + } + + $this->filePermission = $filePermission; + return $this; + } + + /** + * Gets file permission + * + * @return false|int + */ + public function getFilePermission() + { + return $this->filePermission; + } + + /** + * Set value for index filename + * + * @param string $indexFilename + * @return PatternOptions + */ + public function setIndexFilename($indexFilename) + { + $this->indexFilename = (string) $indexFilename; + return $this; + } + + /** + * Get value for index filename + * + * @return string + */ + public function getIndexFilename() + { + return $this->indexFilename; + } + + /** + * Set object to cache + * + * @param mixed $value + * @return $this + */ + public function setObject($object) + { + if (!is_object($object)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects an object; received "%s"', __METHOD__, gettype($object) + )); + } + $this->object = $object; + return $this; + } + + /** + * Get object to cache + * + * @return null|object + */ + public function getObject() + { + return $this->object; + } + + /** + * Set flag indicating whether or not to cache magic properties + * + * Used by: + * - ObjectCache + * + * @param bool $objectCacheMagicProperties + * @return PatternOptions + */ + public function setObjectCacheMagicProperties($objectCacheMagicProperties) + { + $this->objectCacheMagicProperties = (bool) $objectCacheMagicProperties; + return $this; + } + + /** + * Should we cache magic properties? + * + * Used by: + * - ObjectCache + * + * @return bool + */ + public function getObjectCacheMagicProperties() + { + return $this->objectCacheMagicProperties; + } + + /** + * Set list of object methods for which to cache return values + * + * @param array $objectCacheMethods + * @return PatternOptions + * @throws Exception\InvalidArgumentException + */ + public function setObjectCacheMethods(array $objectCacheMethods) + { + $this->objectCacheMethods = $this->normalizeObjectMethods($objectCacheMethods); + return $this; + } + + /** + * Get list of object methods for which to cache return values + * + * @return array + */ + public function getObjectCacheMethods() + { + return $this->objectCacheMethods; + } + + /** + * Set the object key part. + * + * Used to generate a callback key in order to speed up key generation. + * + * Used by: + * - ObjectCache + * + * @param mixed $value + * @return $this + */ + public function setObjectKey($objectKey) + { + if ($objectKey !== null) { + $this->objectKey = (string) $objectKey; + } else { + $this->objectKey = null; + } + return $this; + } + + /** + * Get object key + * + * Used by: + * - ObjectCache + * + * @return mixed + */ + public function getObjectKey() + { + if (!$this->objectKey) { + return get_class($this->getObject()); + } + return $this->objectKey; + } + + /** + * Set list of object methods for which NOT to cache return values + * + * @param array $objectNonCacheMethods + * @return PatternOptions + * @throws Exception\InvalidArgumentException + */ + public function setObjectNonCacheMethods(array $objectNonCacheMethods) + { + $this->objectNonCacheMethods = $this->normalizeObjectMethods($objectNonCacheMethods); + return $this; + } + + /** + * Get list of object methods for which NOT to cache return values + * + * @return array + */ + public function getObjectNonCacheMethods() + { + return $this->objectNonCacheMethods; + } + + /** + * Set location of public directory + * + * Used by: + * - CaptureCache + * + * @param string $publicDir + * @return PatternOptions + */ + public function setPublicDir($publicDir) + { + $publicDir = (string) $publicDir; + + if (!is_dir($publicDir)) { + throw new Exception\InvalidArgumentException( + "Public directory '{$publicDir}' not found or not a directory" + ); + } elseif (!is_writable($publicDir)) { + throw new Exception\InvalidArgumentException( + "Public directory '{$publicDir}' not writable" + ); + } elseif (!is_readable($publicDir)) { + throw new Exception\InvalidArgumentException( + "Public directory '{$publicDir}' not readable" + ); + } + + $this->publicDir = rtrim(realpath($publicDir), \DIRECTORY_SEPARATOR); + return $this; + } + + /** + * Get location of public directory + * + * Used by: + * - CaptureCache + * + * @return null|string + */ + public function getPublicDir() + { + return $this->publicDir; + } + + /** + * Set storage adapter + * + * Required for the following Pattern classes: + * - CallbackCache + * - ClassCache + * - ObjectCache + * - OutputCache + * + * @param string|array|Storage $storage + * @return PatternOptions + */ + public function setStorage($storage) + { + $this->storage = $this->storageFactory($storage); + return $this; + } + + /** + * Get storage adapter + * + * Used by: + * - CallbackCache + * - ClassCache + * - ObjectCache + * - OutputCache + * + * @return null|Storage + */ + public function getStorage() + { + return $this->storage; + } + + /** + * Recursively apply strtolower on all values of an array, and return as a + * list of unique values + * + * @param array $array + * @return array + */ + protected function recursiveStrtolower(array $array) + { + return array_values(array_unique(array_map('strtolower', $array))); + } + + /** + * Normalize object methods + * + * Recursively casts values to lowercase, then determines if any are in a + * list of methods not handled, raising an exception if so. + * + * @param array $methods + * @return array + * @throws Exception\InvalidArgumentException + */ + protected function normalizeObjectMethods(array $methods) + { + $methods = $this->recursiveStrtolower($methods); + $intersect = array_intersect(array('__set', '__get', '__unset', '__isset'), $methods); + if (!empty($intersect)) { + throw new Exception\InvalidArgumentException( + "Magic properties are handled by option 'cache_magic_properties'" + ); + } + return $methods; + } + + /** + * Create a storage object from a given specification + * + * @param array|string|Storage $storage + * @return StorageAdapter + */ + protected function storageFactory($storage) + { + if (is_array($storage)) { + $storage = StorageFactory::factory($storage); + } elseif (is_string($storage)) { + $storage = StorageFactory::adapterFactory($storage); + } elseif (!($storage instanceof Storage)) { + throw new Exception\InvalidArgumentException( + 'The storage must be an instanceof Zend\Cache\Storage\StorageInterface ' + . 'or an array passed to Zend\Cache\Storage::factory ' + . 'or simply the name of the storage adapter' + ); + } + + return $storage; + } +} diff --git a/src/PatternFactory.php b/src/PatternFactory.php new file mode 100644 index 000000000..b39efc9fe --- /dev/null +++ b/src/PatternFactory.php @@ -0,0 +1,97 @@ +setOptions($options); + return $patternName; + } + + $pattern = static::getPluginManager()->get($patternName); + $pattern->setOptions($options); + return $pattern; + } + + /** + * Get the pattern plugin manager + * + * @return PatternPluginManager + */ + public static function getPluginManager() + { + if (static::$plugins === null) { + static::$plugins = new PatternPluginManager(); + } + + return static::$plugins; + } + + /** + * Set the pattern plugin manager + * + * @param PatternPluginManager $plugins + * @return void + */ + public static function setPluginManager(PatternPluginManager $plugins) + { + static::$plugins = $plugins; + } + + /** + * Reset pattern plugin manager to default + * + * @return void + */ + public static function resetPluginManager() + { + static::$plugins = null; + } +} diff --git a/src/PatternPluginManager.php b/src/PatternPluginManager.php new file mode 100644 index 000000000..70cba3e54 --- /dev/null +++ b/src/PatternPluginManager.php @@ -0,0 +1,70 @@ + 'Zend\Cache\Pattern\CallbackCache', + 'capture' => 'Zend\Cache\Pattern\CaptureCache', + 'class' => 'Zend\Cache\Pattern\ClassCache', + 'object' => 'Zend\Cache\Pattern\ObjectCache', + 'output' => 'Zend\Cache\Pattern\OutputCache', + 'page' => 'Zend\Cache\Pattern\PageCache', + ); + + /** + * Don't share by default + * + * @var array + */ + protected $shareByDefault = false; + + /** + * Validate the plugin + * + * Checks that the pattern adapter loaded is an instance of Pattern\PatternInterface. + * + * @param mixed $plugin + * @return void + * @throws Exception\RuntimeException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof Pattern\PatternInterface) { + // we're okay + return; + } + + throw new Exception\RuntimeException(sprintf( + 'Plugin of type %s is invalid; must implement %s\Pattern\PatternInterface', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__ + )); + } +} diff --git a/src/Storage/Adapter/AbstractAdapter.php b/src/Storage/Adapter/AbstractAdapter.php new file mode 100644 index 000000000..f78755110 --- /dev/null +++ b/src/Storage/Adapter/AbstractAdapter.php @@ -0,0 +1,1586 @@ +setOptions($options); + } + } + + /** + * Destructor + * + * detach all registered plugins to free + * event handles of event manager + * + * @return void + */ + public function __destruct() + { + foreach ($this->getPluginRegistry() as $plugin) { + $this->removePlugin($plugin); + } + + if ($this->eventHandles) { + $events = $this->getEventManager(); + foreach ($this->eventHandles as $handle) { + $events->detach($handle); + } + } + } + + /* configuration */ + + /** + * Set options. + * + * @param array|Traversable|AdapterOptions $options + * @return AbstractAdapter + * @see getOptions() + */ + public function setOptions($options) + { + if ($this->options !== $options) { + if (!$options instanceof AdapterOptions) { + $options = new AdapterOptions($options); + } + + if ($this->options) { + $this->options->setAdapter(null); + } + $options->setAdapter($this); + $this->options = $options; + + $event = new Event('option', $this, new ArrayObject($options->toArray())); + $this->getEventManager()->trigger($event); + } + return $this; + } + + /** + * Get options. + * + * @return AdapterOptions + * @see setOptions() + */ + public function getOptions() + { + if (!$this->options) { + $this->setOptions(new AdapterOptions()); + } + return $this->options; + } + + /** + * Enable/Disable caching. + * + * Alias of setWritable and setReadable. + * + * @see setWritable() + * @see setReadable() + * @param boolean $flag + * @return AbstractAdapter + */ + public function setCaching($flag) + { + $flag = (bool) $flag; + $options = $this->getOptions(); + $options->setWritable($flag); + $options->setReadable($flag); + return $this; + } + + /** + * Get caching enabled. + * + * Alias of getWritable and getReadable. + * + * @see getWritable() + * @see getReadable() + * @return boolean + */ + public function getCaching() + { + $options = $this->getOptions(); + return ($options->getWritable() && $options->getReadable()); + } + + /* Event/Plugin handling */ + + /** + * Get the event manager + * + * @return EventManagerInterface + */ + public function getEventManager() + { + if ($this->events === null) { + $this->events = new EventManager(array(__CLASS__, get_called_class())); + } + return $this->events; + } + + /** + * Trigger an pre event and return the event response collection + * + * @param string $eventName + * @param ArrayObject $args + * @return \Zend\EventManager\ResponseCollection All handler return values + */ + protected function triggerPre($eventName, ArrayObject $args) + { + return $this->getEventManager()->trigger(new Event($eventName . '.pre', $this, $args)); + } + + /** + * Triggers the PostEvent and return the result value. + * + * @param string $eventName + * @param ArrayObject $args + * @param mixed $result + * @return mixed + */ + protected function triggerPost($eventName, ArrayObject $args, & $result) + { + $postEvent = new PostEvent($eventName . '.post', $this, $args, $result); + $eventRs = $this->getEventManager()->trigger($postEvent); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + return $postEvent->getResult(); + } + + /** + * Trigger an exception event + * + * If the ExceptionEvent has the flag "throwException" enabled throw the + * exception after trigger else return the result. + * + * @param string $eventName + * @param ArrayObject $args + * @param mixed $result + * @param \Exception $exception + * @throws Exception\ExceptionInterface + * @return mixed + */ + protected function triggerException($eventName, ArrayObject $args, & $result, \Exception $exception) + { + $exceptionEvent = new ExceptionEvent($eventName . '.exception', $this, $args, $result, $exception); + $eventRs = $this->getEventManager()->trigger($exceptionEvent); + + if ($exceptionEvent->getThrowException()) { + throw $exceptionEvent->getException(); + } + + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + return $exceptionEvent->getResult(); + } + + /** + * Check if a plugin is registered + * + * @param Plugin\PluginInterface $plugin + * @return boolean + */ + public function hasPlugin(Plugin\PluginInterface $plugin) + { + $registry = $this->getPluginRegistry(); + return $registry->contains($plugin); + } + + /** + * Register a plugin + * + * @param Plugin\PluginInterface $plugin + * @param int $priority + * @return AbstractAdapter Fluent interface + * @throws Exception\LogicException + */ + public function addPlugin(Plugin\PluginInterface $plugin, $priority = 1) + { + $registry = $this->getPluginRegistry(); + if ($registry->contains($plugin)) { + throw new Exception\LogicException(sprintf( + 'Plugin of type "%s" already registered', + get_class($plugin) + )); + } + + $plugin->attach($this->getEventManager(), $priority); + $registry->attach($plugin); + + return $this; + } + + /** + * Unregister an already registered plugin + * + * @param Plugin\PluginInterface $plugin + * @return AbstractAdapter Fluent interface + * @throws Exception\LogicException + */ + public function removePlugin(Plugin\PluginInterface $plugin) + { + $registry = $this->getPluginRegistry(); + if ($registry->contains($plugin)) { + $plugin->detach($this->getEventManager()); + $registry->detach($plugin); + } + return $this; + } + + /** + * Return registry of plugins + * + * @return SplObjectStorage + */ + public function getPluginRegistry() + { + if (!$this->pluginRegistry instanceof SplObjectStorage) { + $this->pluginRegistry = new SplObjectStorage(); + } + return $this->pluginRegistry; + } + + /* reading */ + + /** + * Get an item. + * + * @param string $key + * @param boolean $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + * + * @triggers getItem.pre(PreEvent) + * @triggers getItem.post(PostEvent) + * @triggers getItem.exception(ExceptionEvent) + */ + public function getItem($key, & $success = null, & $casToken = null) + { + if (!$this->getOptions()->getReadable()) { + $success = false; + return null; + } + + $this->normalizeKey($key); + + $argn = func_num_args(); + $args = array( + 'key' => & $key, + ); + if ($argn > 1) { + $args['success'] = & $success; + } + if ($argn > 2) { + $args['casToken'] = & $casToken; + } + $args = new ArrayObject($args); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + if ($args->offsetExists('success') && $args->offsetExists('casToken')) { + $result = $this->internalGetItem($args['key'], $args['success'], $args['casToken']); + } elseif ($args->offsetExists('success')) { + $result = $this->internalGetItem($args['key'], $args['success']); + } else { + $result = $this->internalGetItem($args['key']); + } + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to get an item. + * + * @param string $normalizedKey + * @param boolean $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + */ + abstract protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null); + + /** + * Get multiple items. + * + * @param array $keys + * @return array Associative array of keys and values + * @throws Exception\ExceptionInterface + * + * @triggers getItems.pre(PreEvent) + * @triggers getItems.post(PostEvent) + * @triggers getItems.exception(ExceptionEvent) + */ + public function getItems(array $keys) + { + if (!$this->getOptions()->getReadable()) { + return array(); + } + + $this->normalizeKeys($keys); + $args = new ArrayObject(array( + 'keys' => & $keys, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalGetItems($args['keys']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = array(); + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to get multiple items. + * + * @param array $normalizedKeys + * @return array Associative array of keys and values + * @throws Exception\ExceptionInterface + */ + protected function internalGetItems(array & $normalizedKeys) + { + $success = null; + $result = array(); + foreach ($normalizedKeys as $normalizedKey) { + $value = $this->internalGetItem($normalizedKey, $success); + if ($success) { + $result[$normalizedKey] = $value; + } + } + + return $result; + } + + /** + * Test if an item exists. + * + * @param string $key + * @return boolean + * @throws Exception\ExceptionInterface + * + * @triggers hasItem.pre(PreEvent) + * @triggers hasItem.post(PostEvent) + * @triggers hasItem.exception(ExceptionEvent) + */ + public function hasItem($key) + { + if (!$this->getOptions()->getReadable()) { + return false; + } + + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'key' => & $key, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalHasItem($args['key']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to test if an item exists. + * + * @param string $normalizedKey + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalHasItem(& $normalizedKey) + { + $success = null; + $this->internalGetItem($normalizedKey, $success); + return $success; + } + + /** + * Test multiple items. + * + * @param array $keys + * @return array Array of found keys + * @throws Exception\ExceptionInterface + * + * @triggers hasItems.pre(PreEvent) + * @triggers hasItems.post(PostEvent) + * @triggers hasItems.exception(ExceptionEvent) + */ + public function hasItems(array $keys) + { + if (!$this->getOptions()->getReadable()) { + return array(); + } + + $this->normalizeKeys($keys); + $args = new ArrayObject(array( + 'keys' => & $keys, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalHasItems($args['keys']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = array(); + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to test multiple items. + * + * @param array $keys + * @return array Array of found keys + * @throws Exception\ExceptionInterface + */ + protected function internalHasItems(array & $normalizedKeys) + { + $result = array(); + foreach ($normalizedKeys as $normalizedKey) { + if ($this->internalHasItem($normalizedKey)) { + $result[] = $normalizedKey; + } + } + return $result; + } + + /** + * Get metadata of an item. + * + * @param string $key + * @return array|boolean Metadata on success, false on failure + * @throws Exception\ExceptionInterface + * + * @triggers getMetadata.pre(PreEvent) + * @triggers getMetadata.post(PostEvent) + * @triggers getMetadata.exception(ExceptionEvent) + */ + public function getMetadata($key) + { + if (!$this->getOptions()->getReadable()) { + return false; + } + + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'key' => & $key, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalGetMetadata($args['key']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to get metadata of an item. + * + * @param string $normalizedKey + * @return array|boolean Metadata on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetMetadata(& $normalizedKey) + { + if (!$this->internalHasItem($normalizedKey)) { + return false; + } + + return array(); + } + + /** + * Get multiple metadata + * + * @param array $keys + * @return array Associative array of keys and metadata + * @throws Exception\ExceptionInterface + * + * @triggers getMetadatas.pre(PreEvent) + * @triggers getMetadatas.post(PostEvent) + * @triggers getMetadatas.exception(ExceptionEvent) + */ + public function getMetadatas(array $keys) + { + if (!$this->getOptions()->getReadable()) { + return array(); + } + + $this->normalizeKeys($keys); + $args = new ArrayObject(array( + 'keys' => & $keys, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalGetMetadatas($args['keys']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = array(); + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to get multiple metadata + * + * @param array $normalizedKeys + * @return array Associative array of keys and metadata + * @throws Exception\ExceptionInterface + */ + protected function internalGetMetadatas(array & $normalizedKeys) + { + $result = array(); + foreach ($normalizedKeys as $normalizedKey) { + $metadata = $this->internalGetMetadata($normalizedKey); + if ($metadata !== false) { + $result[$normalizedKey] = $metadata; + } + } + return $result; + } + + /* writing */ + + /** + * Store an item. + * + * @param string $key + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + * + * @triggers setItem.pre(PreEvent) + * @triggers setItem.post(PostEvent) + * @triggers setItem.exception(ExceptionEvent) + */ + public function setItem($key, $value) + { + if (!$this->getOptions()->getWritable()) { + return false; + } + + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'key' => & $key, + 'value' => & $value, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalSetItem($args['key'], $args['value']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to store an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + */ + abstract protected function internalSetItem(& $normalizedKey, & $value); + + /** + * Store multiple items. + * + * @param array $keyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + * + * @triggers setItems.pre(PreEvent) + * @triggers setItems.post(PostEvent) + * @triggers setItems.exception(ExceptionEvent) + */ + public function setItems(array $keyValuePairs) + { + if (!$this->getOptions()->getWritable()) { + return array_keys($keyValuePairs); + } + + $this->normalizeKeyValuePairs($keyValuePairs); + $args = new ArrayObject(array( + 'keyValuePairs' => & $keyValuePairs, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalSetItems($args['keyValuePairs']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = array_keys($keyValuePairs); + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to store multiple items. + * + * @param array $normalizedKeyValuePairs + * @param array $normalizedOptions + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalSetItems(array & $normalizedKeyValuePairs) + { + $failedKeys = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + if (!$this->internalSetItem($normalizedKey, $value)) { + $failedKeys[] = $normalizedKey; + } + } + return $failedKeys; + } + + /** + * Add an item. + * + * @param string $key + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + * + * @triggers addItem.pre(PreEvent) + * @triggers addItem.post(PostEvent) + * @triggers addItem.exception(ExceptionEvent) + */ + public function addItem($key, $value) + { + if (!$this->getOptions()->getWritable()) { + return false; + } + + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'key' => & $key, + 'value' => & $value, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalAddItem($args['key'], $args['value']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to add an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalAddItem(& $normalizedKey, & $value) + { + if ($this->internalHasItem($normalizedKey)) { + return false; + } + return $this->internalSetItem($normalizedKey, $value); + } + + /** + * Add multiple items. + * + * @param array $keyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + * + * @triggers addItems.pre(PreEvent) + * @triggers addItems.post(PostEvent) + * @triggers addItems.exception(ExceptionEvent) + */ + public function addItems(array $keyValuePairs) + { + if (!$this->getOptions()->getWritable()) { + return array_keys($keyValuePairs); + } + + $this->normalizeKeyValuePairs($keyValuePairs); + $args = new ArrayObject(array( + 'keyValuePairs' => & $keyValuePairs, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalAddItems($args['keyValuePairs']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = array_keys($keyValuePairs); + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to add multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalAddItems(array & $normalizedKeyValuePairs) + { + $result = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + if (!$this->internalAddItem($normalizedKey, $value)) { + $result[] = $normalizedKey; + } + } + return $result; + } + + /** + * Replace an existing item. + * + * @param string $key + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + * + * @triggers replaceItem.pre(PreEvent) + * @triggers replaceItem.post(PostEvent) + * @triggers replaceItem.exception(ExceptionEvent) + */ + public function replaceItem($key, $value) + { + if (!$this->getOptions()->getWritable()) { + return false; + } + + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'key' => & $key, + 'value' => & $value, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalReplaceItem($args['key'], $args['value']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to replace an existing item. + * + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalReplaceItem(& $normalizedKey, & $value) + { + if (!$this->internalhasItem($normalizedKey)) { + return false; + } + + return $this->internalSetItem($normalizedKey, $value); + } + + /** + * Replace multiple existing items. + * + * @param array $keyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + * + * @triggers replaceItems.pre(PreEvent) + * @triggers replaceItems.post(PostEvent) + * @triggers replaceItems.exception(ExceptionEvent) + */ + public function replaceItems(array $keyValuePairs) + { + if (!$this->getOptions()->getWritable()) { + return array_keys($keyValuePairs); + } + + $this->normalizeKeyValuePairs($keyValuePairs); + $args = new ArrayObject(array( + 'keyValuePairs' => & $keyValuePairs, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalReplaceItems($args['keyValuePairs']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = array_keys($keyValuePairs); + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to replace multiple existing items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalReplaceItems(array & $normalizedKeyValuePairs) + { + $result = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + if (!$this->internalReplaceItem($normalizedKey, $value)) { + $result[] = $normalizedKey; + } + } + return $result; + } + + /** + * Set an item only if token matches + * + * It uses the token received from getItem() to check if the item has + * changed before overwriting it. + * + * @param mixed $token + * @param string $key + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + * @see getItem() + * @see setItem() + */ + public function checkAndSetItem($token, $key, $value) + { + if (!$this->getOptions()->getWritable()) { + return false; + } + + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'token' => & $token, + 'key' => & $key, + 'value' => & $value, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalCheckAndSetItem($args['token'], $args['key'], $args['value']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to set an item only if token matches + * + * @param mixed $token + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + * @see getItem() + * @see setItem() + */ + protected function internalCheckAndSetItem(& $token, & $normalizedKey, & $value) + { + $oldValue = $this->internalGetItem($normalizedKey); + if ($oldValue !== $token) { + return false; + } + + return $this->internalSetItem($normalizedKey, $value); + } + + /** + * Reset lifetime of an item + * + * @param string $key + * @return boolean + * @throws Exception\ExceptionInterface + * + * @triggers touchItem.pre(PreEvent) + * @triggers touchItem.post(PostEvent) + * @triggers touchItem.exception(ExceptionEvent) + */ + public function touchItem($key) + { + if (!$this->getOptions()->getWritable()) { + return false; + } + + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'key' => & $key, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalTouchItem($args['key']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to reset lifetime of an item + * + * @param string $normalizedKey + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalTouchItem(& $normalizedKey) + { + $success = null; + $value = $this->internalGetItem($normalizedKey, $success); + if (!$success) { + return false; + } + + return $this->internalReplaceItem($normalizedKey, $value); + } + + /** + * Reset lifetime of multiple items. + * + * @param array $keys + * @return array Array of not updated keys + * @throws Exception\ExceptionInterface + * + * @triggers touchItems.pre(PreEvent) + * @triggers touchItems.post(PostEvent) + * @triggers touchItems.exception(ExceptionEvent) + */ + public function touchItems(array $keys) + { + if (!$this->getOptions()->getWritable()) { + return $keys; + } + + $this->normalizeKeys($keys); + $args = new ArrayObject(array( + 'keys' => & $keys, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalTouchItems($args['keys']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + return $this->triggerException(__FUNCTION__, $args, $keys, $e); + } + } + + /** + * Internal method to reset lifetime of multiple items. + * + * @param array $normalizedKeys + * @return array Array of not updated keys + * @throws Exception\ExceptionInterface + */ + protected function internalTouchItems(array & $normalizedKeys) + { + $result = array(); + foreach ($normalizedKeys as $normalizedKey) { + if (!$this->internalTouchItem($normalizedKey)) { + $result[] = $normalizedKey; + } + } + return $result; + } + + /** + * Remove an item. + * + * @param string $key + * @return boolean + * @throws Exception\ExceptionInterface + * + * @triggers removeItem.pre(PreEvent) + * @triggers removeItem.post(PostEvent) + * @triggers removeItem.exception(ExceptionEvent) + */ + public function removeItem($key) + { + if (!$this->getOptions()->getWritable()) { + return false; + } + + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'key' => & $key, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalRemoveItem($args['key']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to remove an item. + * + * @param string $normalizedKey + * @return boolean + * @throws Exception\ExceptionInterface + */ + abstract protected function internalRemoveItem(& $normalizedKey); + + /** + * Remove multiple items. + * + * @param array $keys + * @return array Array of not removed keys + * @throws Exception\ExceptionInterface + * + * @triggers removeItems.pre(PreEvent) + * @triggers removeItems.post(PostEvent) + * @triggers removeItems.exception(ExceptionEvent) + */ + public function removeItems(array $keys) + { + if (!$this->getOptions()->getWritable()) { + return $keys; + } + + $this->normalizeKeys($keys); + $args = new ArrayObject(array( + 'keys' => & $keys, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalRemoveItems($args['keys']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + return $this->triggerException(__FUNCTION__, $args, $keys, $e); + } + } + + /** + * Internal method to remove multiple items. + * + * @param array $keys + * @return array Array of not removed keys + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItems(array & $normalizedKeys) + { + $result = array(); + foreach ($normalizedKeys as $normalizedKey) { + if (!$this->internalRemoveItem($normalizedKey)) { + $result[] = $normalizedKey; + } + } + return $result; + } + + /** + * Increment an item. + * + * @param string $key + * @param int $value + * @return int|boolean The new value on success, false on failure + * @throws Exception\ExceptionInterface + * + * @triggers incrementItem.pre(PreEvent) + * @triggers incrementItem.post(PostEvent) + * @triggers incrementItem.exception(ExceptionEvent) + */ + public function incrementItem($key, $value) + { + if (!$this->getOptions()->getWritable()) { + return false; + } + + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'key' => & $key, + 'value' => & $value, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalIncrementItem($args['key'], $args['value']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to increment an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|boolean The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalIncrementItem(& $normalizedKey, & $value) + { + $success = null; + $value = (int) $value; + $get = (int) $this->internalGetItem($normalizedKey, $success); + $newValue = $get + $value; + + if ($success) { + $this->internalReplaceItem($normalizedKey, $newValue); + } else { + $this->internalAddItem($normalizedKey, $newValue); + } + + return $newValue; + } + + /** + * Increment multiple items. + * + * @param array $keyValuePairs + * @return array Associative array of keys and new values + * @throws Exception\ExceptionInterface + * + * @triggers incrementItems.pre(PreEvent) + * @triggers incrementItems.post(PostEvent) + * @triggers incrementItems.exception(ExceptionEvent) + */ + public function incrementItems(array $keyValuePairs) + { + if (!$this->getOptions()->getWritable()) { + return array(); + } + + $this->normalizeKeyValuePairs($keyValuePairs); + $args = new ArrayObject(array( + 'keyValuePairs' => & $keyValuePairs, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalIncrementItems($args['keyValuePairs']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = array(); + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to increment multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Associative array of keys and new values + * @throws Exception\ExceptionInterface + */ + protected function internalIncrementItems(array & $normalizedKeyValuePairs) + { + $result = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + $newValue = $this->internalIncrementItem($normalizedKey, $value); + if ($newValue !== false) { + $result[$normalizedKey] = $newValue; + } + } + return $result; + } + + /** + * Decrement an item. + * + * @param string $key + * @param int $value + * @return int|boolean The new value on success, false on failure + * @throws Exception\ExceptionInterface + * + * @triggers decrementItem.pre(PreEvent) + * @triggers decrementItem.post(PostEvent) + * @triggers decrementItem.exception(ExceptionEvent) + */ + public function decrementItem($key, $value) + { + if (!$this->getOptions()->getWritable()) { + return false; + } + + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'key' => & $key, + 'value' => & $value, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalDecrementItem($args['key'], $args['value']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to decrement an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|boolean The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalDecrementItem(& $normalizedKey, & $value) + { + $success = null; + $value = (int) $value; + $get = (int) $this->internalGetItem($normalizedKey, $success); + $newValue = $get - $value; + + if ($success) { + $this->internalReplaceItem($normalizedKey, $newValue); + } else { + $this->internalAddItem($normalizedKey, $newValue); + } + + return $newValue; + } + + /** + * Decrement multiple items. + * + * @param array $keyValuePairs + * @return array Associative array of keys and new values + * @throws Exception\ExceptionInterface + * + * @triggers incrementItems.pre(PreEvent) + * @triggers incrementItems.post(PostEvent) + * @triggers incrementItems.exception(ExceptionEvent) + */ + public function decrementItems(array $keyValuePairs) + { + if (!$this->getOptions()->getWritable()) { + return array(); + } + + $this->normalizeKeyValuePairs($keyValuePairs); + $args = new ArrayObject(array( + 'keyValuePairs' => & $keyValuePairs, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalDecrementItems($args['keyValuePairs']); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = array(); + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to decrement multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Associative array of keys and new values + * @throws Exception\ExceptionInterface + */ + protected function internalDecrementItems(array & $normalizedKeyValuePairs) + { + $result = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + $newValue = $this->decrementItem($normalizedKey, $value); + if ($newValue !== false) { + $result[$normalizedKey] = $newValue; + } + } + return $result; + } + + /* status */ + + /** + * Get capabilities of this adapter + * + * @return Capabilities + * @triggers getCapabilities.pre(PreEvent) + * @triggers getCapabilities.post(PostEvent) + * @triggers getCapabilities.exception(ExceptionEvent) + */ + public function getCapabilities() + { + $args = new ArrayObject(); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $result = $this->internalGetCapabilities(); + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + $result = false; + return $this->triggerException(__FUNCTION__, $args, $result, $e); + } + } + + /** + * Internal method to get capabilities of this adapter + * + * @return Capabilities + */ + protected function internalGetCapabilities() + { + if ($this->capabilities === null) { + $this->capabilityMarker = new stdClass(); + $this->capabilities = new Capabilities($this, $this->capabilityMarker); + } + return $this->capabilities; + } + + /* internal */ + + /** + * Validates and normalizes a key + * + * @param string $key + * @return void + * @throws Exception\InvalidArgumentException On an invalid key + */ + protected function normalizeKey(& $key) + { + $key = (string) $key; + + if ($key === '') { + throw new Exception\InvalidArgumentException( + "An empty key isn't allowed" + ); + } elseif (($p = $this->getOptions()->getKeyPattern()) && !preg_match($p, $key)) { + throw new Exception\InvalidArgumentException( + "The key '{$key}' doesn't match agains pattern '{$p}'" + ); + } + } + + /** + * Validates and normalizes multiple keys + * + * @param array $keys + * @return void + * @throws Exception\InvalidArgumentException On an invalid key + */ + protected function normalizeKeys(array & $keys) + { + if (!$keys) { + throw new Exception\InvalidArgumentException( + "An empty list of keys isn't allowed" + ); + } + + array_walk($keys, array($this, 'normalizeKey')); + $keys = array_values(array_unique($keys)); + } + + /** + * Validates and normalizes an array of key-value pairs + * + * @param array $keyValuePairs + * @return void + * @throws Exception\InvalidArgumentException On an invalid key + */ + protected function normalizeKeyValuePairs(array & $keyValuePairs) + { + $normalizedKeyValuePairs = array(); + foreach ($keyValuePairs as $key => $value) { + $this->normalizeKey($key); + $normalizedKeyValuePairs[$key] = $value; + } + $keyValuePairs = $normalizedKeyValuePairs; + } +} diff --git a/src/Storage/Adapter/AbstractZendServer.php b/src/Storage/Adapter/AbstractZendServer.php new file mode 100644 index 000000000..b18991add --- /dev/null +++ b/src/Storage/Adapter/AbstractZendServer.php @@ -0,0 +1,267 @@ +getOptions()->getNamespace() . self::NAMESPACE_SEPARATOR; + $internalKey = $prefix . $normalizedKey; + + $result = $this->zdcFetch($internalKey); + if ($result === false) { + $success = false; + $result = null; + } else { + $success = true; + $casToken = $result; + } + + return $result; + } + + /** + * Internal method to get multiple items. + * + * @param array $normalizedKeys + * @return array Associative array of keys and values + * @throws Exception\ExceptionInterface + */ + protected function internalGetItems(array & $normalizedKeys) + { + $prefix = $this->getOptions()->getNamespace() . self::NAMESPACE_SEPARATOR; + $prefixL = strlen($prefix); + + $internalKeys = array(); + foreach ($normalizedKeys as $normalizedKey) { + $internalKeys[] = $prefix . $normalizedKey; + } + + $fetch = $this->zdcFetchMulti($internalKeys); + $result = array(); + foreach ($fetch as $k => & $v) { + $result[ substr($k, $prefixL) ] = $v; + } + + return $result; + } + + /** + * Internal method to test if an item exists. + * + * @param string $normalizedKey + * @param array $normalizedOptions + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalHasItem(& $normalizedKey) + { + + $prefix = $this->getOptions()->getNamespace() . self::NAMESPACE_SEPARATOR; + return ($this->zdcFetch($prefix . $normalizedKey) !== false); + } + + /** + * Internal method to test multiple items. + * + * @param array $keys + * @return array Array of found keys + * @throws Exception\ExceptionInterface + */ + protected function internalHasItems(array & $normalizedKeys) + { + $prefix = $this->getOptions()->getNamespace() . self::NAMESPACE_SEPARATOR; + $prefixL = strlen($prefix); + + $internalKeys = array(); + foreach ($normalizedKeys as $normalizedKey) { + $internalKeys[] = $prefix . $normalizedKey; + } + + $fetch = $this->zdcFetchMulti($internalKeys); + $result = array(); + foreach ($fetch as $internalKey => & $value) { + $result[] = substr($internalKey, $prefixL); + } + + return $result; + } + + /** + * Get metadata for multiple items + * + * @param array $normalizedKeys + * @return array Associative array of keys and metadata + * + * @triggers getMetadatas.pre(PreEvent) + * @triggers getMetadatas.post(PostEvent) + * @triggers getMetadatas.exception(ExceptionEvent) + */ + protected function internalGetMetadatas(array & $normalizedKeys) + { + $prefix = $this->getOptions()->getNamespace() . self::NAMESPACE_SEPARATOR; + $prefixL = strlen($prefix); + + $internalKeys = array(); + foreach ($normalizedKeys as $normalizedKey) { + $internalKeys[] = $prefix . $normalizedKey; + } + + $fetch = $this->zdcFetchMulti($internalKeys); + $result = array(); + foreach ($fetch as $internalKey => $value) { + $result[ substr($internalKey, $prefixL) ] = array(); + } + + return $result; + } + + /* writing */ + + /** + * Internal method to store an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalSetItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $internalKey = $options->getNamespace() . self::NAMESPACE_SEPARATOR . $normalizedKey; + $this->zdcStore($internalKey, $value, $options->getTtl()); + return true; + } + + /** + * Internal method to remove an item. + * + * @param string $normalizedKey + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItem(& $normalizedKey) + { + $internalKey = $this->getOptions()->getNamespace() . self::NAMESPACE_SEPARATOR . $normalizedKey; + return $this->zdcDelete($internalKey); + } + + /* status */ + + /** + * Internal method to get capabilities of this adapter + * + * @return Capabilities + */ + protected function internalGetCapabilities() + { + if ($this->capabilities === null) { + $this->capabilityMarker = new stdClass(); + $this->capabilities = new Capabilities( + $this, + $this->capabilityMarker, + array( + 'supportedDatatypes' => array( + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => true, + 'object' => 'object', + 'resource' => false, + ), + 'supportedMetadata' => array(), + 'maxTtl' => 0, + 'staticTtl' => true, + 'ttlPrecision' => 1, + 'useRequestTime' => false, + 'expiredRead' => false, + 'maxKeyLength' => 0, + 'namespaceIsPrefix' => true, + 'namespaceSeparator' => self::NAMESPACE_SEPARATOR, + ) + ); + } + + return $this->capabilities; + } + + /* internal wrapper of zend_[disk|shm]_cache_* functions */ + + /** + * Store data into Zend Data Cache (zdc) + * + * @param string $internalKey + * @param mixed $value + * @param int $ttl + * @return void + * @throws Exception\RuntimeException + */ + abstract protected function zdcStore($internalKey, $value, $ttl); + + /** + * Fetch a single item from Zend Data Cache (zdc) + * + * @param string $internalKey + * @return mixed The stored value or FALSE if item wasn't found + * @throws Exception\RuntimeException + */ + abstract protected function zdcFetch($internalKey); + + /** + * Fetch multiple items from Zend Data Cache (zdc) + * + * @param array $internalKeys + * @return array All found items + * @throws Exception\RuntimeException + */ + abstract protected function zdcFetchMulti(array $internalKeys); + + /** + * Delete data from Zend Data Cache (zdc) + * + * @param string $internalKey + * @return boolean + * @throws Exception\RuntimeException + */ + abstract protected function zdcDelete($internalKey); +} diff --git a/src/Storage/Adapter/AdapterOptions.php b/src/Storage/Adapter/AdapterOptions.php new file mode 100644 index 000000000..70adb9d5b --- /dev/null +++ b/src/Storage/Adapter/AdapterOptions.php @@ -0,0 +1,269 @@ +adapter = $adapter; + return $this; + } + + /** + * Set key pattern + * + * @param null|string $keyPattern + * @return AdapterOptions + */ + public function setKeyPattern($keyPattern) + { + $keyPattern = (string) $keyPattern; + if ($this->keyPattern !== $keyPattern) { + // validate pattern + if ($keyPattern !== '') { + ErrorHandler::start(E_WARNING); + $result = preg_match($keyPattern, ''); + $error = ErrorHandler::stop(); + if ($result === false) { + throw new Exception\InvalidArgumentException(sprintf( + 'Invalid pattern "%s"%s', + $keyPattern, + ($error ? ': ' . $error->getMessage() : '') + ), 0, $error); + } + } + + $this->triggerOptionEvent('key_pattern', $keyPattern); + $this->keyPattern = $keyPattern; + } + + return $this; + } + + /** + * Get key pattern + * + * @return string + */ + public function getKeyPattern() + { + return $this->keyPattern; + } + + /** + * Set namespace. + * + * @param string $namespace + * @return AdapterOptions + */ + public function setNamespace($namespace) + { + $namespace = (string) $namespace; + if ($this->namespace !== $namespace) { + $this->triggerOptionEvent('namespace', $namespace); + $this->namespace = $namespace; + } + + return $this; + } + + /** + * Get namespace + * + * @return string + */ + public function getNamespace() + { + return $this->namespace; + } + + /** + * Enable/Disable reading data from cache. + * + * @param boolean $readable + * @return AbstractAdapter + */ + public function setReadable($readable) + { + $readable = (bool) $readable; + if ($this->readable !== $readable) { + $this->triggerOptionEvent('readable', $readable); + $this->readable = $readable; + } + return $this; + } + + /** + * If reading data from cache enabled. + * + * @return boolean + */ + public function getReadable() + { + return $this->readable; + } + + /** + * Set time to live. + * + * @param int|float $ttl + * @return AdapterOptions + */ + public function setTtl($ttl) + { + $this->normalizeTtl($ttl); + if ($this->ttl !== $ttl) { + $this->triggerOptionEvent('ttl', $ttl); + $this->ttl = $ttl; + } + return $this; + } + + /** + * Get time to live. + * + * @return float + */ + public function getTtl() + { + return $this->ttl; + } + + /** + * Enable/Disable writing data to cache. + * + * @param boolean $writable + * @return AdapterOptions + */ + public function setWritable($writable) + { + $writable = (bool) $writable; + if ($this->writable !== $writable) { + $this->triggerOptionEvent('writable', $writable); + $this->writable = $writable; + } + return $this; + } + + /** + * If writing data to cache enabled. + * + * @return boolean + */ + public function getWritable() + { + return $this->writable; + } + + /** + * Triggers an option event if this options instance has a connection to + * an adapter implements EventsCapableInterface. + * + * @param string $optionName + * @param mixed $optionValue + * @return void + */ + protected function triggerOptionEvent($optionName, $optionValue) + { + if ($this->adapter instanceof EventsCapableInterface) { + $event = new Event('option', $this->adapter, new ArrayObject(array($optionName => $optionValue))); + $this->adapter->getEventManager()->trigger($event); + } + } + + /** + * Validates and normalize a TTL. + * + * @param int|float $ttl + * @throws Exception\InvalidArgumentException + * @return void + */ + protected function normalizeTtl(&$ttl) + { + if (!is_int($ttl)) { + $ttl = (float) $ttl; + + // convert to int if possible + if ($ttl === (float) (int) $ttl) { + $ttl = (int) $ttl; + } + } + + if ($ttl < 0) { + throw new Exception\InvalidArgumentException("TTL can't be negative"); + } + } +} diff --git a/src/Storage/Adapter/Apc.php b/src/Storage/Adapter/Apc.php new file mode 100644 index 000000000..3785ecb4a --- /dev/null +++ b/src/Storage/Adapter/Apc.php @@ -0,0 +1,696 @@ + 0) { + throw new Exception\ExtensionNotLoadedException("Missing ext/apc >= 3.1.6"); + } + + $enabled = ini_get('apc.enabled'); + if (PHP_SAPI == 'cli') { + $enabled = $enabled && (bool) ini_get('apc.enable_cli'); + } + + if (!$enabled) { + throw new Exception\ExtensionNotLoadedException( + "ext/apc is disabled - see 'apc.enabled' and 'apc.enable_cli'" + ); + } + + parent::__construct($options); + } + + /* options */ + + /** + * Set options. + * + * @param array|Traversable|ApcOptions $options + * @return Apc + * @see getOptions() + */ + public function setOptions($options) + { + if (!$options instanceof ApcOptions) { + $options = new ApcOptions($options); + } + + return parent::setOptions($options); + } + + /** + * Get options. + * + * @return ApcOptions + * @see setOptions() + */ + public function getOptions() + { + if (!$this->options) { + $this->setOptions(new ApcOptions()); + } + return $this->options; + } + + /* TotalSpaceCapableInterface */ + + /** + * Get total space in bytes + * + * @return int|float + */ + public function getTotalSpace() + { + if ($this->totalSpace !== null) { + $smaInfo = apc_sma_info(true); + $this->totalSpace = $smaInfo['num_seg'] * $smaInfo['seg_size']; + } + + return $this->totalSpace; + } + + /* AvailableSpaceCapableInterface */ + + /** + * Get available space in bytes + * + * @return int|float + */ + public function getAvailableSpace() + { + $smaInfo = apc_sma_info(true); + return $smaInfo['avail_mem']; + } + + /* IterableInterface */ + + /** + * Get the storage iterator + * + * @return ApcIterator + */ + public function getIterator() + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $pattern = '/^' . preg_quote($prefix, '/') . '/'; + $format = 0; + + $baseIt = new BaseApcIterator('user', $pattern, 0, 1, \APC_LIST_ACTIVE); + return new ApcIterator($this, $baseIt, $prefix); + } + + /* FlushableInterface */ + + /** + * Flush the whole storage + * + * @return boolean + */ + public function flush() + { + return apc_clear_cache('user'); + } + + /* ClearByNamespaceInterface */ + + /** + * Remove items by given namespace + * + * @param string $prefix + * @return boolean + */ + public function clearByNamespace($namespace) + { + $options = $this->getOptions(); + $prefix = $namespace . $options->getNamespaceSeparator(); + $pattern = '/^' . preg_quote($prefix, '/') . '+/'; + return apc_delete(new BaseApcIterator('user', $pattern, 0, 1, \APC_LIST_ACTIVE)); + } + + /* ClearByPrefixInterface */ + + /** + * Remove items matching given prefix + * + * @param string $prefix + * @return boolean + */ + public function clearByPrefix($prefix) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator() . $prefix; + $pattern = '/^' . preg_quote($prefix, '/') . '+/'; + return apc_delete(new BaseApcIterator('user', $pattern, 0, 1, \APC_LIST_ACTIVE)); + } + + /* reading */ + + /** + * Internal method to get an item. + * + * @param string $normalizedKey + * @param boolean $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $result = apc_fetch($internalKey, $success); + + if (!$success) { + return null; + } + + $casToken = $result; + return $result; + } + + /** + * Internal method to get multiple items. + * + * @param array $normalizedKeys + * @return array Associative array of keys and values + * @throws Exception\ExceptionInterface + */ + protected function internalGetItems(array & $normalizedKeys) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + + $internalKeys = array(); + foreach ($normalizedKeys as $normalizedKey) { + $internalKeys[] = $prefix . $normalizedKey; + } + + $fetch = apc_fetch($internalKeys); + + // remove namespace prefix + $prefixL = strlen($prefix); + $result = array(); + foreach ($fetch as $internalKey => & $value) { + $result[ substr($internalKey, $prefixL) ] = $value; + } + + return $result; + } + + /** + * Internal method to test if an item exists. + * + * @param string $normalizedKey + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalHasItem(& $normalizedKey) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + return apc_exists($prefix . $normalizedKey); + } + + /** + * Internal method to test multiple items. + * + * @param array $keys + * @return array Array of found keys + * @throws Exception\ExceptionInterface + */ + protected function internalHasItems(array & $normalizedKeys) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + + $internalKeys = array(); + foreach ($normalizedKeys as $normalizedKey) { + $internalKeys[] = $prefix . $normalizedKey; + } + + $exists = apc_exists($internalKeys); + $result = array(); + $prefixL = strlen($prefix); + foreach ($exists as $internalKey => $bool) { + if ($bool === true) { + $result[] = substr($internalKey, $prefixL); + } + } + + return $result; + } + + /** + * Get metadata of an item. + * + * @param string $normalizedKey + * @return array|boolean Metadata on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetMetadata(& $normalizedKey) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + + // @see http://pecl.php.net/bugs/bug.php?id=22564 + if (!apc_exists($internalKey)) { + $metadata = false; + } else { + $format = \APC_ITER_ALL ^ \APC_ITER_VALUE ^ \APC_ITER_TYPE ^ \APC_ITER_REFCOUNT; + $regexp = '/^' . preg_quote($internalKey, '/') . '$/'; + $it = new BaseApcIterator('user', $regexp, $format, 100, \APC_LIST_ACTIVE); + $metadata = $it->current(); + } + + if (!$metadata) { + return false; + } + + $this->normalizeMetadata($metadata); + return $metadata; + } + + /** + * Get metadata of multiple items + * + * @param array $normalizedKeys + * @return array Associative array of keys and metadata + * + * @triggers getMetadatas.pre(PreEvent) + * @triggers getMetadatas.post(PostEvent) + * @triggers getMetadatas.exception(ExceptionEvent) + */ + protected function internalGetMetadatas(array & $normalizedKeys) + { + $keysRegExp = array(); + foreach ($normalizedKeys as $normalizedKey) { + $keysRegExp[] = preg_quote($normalizedKey, '/'); + } + + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $regexp = '/^' . preg_quote($prefix, '/') . '(' . implode('|', $keysRegExp) . ')' . '$/'; + $format = \APC_ITER_ALL ^ \APC_ITER_VALUE ^ \APC_ITER_TYPE ^ \APC_ITER_REFCOUNT; + + $it = new BaseApcIterator('user', $regexp, $format, 100, \APC_LIST_ACTIVE); + $result = array(); + $prefixL = strlen($prefix); + foreach ($it as $internalKey => $metadata) { + // @see http://pecl.php.net/bugs/bug.php?id=22564 + if (!apc_exists($internalKey)) { + continue; + } + + $this->normalizeMetadata($metadata); + $result[ substr($internalKey, $prefixL) ] = & $metadata; + } + + return $result; + } + + /* writing */ + + /** + * Internal method to store an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalSetItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $ttl = $options->getTtl(); + + if (!apc_store($internalKey, $value, $ttl)) { + $type = is_object($value) ? get_class($value) : gettype($value); + throw new Exception\RuntimeException( + "apc_store('{$internalKey}', <{$type}>, {$ttl}) failed" + ); + } + + return true; + } + + /** + * Internal method to store multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalSetItems(array & $normalizedKeyValuePairs) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + + $internalKeyValuePairs = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => &$value) { + $internalKey = $prefix . $normalizedKey; + $internalKeyValuePairs[$internalKey] = &$value; + } + + $failedKeys = apc_store($internalKeyValuePairs, null, $options->getTtl()); + $failedKeys = array_keys($failedKeys); + + // remove prefix + $prefixL = strlen($prefix); + foreach ($failedKeys as & $key) { + $key = substr($key, $prefixL); + } + + return $failedKeys; + } + + /** + * Add an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalAddItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $ttl = $options->getTtl(); + + if (!apc_add($internalKey, $value, $ttl)) { + if (apc_exists($internalKey)) { + return false; + } + + $type = is_object($value) ? get_class($value) : gettype($value); + throw new Exception\RuntimeException( + "apc_add('{$internalKey}', <{$type}>, {$ttl}) failed" + ); + } + + return true; + } + + /** + * Internal method to add multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalAddItems(array & $normalizedKeyValuePairs) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + + $internalKeyValuePairs = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + $internalKey = $prefix . $normalizedKey; + $internalKeyValuePairs[$internalKey] = $value; + } + + $failedKeys = apc_add($internalKeyValuePairs, null, $options->getTtl()); + $failedKeys = array_keys($failedKeys); + + // remove prefix + $prefixL = strlen($prefix); + foreach ($failedKeys as & $key) { + $key = substr($key, $prefixL); + } + + return $failedKeys; + } + + /** + * Internal method to replace an existing item. + * + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalReplaceItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $ttl = $options->getTtl(); + + if (!apc_exists($internalKey)) { + return false; + } + + if (!apc_store($internalKey, $value, $ttl)) { + $type = is_object($value) ? get_class($value) : gettype($value); + throw new Exception\RuntimeException( + "apc_store('{$internalKey}', <{$type}>, {$ttl}) failed" + ); + } + + return true; + } + + /** + * Internal method to remove an item. + * + * @param string $normalizedKey + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItem(& $normalizedKey) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + return apc_delete($internalKey); + } + + /** + * Internal method to remove multiple items. + * + * @param array $keys + * @return array Array of not removed keys + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItems(array & $normalizedKeys) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + + $internalKeys = array(); + foreach ($normalizedKeys as $normalizedKey) { + $internalKeys[] = $prefix . $normalizedKey; + } + + $failedKeys = apc_delete($internalKeys); + + // remove prefix + $prefixL = strlen($prefix); + foreach ($failedKeys as & $key) { + $key = substr($key, $prefixL); + } + + return $failedKeys; + } + + /** + * Internal method to increment an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|boolean The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalIncrementItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $ttl = $options->getTtl(); + $value = (int) $value; + $newValue = apc_inc($internalKey, $value); + + // initial value + if ($newValue === false) { + $ttl = $options->getTtl(); + $newValue = $value; + if (!apc_add($internalKey, $newValue, $ttl)) { + throw new Exception\RuntimeException( + "apc_add('{$internalKey}', {$newValue}, {$ttl}) failed" + ); + } + } + + return $newValue; + } + + /** + * Internal method to decrement an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|boolean The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalDecrementItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $value = (int) $value; + $newValue = apc_dec($internalKey, $value); + + // initial value + if ($newValue === false) { + $ttl = $options->getTtl(); + $newValue = -$value; + if (!apc_add($internalKey, $newValue, $ttl)) { + throw new Exception\RuntimeException( + "apc_add('{$internalKey}', {$newValue}, {$ttl}) failed" + ); + } + } + + return $newValue; + } + + /* status */ + + /** + * Internal method to get capabilities of this adapter + * + * @return Capabilities + */ + protected function internalGetCapabilities() + { + if ($this->capabilities === null) { + $marker = new stdClass(); + $capabilities = new Capabilities( + $this, + $marker, + array( + 'supportedDatatypes' => array( + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => true, + 'object' => 'object', + 'resource' => false, + ), + 'supportedMetadata' => array( + 'internal_key', + 'atime', 'ctime', 'mtime', 'rtime', + 'size', 'hits', 'ttl', + ), + 'minTtl' => 1, + 'maxTtl' => 0, + 'staticTtl' => true, + 'ttlPrecision' => 1, + 'useRequestTime' => (bool) ini_get('apc.use_request_time'), + 'expiredRead' => false, + 'maxKeyLength' => 5182, + 'namespaceIsPrefix' => true, + 'namespaceSeparator' => $this->getOptions()->getNamespaceSeparator(), + ) + ); + + // update namespace separator on change option + $this->getEventManager()->attach('option', function ($event) use ($capabilities, $marker) { + $params = $event->getParams(); + + if (isset($params['namespace_separator'])) { + $capabilities->setNamespaceSeparator($marker, $params['namespace_separator']); + } + }); + + $this->capabilities = $capabilities; + $this->capabilityMarker = $marker; + } + + return $this->capabilities; + } + + /* internal */ + + /** + * Normalize metadata to work with APC + * + * @param array $metadata + * @return void + */ + protected function normalizeMetadata(array & $metadata) + { + $metadata['internal_key'] = $metadata['key']; + $metadata['ctime'] = $metadata['creation_time']; + $metadata['atime'] = $metadata['access_time']; + $metadata['rtime'] = $metadata['deletion_time']; + $metadata['size'] = $metadata['mem_size']; + $metadata['hits'] = $metadata['num_hits']; + + unset( + $metadata['key'], + $metadata['creation_time'], + $metadata['access_time'], + $metadata['deletion_time'], + $metadata['mem_size'], + $metadata['num_hits'] + ); + } +} diff --git a/src/Storage/Adapter/ApcIterator.php b/src/Storage/Adapter/ApcIterator.php new file mode 100644 index 000000000..6cf8b8176 --- /dev/null +++ b/src/Storage/Adapter/ApcIterator.php @@ -0,0 +1,164 @@ +storage = $storage; + $this->baseIterator = $baseIterator; + $this->prefixLength = strlen($prefix); + } + + /** + * Get storage instance + * + * @return Apc + */ + public function getStorage() + { + return $this->storage; + } + + /** + * Get iterator mode + * + * @return int Value of IteratorInterface::CURRENT_AS_* + */ + public function getMode() + { + return $this->mode; + } + + /** + * Set iterator mode + * + * @param int $mode + * @return ApcIterator Fluent interface + */ + public function setMode($mode) + { + $this->mode = (int) $mode; + return $this; + } + + /* Iterator */ + + /** + * Get current key, value or metadata. + * + * @return mixed + */ + public function current() + { + if ($this->mode == IteratorInterface::CURRENT_AS_SELF) { + return $this; + } + + $key = $this->key(); + + if ($this->mode == IteratorInterface::CURRENT_AS_VALUE) { + return $this->storage->getItem($key); + } elseif ($this->mode == IteratorInterface::CURRENT_AS_METADATA) { + return $this->storage->getMetadata($key); + } + + return $key; + } + + /** + * Get current key + * + * @return string + */ + public function key() + { + $key = $this->baseIterator->key(); + + // remove namespace prefix + return substr($key, $this->prefixLength); + } + + /** + * Move forward to next element + * + * @return void + */ + public function next() + { + $this->baseIterator->next(); + } + + /** + * Checks if current position is valid + * + * @return boolean + */ + public function valid() + { + return $this->baseIterator->valid(); + } + + /** + * Rewind the Iterator to the first element. + * + * @return void + */ + public function rewind() + { + return $this->baseIterator->rewind(); + } +} diff --git a/src/Storage/Adapter/ApcOptions.php b/src/Storage/Adapter/ApcOptions.php new file mode 100644 index 000000000..2e5d63e6c --- /dev/null +++ b/src/Storage/Adapter/ApcOptions.php @@ -0,0 +1,52 @@ +triggerOptionEvent('namespace_separator', $namespaceSeparator); + $this->namespaceSeparator = $namespaceSeparator; + return $this; + } + + /** + * Get namespace separator + * + * @return string + */ + public function getNamespaceSeparator() + { + return $this->namespaceSeparator; + } +} diff --git a/src/Storage/Adapter/Dba.php b/src/Storage/Adapter/Dba.php new file mode 100644 index 000000000..a85ac19a8 --- /dev/null +++ b/src/Storage/Adapter/Dba.php @@ -0,0 +1,524 @@ +_close(); + + parent::__destruct(); + } + + /* options */ + + /** + * Set options. + * + * @param array|Traversable|DbaOptions $options + * @return Apc + * @see getOptions() + */ + public function setOptions($options) + { + if (!$options instanceof DbaOptions) { + $options = new DbaOptions($options); + } + + return parent::setOptions($options); + } + + /** + * Get options. + * + * @return DbaOptions + * @see setOptions() + */ + public function getOptions() + { + if (!$this->options) { + $this->setOptions(new DbaOptions()); + } + return $this->options; + } + + /* TotalSpaceCapableInterface */ + + /** + * Get total space in bytes + * + * @return int|float + */ + public function getTotalSpace() + { + if ($this->totalSpace !== null) { + $pathname = $this->getOptions()->getPathname(); + + if ($pathname === '') { + throw new Exception\LogicException('No pathname to database file'); + } + + ErrorHandler::start(); + $total = disk_total_space($pathname); + $error = ErrorHandler::stop(); + if ($total === false) { + throw new Exception\RuntimeException("Can't detect total space of '{$pathname}'", 0, $error); + } + + // clean total space buffer on change pathname + $events = $this->getEventManager(); + $handle = null; + $totalSpace = & $this->totalSpace; + $callback = function ($event) use (& $events, & $handle, & $totalSpace) { + $params = $event->getParams(); + if (isset($params['pathname'])) { + $totalSpace = null; + $events->detach($handle); + } + }; + $handle = $events->attach($callback); + } + + return $this->totalSpace; + } + + /* AvailableSpaceCapableInterface */ + + /** + * Get available space in bytes + * + * @return int|float + */ + public function getAvailableSpace() + { + $pathname = $this->getOptions()->getPathname(); + + if ($pathname === '') { + throw new Exception\LogicException('No pathname to database file'); + } + + ErrorHandler::start(); + $avail = disk_free_space($pathname); + $error = ErrorHandler::stop(); + if ($avail === false) { + throw new Exception\RuntimeException("Can't detect free space of '{$pathname}'", 0, $error); + } + + return $avail; + } + + /* FlushableInterface */ + + /** + * Flush the whole storage + * + * @return boolean + */ + public function flush() + { + $pathname = $this->getOptions()->getPathname(); + + if ($pathname === '') { + throw new Exception\LogicException('No pathname to database file'); + } + + if (file_exists($pathname)) { + + // close the dba file before delete + // and reopen (create) on next use + $this->_close(); + + ErrorHandler::start(); + $result = unlink($pathname); + $error = ErrorHandler::stop(); + if (!$result) { + throw new Exception\RuntimeException("unlink('{$pathname}') failed", 0, $error); + } + } + + return true; + } + + /* ClearByNamespaceInterface */ + + /** + * Remove items by given namespace + * + * @param string $prefix + * @return boolean + */ + public function clearByNamespace($namespace) + { + $prefix = $namespace . $this->getOptions()->getNamespaceSeparator(); + $prefixl = strlen($prefix); + $result = true; + + $this->_open(); + + do { // Workaround for PHP-Bug #62491 & #62492 + $recheck = false; + $internalKey = dba_firstkey($this->handle); + while ($internalKey !== false && $internalKey !== null) { + if (substr($internalKey, 0, $prefixl) === $prefix) { + $result = dba_delete($internalKey, $this->handle) && $result; + } + + $internalKey = dba_nextkey($this->handle); + } + } while ($recheck); + + return $result; + } + + /* ClearByPrefixInterface */ + + /** + * Remove items matching given prefix + * + * @param string $prefix + * @return boolean + */ + public function clearByPrefix($prefix) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator() . $prefix; + $prefixl = strlen($prefix); + $result = true; + + $this->_open(); + + do { // Workaround for PHP-Bug #62491 & #62492 + $recheck = false; + $internalKey = dba_firstkey($this->handle); + while ($internalKey !== false && $internalKey !== null) { + if (substr($internalKey, 0, $prefixl) === $prefix) { + $result = dba_delete($internalKey, $this->handle) && $result; + $recheck = true; + } + + $internalKey = dba_nextkey($this->handle); + } + } while ($recheck); + + return $result; + } + + /* IterableInterface */ + + /** + * Get the storage iterator + * + * @return ApcIterator + */ + public function getIterator() + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + + return new DbaIterator($this, $this->handle, $prefix); + } + + /* OptimizableInterface */ + + /** + * Optimize the storage + * + * @return boolean + * @return Exception\RuntimeException + */ + public function optimize() + { + $this->_open(); + if (!dba_optimize($this->handle)) { + throw new Exception\RuntimeException('dba_optimize failed'); + } + return true; + } + + /* reading */ + + /** + * Internal method to get an item. + * + * @param string $normalizedKey + * @param boolean $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + + $this->_open(); + $value = dba_fetch($internalKey, $this->handle); + + if ($value === false) { + $success = false; + return null; + } + + $success = true; + $casToken = $value; + return $value; + } + + /** + * Internal method to test if an item exists. + * + * @param string $normalizedKey + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalHasItem(& $normalizedKey) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + + $this->_open(); + return dba_exists($internalKey, $this->handle); + } + + /* writing */ + + /** + * Internal method to store an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalSetItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + + $this->_open(); + if (!dba_replace($internalKey, $value, $this->handle)) { + throw new Exception\RuntimeException("dba_replace('{$internalKey}', ...) failed"); + } + + return true; + } + + /** + * Add an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalAddItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + + $this->_open(); + + // Workaround for PHP-Bug #54242 & #62489 + if (dba_exists($internalKey, $this->handle)) { + return false; + } + + // Workaround for PHP-Bug #54242 & #62489 + // dba_insert returns true if key already exists + ErrorHandler::start(); + $result = dba_insert($internalKey, $value, $this->handle); + $error = ErrorHandler::stop(); + if (!$result || $error) { + return false; + } + + return true; + } + + /** + * Internal method to remove an item. + * + * @param string $normalizedKey + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItem(& $normalizedKey) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + + $this->_open(); + + // Workaround for PHP-Bug #62490 + if (!dba_exists($internalKey, $this->handle)) { + return false; + } + + return dba_delete($internalKey, $this->handle); + } + + /* status */ + + /** + * Internal method to get capabilities of this adapter + * + * @return Capabilities + */ + protected function internalGetCapabilities() + { + if ($this->capabilities === null) { + $marker = new stdClass(); + $capabilities = new Capabilities( + $this, + $marker, + array( + 'supportedDatatypes' => array( + 'NULL' => 'string', + 'boolean' => 'string', + 'integer' => 'string', + 'double' => 'string', + 'string' => true, + 'array' => false, + 'object' => false, + 'resource' => false, + ), + 'minTtl' => 0, + 'supportedMetadata' => array(), + 'maxKeyLength' => 0, // TODO: maxKeyLength ???? + 'namespaceIsPrefix' => true, + 'namespaceSeparator' => $this->getOptions()->getNamespaceSeparator(), + ) + ); + + // update namespace separator on change option + $this->getEventManager()->attach('option', function ($event) use ($capabilities, $marker) { + $params = $event->getParams(); + + if (isset($params['namespace_separator'])) { + $capabilities->setNamespaceSeparator($marker, $params['namespace_separator']); + } + }); + + $this->capabilities = $capabilities; + $this->capabilityMarker = $marker; + } + + return $this->capabilities; + } + + /** + * Open the database if not already done. + * + * @return void + * @throws Exception\LogicException + * @throws Exception\RuntimeException + */ + protected function _open() + { + if (!$this->handle) { + $options = $this->getOptions(); + $pathname = $options->getPathname(); + $mode = $options->getMode(); + $handler = $options->getHandler(); + + if ($pathname === '') { + throw new Exception\LogicException('No pathname to database file'); + } + + ErrorHandler::start(); + $dba = dba_open($pathname, $mode, $handler); + $err = ErrorHandler::stop(); + if (!$dba) { + throw new Exception\RuntimeException( + "dba_open('{$pathname}', '{$mode}', '{$handler}') failed", 0, $err + ); + } + $this->handle = $dba; + } + } + + /** + * Close database file if opened + * + * @return void + */ + protected function _close() + { + if ($this->handle) { + ErrorHandler::start(E_WARNING); + dba_close($this->handle); + ErrorHandler::stop(); + $this->handle = null; + } + } +} diff --git a/src/Storage/Adapter/DbaIterator.php b/src/Storage/Adapter/DbaIterator.php new file mode 100644 index 000000000..ceaae999d --- /dev/null +++ b/src/Storage/Adapter/DbaIterator.php @@ -0,0 +1,196 @@ +storage = $storage; + $this->handle = $handle; + $this->prefixLength = strlen($prefix); + + $this->rewind(); + } + + /** + * Get storage instance + * + * @return Dba + */ + public function getStorage() + { + return $this->storage; + } + + /** + * Get iterator mode + * + * @return int Value of IteratorInterface::CURRENT_AS_* + */ + public function getMode() + { + return $this->mode; + } + + /** + * Set iterator mode + * + * @param int $mode + * @return ApcIterator Fluent interface + */ + public function setMode($mode) + { + $this->mode = (int) $mode; + return $this; + } + + /* Iterator */ + + /** + * Get current key, value or metadata. + * + * @return mixed + * @throws Exception\RuntimeException + */ + public function current() + { + if ($this->mode == IteratorInterface::CURRENT_AS_SELF) { + return $this; + } + + $key = $this->key(); + + if ($this->mode == IteratorInterface::CURRENT_AS_VALUE) { + return $this->storage->getItem($key); + } elseif ($this->mode == IteratorInterface::CURRENT_AS_METADATA) { + return $this->storage->getMetadata($key); + } + + return $key; + } + + /** + * Get current key + * + * @return string + * @throws Exception\RuntimeException + */ + public function key() + { + if ($this->currentInternalKey === false) { + throw new Exception\RuntimeException("Iterater is on an invalid state"); + } + + // remove namespace prefix + return substr($this->currentInternalKey, $this->prefixLength); + } + + /** + * Move forward to next element + * + * @return void + * @throws Exception\RuntimeException + */ + public function next() + { + if ($this->currentInternalKey === false) { + throw new Exception\RuntimeException("Iterater is on an invalid state"); + } + + $this->currentInternalKey = dba_nextkey($this->handle); + + // Workaround for PHP-Bug #62492 + if ($this->currentInternalKey === null) { + $this->currentInternalKey = false; + } + } + + /** + * Checks if current position is valid + * + * @return boolean + */ + public function valid() + { + return ($this->currentInternalKey !== false); + } + + /** + * Rewind the Iterator to the first element. + * + * @return void + * @throws Exception\RuntimeException + */ + public function rewind() + { + if ($this->currentInternalKey === false) { + throw new Exception\RuntimeException("Iterator is on an invalid state"); + } + + $this->currentInternalKey = dba_firstkey($this->handle); + + // Workaround for PHP-Bug #62492 + if ($this->currentInternalKey === null) { + $this->currentInternalKey = false; + } + } +} diff --git a/src/Storage/Adapter/DbaOptions.php b/src/Storage/Adapter/DbaOptions.php new file mode 100644 index 000000000..bcf78379a --- /dev/null +++ b/src/Storage/Adapter/DbaOptions.php @@ -0,0 +1,134 @@ +triggerOptionEvent('namespace_separator', $namespaceSeparator); + $this->namespaceSeparator = $namespaceSeparator; + return $this; + } + + /** + * Get namespace separator + * + * @return string + */ + public function getNamespaceSeparator() + { + return $this->namespaceSeparator; + } + + /** + * Set pathname to database file + * + * @param string $pathname + * @return DbaOptions + */ + public function setPathname($pathname) + { + $this->pathname = (string) $pathname; + $this->triggerOptionEvent('pathname', $pathname); + return $this; + } + + /** + * Get pathname to database file + * + * @return string + */ + public function getPathname() + { + return $this->pathname; + } + + /** + * + * + * @param unknown_type $mode + * @return \Zend\Cache\Storage\Adapter\DbaOptions + */ + public function setMode($mode) + { + $this->mode = (string) $mode; + $this->triggerOptionEvent('mode', $mode); + return $this; + } + + public function getMode() + { + return $this->mode; + } + + public function setHandler($handler) + { + $handler = (string) $handler; + + if (!function_exists('dba_handlers') || !in_array($handler, dba_handlers())) { + throw new Exception\ExtensionNotLoadedException("DBA-Handler '{$handler}' not supported"); + } + + $this->triggerOptionEvent('handler', $handler); + $this->handler = $handler; + return $this; + } + + public function getHandler() + { + return $this->handler; + } +} diff --git a/src/Storage/Adapter/Filesystem.php b/src/Storage/Adapter/Filesystem.php new file mode 100644 index 000000000..505cf84a1 --- /dev/null +++ b/src/Storage/Adapter/Filesystem.php @@ -0,0 +1,1604 @@ +options) { + $this->setOptions(new FilesystemOptions()); + } + return $this->options; + } + + /* FlushableInterface */ + + /** + * Flush the whole storage + * + * @return boolean + */ + public function flush() + { + $flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_PATHNAME; + $dir = $this->getOptions()->getCacheDir(); + $clearFolder = null; + $clearFolder = function ($dir) use (& $clearFolder, $flags) { + $it = new GlobIterator($dir . \DIRECTORY_SEPARATOR . '*', $flags); + foreach ($it as $pathname) { + if ($it->isDir()) { + $clearFolder($pathname); + rmdir($pathname); + } else { + unlink($pathname); + } + } + }; + + ErrorHandler::start(); + $clearFolder($dir); + $error = ErrorHandler::stop(); + if ($error) { + throw new Exception\RuntimeException("Flushing directory '{$dir}' failed", 0, $error); + } + + return true; + } + + /* ClearExpiredInterface */ + + /** + * Remove expired items + * + * @return boolean + */ + public function clearExpired() + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + + $flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_FILEINFO; + $path = $options->getCacheDir() + . str_repeat(\DIRECTORY_SEPARATOR . $prefix . '*', $options->getDirLevel()) + . \DIRECTORY_SEPARATOR . $prefix . '*.dat'; + $glob = new GlobIterator($path, $flags); + $time = time(); + $ttl = $options->getTtl(); + + ErrorHandler::start(); + foreach ($glob as $entry) { + $mtime = $entry->getMTime(); + if ($time >= $mtime + $ttl) { + $pathname = $entry->getPathname(); + unlink($pathname); + + $tagPathname = substr($pathname, 0, -4) . '.tag'; + if (file_exists($tagPathname)) { + unlink($tagPathname); + } + } + } + $error = ErrorHandler::stop(); + if ($error) { + throw new Exception\RuntimeException("Failed to clear expired items", 0, $error); + } + + return true; + } + + /* ClearByNamespaceInterface */ + + /** + * Remove items by given namespace + * + * @param string $namespace + * @return boolean + */ + public function clearByNamespace($namespace) + { + $options = $this->getOptions(); + $nsPrefix = $namespace . $options->getNamespaceSeparator(); + + $flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_PATHNAME; + $path = $options->getCacheDir() + . str_repeat(\DIRECTORY_SEPARATOR . $nsPrefix . '*', $options->getDirLevel()) + . \DIRECTORY_SEPARATOR . $nsPrefix . '*'; + $glob = new GlobIterator($path, $flags); + $time = time(); + $ttl = $options->getTtl(); + + ErrorHandler::start(); + foreach ($glob as $pathname) { + unlink($pathname); + } + $error = ErrorHandler::stop(); + if ($error) { + throw new Exception\RuntimeException("Failed to remove file '{$pathname}'", 0, $error); + } + + return true; + } + + /* ClearByPrefixInterface */ + + /** + * Remove items matching given prefix + * + * @param string $prefix + * @return boolean + */ + public function clearByPrefix($prefix) + { + $options = $this->getOptions(); + $nsPrefix = $options->getNamespace() . $options->getNamespaceSeparator(); + + $flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_PATHNAME; + $path = $options->getCacheDir() + . str_repeat(\DIRECTORY_SEPARATOR . $nsPrefix . '*', $options->getDirLevel()) + . \DIRECTORY_SEPARATOR . $nsPrefix . $prefix . '*'; + $glob = new GlobIterator($path, $flags); + $time = time(); + $ttl = $options->getTtl(); + + ErrorHandler::start(); + foreach ($glob as $pathname) { + unlink($pathname); + } + $error = ErrorHandler::stop(); + if ($error) { + throw new Exception\RuntimeException("Failed to remove file '{$pathname}'", 0, $error); + } + + return true; + } + + /* TaggableInterface */ + + /** + * Set tags to an item by given key. + * An empty array will remove all tags. + * + * @param string $key + * @param string[] $tags + * @return boolean + */ + public function setTags($key, array $tags) + { + $this->normalizeKey($key); + if (!$this->internalHasItem($key)) { + return false; + } + + $filespec = $this->getFileSpec($key); + + if (!$tags) { + $this->unlink($filespec . '.tag'); + return true; + } + + $this->putFileContent($filespec . '.tag', implode("\n", $tags)); + return true; + } + + /** + * Get tags of an item by given key + * + * @param string $key + * @return string[]|FALSE + */ + public function getTags($key) + { + $this->normalizeKey($key); + if (!$this->internalHasItem($key)) { + return false; + } + + $filespec = $this->getFileSpec($key); + $tags = array(); + if (file_exists($filespec . '.tag')) { + $tags = explode("\n", $this->getFileContent($filespec . '.tag')); + } + + return $tags; + } + + /** + * Remove items matching given tags. + * + * If $disjunction only one of the given tags must match + * else all given tags must match. + * + * @param string[] $tags + * @param boolean $disjunction + * @return boolean + */ + public function clearByTags(array $tags, $disjunction = false) + { + if (!$tags) { + return true; + } + + $tagCount = count($tags); + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + + $flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_PATHNAME; + $path = $options->getCacheDir() + . str_repeat(\DIRECTORY_SEPARATOR . $prefix . '*', $options->getDirLevel()) + . \DIRECTORY_SEPARATOR . $prefix . '*.tag'; + $glob = new GlobIterator($path, $flags); + $time = time(); + $ttl = $options->getTtl(); + + foreach ($glob as $pathname) { + $diff = array_diff($tags, explode("\n", $this->getFileContent($pathname))); + + $rem = false; + if ($disjunction && count($diff) < $tagCount) { + $rem = true; + } elseif (!$disjunction && !$diff) { + $rem = true; + } + + if ($rem) { + unlink($pathname); + + $datPathname = substr($pathname, 0, -4) . '.dat'; + if (file_exists($datPathname)) { + unlink($datPathname); + } + } + } + + return true; + } + + /* IterableInterface */ + + /** + * Get the storage iterator + * + * @return FilesystemIterator + */ + public function getIterator() + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $path = $options->getCacheDir() + . str_repeat(\DIRECTORY_SEPARATOR . $prefix . '*', $options->getDirLevel()) + . \DIRECTORY_SEPARATOR . $prefix . '*.dat'; + return new FilesystemIterator($this, $path, $prefix); + } + + /* OptimizableInterface */ + + /** + * Optimize the storage + * + * @return boolean + * @return Exception\RuntimeException + */ + public function optimize() + { + $baseOptions = $this->getOptions(); + if ($baseOptions->getDirLevel()) { + // removes only empty directories + $this->rmDir( + $baseOptions->getCacheDir(), + $baseOptions->getNamespace() . $baseOptions->getNamespaceSeparator() + ); + } + return true; + } + + /* TotalSpaceCapableInterface */ + + /** + * Get total space in bytes + * + * @return int|float + */ + public function getTotalSpace() + { + if ($this->totalSpace !== null) { + $path = $this->getOptions()->getCacheDir(); + + ErrorHandler::start(); + $total = disk_total_space($path); + $error = ErrorHandler::stop(); + if ($total === false) { + throw new Exception\RuntimeException("Can't detect total space of '{$path}'", 0, $error); + } + + // clean total space buffer on change cache_dir + $events = $this->getEventManager(); + $handle = null; + $totalSpace = & $this->totalSpace; + $callback = function ($event) use (& $events, & $handle, & $totalSpace) { + $params = $event->getParams(); + if (isset($params['cache_dir'])) { + $totalSpace = null; + $events->detach($handle); + } + }; + $handle = $events->attach($callback); + } + + return $this->totalSpace; + } + + /* AvailableSpaceCapableInterface */ + + /** + * Get available space in bytes + * + * @return int|float + */ + public function getAvailableSpace() + { + $path = $this->getOptions()->getCacheDir(); + + ErrorHandler::start(); + $avail = disk_free_space($path); + $error = ErrorHandler::stop(); + if ($avail === false) { + throw new Exception\RuntimeException("Can't detect free space of '{$path}'", 0, $error); + } + + return $avail; + } + + /* reading */ + + /** + * Get an item. + * + * @param string $key + * @param boolean $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + * + * @triggers getItem.pre(PreEvent) + * @triggers getItem.post(PostEvent) + * @triggers getItem.exception(ExceptionEvent) + */ + public function getItem($key, & $success = null, & $casToken = null) + { + $options = $this->getOptions(); + if ($options->getReadable() && $options->getClearStatCache()) { + clearstatcache(); + } + + $argn = func_num_args(); + if ($argn > 2) { + return parent::getItem($key, $success, $casToken); + } elseif ($argn > 1) { + return parent::getItem($key, $success); + } else { + return parent::getItem($key); + } + } + + /** + * Get multiple items. + * + * @param array $keys + * @return array Associative array of keys and values + * @throws Exception\ExceptionInterface + * + * @triggers getItems.pre(PreEvent) + * @triggers getItems.post(PostEvent) + * @triggers getItems.exception(ExceptionEvent) + */ + public function getItems(array $keys) + { + $options = $this->getOptions(); + if ($options->getReadable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::getItems($keys); + } + + /** + * Internal method to get an item. + * + * @param string $normalizedKey + * @param boolean $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null) + { + if (!$this->internalHasItem($normalizedKey)) { + $success = false; + return null; + } + + try { + $filespec = $this->getFileSpec($normalizedKey); + $data = $this->getFileContent($filespec . '.dat'); + + // use filemtime + filesize as CAS token + if (func_num_args() > 2) { + $casToken = filemtime($filespec . '.dat') . filesize($filespec . '.dat'); + } + $success = true; + return $data; + + } catch (BaseException $e) { + $success = false; + throw $e; + } + } + + /** + * Internal method to get multiple items. + * + * @param array $normalizedKeys + * @return array Associative array of keys and values + * @throws Exception\ExceptionInterface + */ + protected function internalGetItems(array & $normalizedKeys) + { + $options = $this->getOptions(); + $keys = $normalizedKeys; // Don't change argument passed by reference + $result = array(); + while ($keys) { + + // LOCK_NB if more than one items have to read + $nonBlocking = count($keys) > 1; + $wouldblock = null; + + // read items + foreach ($keys as $i => $key) { + if (!$this->internalHasItem($key)) { + unset($keys[$i]); + continue; + } + + $filespec = $this->getFileSpec($key); + $data = $this->getFileContent($filespec . '.dat', $nonBlocking, $wouldblock); + if ($nonBlocking && $wouldblock) { + continue; + } else { + unset($keys[$i]); + } + + $result[$key] = $data; + } + + // TODO: Don't check ttl after first iteration + // $options['ttl'] = 0; + } + + return $result; + } + + /** + * Test if an item exists. + * + * @param string $key + * @return boolean + * @throws Exception\ExceptionInterface + * + * @triggers hasItem.pre(PreEvent) + * @triggers hasItem.post(PostEvent) + * @triggers hasItem.exception(ExceptionEvent) + */ + public function hasItem($key) + { + $options = $this->getOptions(); + if ($options->getReadable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::hasItem($key); + } + + /** + * Test multiple items. + * + * @param array $keys + * @return array Array of found keys + * @throws Exception\ExceptionInterface + * + * @triggers hasItems.pre(PreEvent) + * @triggers hasItems.post(PostEvent) + * @triggers hasItems.exception(ExceptionEvent) + */ + public function hasItems(array $keys) + { + $options = $this->getOptions(); + if ($options->getReadable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::hasItems($keys); + } + + /** + * Internal method to test if an item exists. + * + * @param string $normalizedKey + * @param array $normalizedOptions + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalHasItem(& $normalizedKey) + { + $file = $this->getFileSpec($normalizedKey) . '.dat'; + if (!file_exists($file)) { + return false; + } + + $ttl = $this->getOptions()->getTtl(); + if ($ttl) { + ErrorHandler::start(); + $mtime = filemtime($file); + $error = ErrorHandler::stop(); + if (!$mtime) { + throw new Exception\RuntimeException( + "Error getting mtime of file '{$file}'", 0, $error + ); + } + + if (time() >= ($mtime + $ttl)) { + return false; + } + } + + return true; + } + + /** + * Get metadata + * + * @param string $key + * @return array|boolean Metadata on success, false on failure + */ + public function getMetadata($key) + { + $options = $this->getOptions(); + if ($options->getReadable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::getMetadata($key); + } + + /** + * Get metadatas + * + * @param array $keys + * @return array Associative array of keys and metadata + */ + public function getMetadatas(array $keys, array $options = array()) + { + $options = $this->getOptions(); + if ($options->getReadable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::getMetadatas($keys); + } + + /** + * Get info by key + * + * @param string $normalizedKey + * @return array|boolean Metadata on success, false on failure + */ + protected function internalGetMetadata(& $normalizedKey) + { + if (!$this->internalHasItem($normalizedKey)) { + return false; + } + + $options = $this->getOptions(); + $filespec = $this->getFileSpec($normalizedKey); + $file = $filespec . '.dat'; + + $metadata = array( + 'filespec' => $filespec, + 'mtime' => filemtime($file) + ); + + if (!$options->getNoCtime()) { + $metadata['ctime'] = filectime($file); + } + + if (!$options->getNoAtime()) { + $metadata['atime'] = fileatime($file); + } + + return $metadata; + } + + /** + * Internal method to get multiple metadata + * + * @param array $normalizedKeys + * @return array Associative array of keys and metadata + * @throws Exception\ExceptionInterface + */ + protected function internalGetMetadatas(array & $normalizedKeys) + { + $options = $this->getOptions(); + $result = array(); + + foreach ($normalizedKeys as $normalizedKey) { + $filespec = $this->getFileSpec($normalizedKey); + $file = $filespec . '.dat'; + + $metadata = array( + 'filespec' => $filespec, + 'mtime' => filemtime($file), + ); + + if (!$options->getNoCtime()) { + $metadata['ctime'] = filectime($file); + } + + if (!$options->getNoAtime()) { + $metadata['atime'] = fileatime($file); + } + + $result[$normalizedKey] = $metadata; + } + + return $result; + } + + /* writing */ + + /** + * Store an item. + * + * @param string $key + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + * + * @triggers setItem.pre(PreEvent) + * @triggers setItem.post(PostEvent) + * @triggers setItem.exception(ExceptionEvent) + */ + public function setItem($key, $value) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + return parent::setItem($key, $value); + } + + /** + * Store multiple items. + * + * @param array $keyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + * + * @triggers setItems.pre(PreEvent) + * @triggers setItems.post(PostEvent) + * @triggers setItems.exception(ExceptionEvent) + */ + public function setItems(array $keyValuePairs) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::setItems($keyValuePairs); + } + + /** + * Add an item. + * + * @param string $key + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + * + * @triggers addItem.pre(PreEvent) + * @triggers addItem.post(PostEvent) + * @triggers addItem.exception(ExceptionEvent) + */ + public function addItem($key, $value) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::addItem($key, $value); + } + + /** + * Add multiple items. + * + * @param array $keyValuePairs + * @return boolean + * @throws Exception\ExceptionInterface + * + * @triggers addItems.pre(PreEvent) + * @triggers addItems.post(PostEvent) + * @triggers addItems.exception(ExceptionEvent) + */ + public function addItems(array $keyValuePairs) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::addItems($keyValuePairs); + } + + /** + * Replace an existing item. + * + * @param string $key + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + * + * @triggers replaceItem.pre(PreEvent) + * @triggers replaceItem.post(PostEvent) + * @triggers replaceItem.exception(ExceptionEvent) + */ + public function replaceItem($key, $value) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::replaceItem($key, $value); + } + + /** + * Replace multiple existing items. + * + * @param array $keyValuePairs + * @return boolean + * @throws Exception\ExceptionInterface + * + * @triggers replaceItems.pre(PreEvent) + * @triggers replaceItems.post(PostEvent) + * @triggers replaceItems.exception(ExceptionEvent) + */ + public function replaceItems(array $keyValuePairs) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::replaceItems($keyValuePairs); + } + + /** + * Internal method to store an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalSetItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $filespec = $this->getFileSpec($normalizedKey); + $this->prepareDirectoryStructure($filespec); + + $this->putFileContent($filespec . '.dat', $value); + $this->unlink($filespec . '.tag'); + + return true; + } + + /** + * Internal method to store multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalSetItems(array & $normalizedKeyValuePairs) + { + $baseOptions = $this->getOptions(); + $oldUmask = null; + + // create an associated array of files and contents to write + $contents = array(); + foreach ($normalizedKeyValuePairs as $key => & $value) { + $filespec = $this->getFileSpec($key); + $this->prepareDirectoryStructure($filespec); + + // *.dat file + $contents[$filespec . '.dat'] = & $value; + + // *.tag file + $this->unlink($filespec . '.tag'); + } + + // write to disk + while ($contents) { + $nonBlocking = count($contents) > 1; + $wouldblock = null; + + foreach ($contents as $file => & $content) { + $this->putFileContent($file, $content, $nonBlocking, $wouldblock); + if (!$nonBlocking || !$wouldblock) { + unset($contents[$file]); + } + } + } + + // return OK + return array(); + } + + /** + * Set an item only if token matches + * + * It uses the token received from getItem() to check if the item has + * changed before overwriting it. + * + * @param mixed $token + * @param string $key + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + * @see getItem() + * @see setItem() + */ + public function checkAndSetItem($token, $key, $value) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::checkAndSetItem($token, $key, $value); + } + + /** + * Internal method to set an item only if token matches + * + * @param mixed $token + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + * @see getItem() + * @see setItem() + */ + protected function internalCheckAndSetItem(& $token, & $normalizedKey, & $value) + { + if (!$this->internalHasItem($normalizedKey)) { + return false; + } + + // use filemtime + filesize as CAS token + $file = $this->getFileSpec($normalizedKey) . '.dat'; + $check = filemtime($file) . filesize($file); + if ($token !== $check) { + return false; + } + + return $this->internalSetItem($normalizedKey, $value); + } + + /** + * Reset lifetime of an item + * + * @param string $key + * @return boolean + * @throws Exception\ExceptionInterface + * + * @triggers touchItem.pre(PreEvent) + * @triggers touchItem.post(PostEvent) + * @triggers touchItem.exception(ExceptionEvent) + */ + public function touchItem($key) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::touchItem($key); + } + + /** + * Reset lifetime of multiple items. + * + * @param array $keys + * @return array Array of not updated keys + * @throws Exception\ExceptionInterface + * + * @triggers touchItems.pre(PreEvent) + * @triggers touchItems.post(PostEvent) + * @triggers touchItems.exception(ExceptionEvent) + */ + public function touchItems(array $keys) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::touchItems($keys); + } + + /** + * Internal method to reset lifetime of an item + * + * @param string $key + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalTouchItem(& $normalizedKey) + { + if (!$this->internalHasItem($normalizedKey)) { + return false; + } + + $filespec = $this->getFileSpec($normalizedKey); + + ErrorHandler::start(); + $touch = touch($filespec . '.dat'); + $error = ErrorHandler::stop(); + if (!$touch) { + throw new Exception\RuntimeException( + "Error touching file '{$filespec}.dat'", 0, $error + ); + } + + return true; + } + + /** + * Remove an item. + * + * @param string $key + * @return boolean + * @throws Exception\ExceptionInterface + * + * @triggers removeItem.pre(PreEvent) + * @triggers removeItem.post(PostEvent) + * @triggers removeItem.exception(ExceptionEvent) + */ + public function removeItem($key) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::removeItem($key); + } + + /** + * Remove multiple items. + * + * @param array $keys + * @return array Array of not removed keys + * @throws Exception\ExceptionInterface + * + * @triggers removeItems.pre(PreEvent) + * @triggers removeItems.post(PostEvent) + * @triggers removeItems.exception(ExceptionEvent) + */ + public function removeItems(array $keys) + { + $options = $this->getOptions(); + if ($options->getWritable() && $options->getClearStatCache()) { + clearstatcache(); + } + + return parent::removeItems($keys); + } + + /** + * Internal method to remove an item. + * + * @param string $normalizedKey + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItem(& $normalizedKey) + { + $filespec = $this->getFileSpec($normalizedKey); + if (!file_exists($filespec . '.dat')) { + return false; + } else { + $this->unlink($filespec . '.dat'); + $this->unlink($filespec . '.tag'); + } + return true; + } + + /* status */ + + /** + * Internal method to get capabilities of this adapter + * + * @return Capabilities + */ + protected function internalGetCapabilities() + { + if ($this->capabilities === null) { + $marker = new stdClass(); + $options = $this->getOptions(); + + // detect metadata + $metadata = array('mtime', 'filespec'); + if (!$options->getNoAtime()) { + $metadata[] = 'atime'; + } + if (!$options->getNoCtime()) { + $metadata[] = 'ctime'; + } + + $capabilities = new Capabilities( + $this, + $marker, + array( + 'supportedDatatypes' => array( + 'NULL' => 'string', + 'boolean' => 'string', + 'integer' => 'string', + 'double' => 'string', + 'string' => true, + 'array' => false, + 'object' => false, + 'resource' => false, + ), + 'supportedMetadata' => $metadata, + 'minTtl' => 1, + 'maxTtl' => 0, + 'staticTtl' => false, + 'ttlPrecision' => 1, + 'expiredRead' => true, + 'maxKeyLength' => 251, // 255 - strlen(.dat | .tag) + 'namespaceIsPrefix' => true, + 'namespaceSeparator' => $options->getNamespaceSeparator(), + ) + ); + + // update capabilities on change options + $this->getEventManager()->attach('option', function ($event) use ($capabilities, $marker) { + $params = $event->getParams(); + + if (isset($params['namespace_separator'])) { + $capabilities->setNamespaceSeparator($marker, $params['namespace_separator']); + } + + if (isset($params['no_atime']) || isset($params['no_ctime'])) { + $metadata = $capabilities->getSupportedMetadata(); + + if (isset($params['no_atime']) && !$params['no_atime']) { + $metadata[] = 'atime'; + } elseif (isset($params['no_atime']) && ($index = array_search('atime', $metadata)) !== false) { + unset($metadata[$index]); + } + + if (isset($params['no_ctime']) && !$params['no_ctime']) { + $metadata[] = 'ctime'; + } elseif (isset($params['no_ctime']) && ($index = array_search('ctime', $metadata)) !== false) { + unset($metadata[$index]); + } + + $capabilities->setSupportedMetadata($marker, $metadata); + } + }); + + $this->capabilityMarker = $marker; + $this->capabilities = $capabilities; + } + + return $this->capabilities; + } + + /* internal */ + + /** + * Removes directories recursive by namespace + * + * @param string $dir Directory to delete + * @param string $prefix Namespace + Separator + * @return bool + */ + protected function rmDir($dir, $prefix) + { + $glob = glob( + $dir . \DIRECTORY_SEPARATOR . $prefix . '*', + \GLOB_ONLYDIR | \GLOB_NOESCAPE | \GLOB_NOSORT + ); + if (!$glob) { + // On some systems glob returns false even on empty result + return true; + } + + $ret = true; + foreach ($glob as $subdir) { + // skip removing current directory if removing of sub-directory failed + if ($this->rmDir($subdir, $prefix)) { + // ignore not empty directories + ErrorHandler::start(); + $ret = rmdir($subdir) && $ret; + ErrorHandler::stop(); + } else { + $ret = false; + } + } + + return $ret; + } + + /** + * Get file spec of the given key and namespace + * + * @param string $normalizedKey + * @return string + */ + protected function getFileSpec($normalizedKey) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $path = $options->getCacheDir() . \DIRECTORY_SEPARATOR; + $level = $options->getDirLevel(); + + $fileSpecId = $path . $prefix . $normalizedKey . '/' . $level; + if ($this->lastFileSpecId !== $fileSpecId) { + if ($level > 0) { + // create up to 256 directories per directory level + $hash = md5($normalizedKey); + for ($i = 0, $max = ($level * 2); $i < $max; $i+= 2) { + $path .= $prefix . $hash[$i] . $hash[$i+1] . \DIRECTORY_SEPARATOR; + } + } + + $this->lastFileSpecId = $fileSpecId; + $this->lastFileSpec = $path . $prefix . $normalizedKey; + } + + return $this->lastFileSpec; + } + + /** + * Read info file + * + * @param string $file + * @param boolean $nonBlocking Don't block script if file is locked + * @param boolean $wouldblock The optional argument is set to TRUE if the lock would block + * @return array|boolean The info array or false if file wasn't found + * @throws Exception\RuntimeException + */ + protected function readInfoFile($file, $nonBlocking = false, & $wouldblock = null) + { + if (!file_exists($file)) { + return false; + } + + $content = $this->getFileContent($file, $nonBlocking, $wouldblock); + if ($nonBlocking && $wouldblock) { + return false; + } + + ErrorHandler::start(); + $ifo = unserialize($content); + $err = ErrorHandler::stop(); + if (!is_array($ifo)) { + throw new Exception\RuntimeException( + "Corrupted info file '{$file}'", 0, $err + ); + } + + return $ifo; + } + + /** + * Read a complete file + * + * @param string $file File complete path + * @param boolean $nonBlocking Don't block script if file is locked + * @param boolean $wouldblock The optional argument is set to TRUE if the lock would block + * @return string + * @throws Exception\RuntimeException + */ + protected function getFileContent($file, $nonBlocking = false, & $wouldblock = null) + { + $locking = $this->getOptions()->getFileLocking(); + $wouldblock = null; + + ErrorHandler::start(); + + // if file locking enabled -> file_get_contents can't be used + if ($locking) { + $fp = fopen($file, 'rb'); + if ($fp === false) { + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException( + "Error opening file '{$file}'", 0, $err + ); + } + + if ($nonBlocking) { + $lock = flock($fp, \LOCK_SH | \LOCK_NB, $wouldblock); + if ($wouldblock) { + fclose($fp); + ErrorHandler::stop(); + return; + } + } else { + $lock = flock($fp, \LOCK_SH); + } + + if (!$lock) { + fclose($fp); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException( + "Error locking file '{$file}'", 0, $err + ); + } + + $res = stream_get_contents($fp); + if ($res === false) { + flock($fp, \LOCK_UN); + fclose($fp); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException( + 'Error getting stream contents', 0, $err + ); + } + + flock($fp, \LOCK_UN); + fclose($fp); + + // if file locking disabled -> file_get_contents can be used + } else { + $res = file_get_contents($file, false); + if ($res === false) { + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException( + "Error getting file contents for file '{$file}'", 0, $err + ); + } + } + + ErrorHandler::stop(); + return $res; + } + + /** + * Prepares a directory structure for the given file(spec) + * using the configured directory level. + * + * @param string $file + * @return void + * @throws Exception\RuntimeException + */ + protected function prepareDirectoryStructure($file) + { + $options = $this->getOptions(); + $level = $options->getDirLevel(); + + // Directory structure is required only if directory level > 0 + if (!$level) { + return; + } + + // Directory structure already exists + $pathname = dirname($file); + if (file_exists($pathname)) { + return; + } + + $perm = $options->getDirPermission(); + $umask = $options->getUmask(); + if ($umask !== false && $perm !== false) { + $perm = $perm & ~$umask; + } + + ErrorHandler::start(); + + if ($perm === false || $level == 1) { + // build-in mkdir function is enough + + $umask = ($umask !== false) ? umask($umask) : false; + $res = mkdir($pathname, ($perm !== false) ? $perm : 0777, true); + + if ($umask !== false) { + umask($umask); + } + + if (!$res) { + $oct = ($perm === false) ? '777' : decoct($perm); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException( + "mkdir('{$pathname}', 0{$oct}, true) failed", 0, $err + ); + } + + if ($perm !== false && !chmod($pathname, $perm)) { + $oct = decoct($perm); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException( + "chmod('{$pathname}', 0{$oct}) failed", 0, $err + ); + } + + } else { + // build-in mkdir function sets permission together with current umask + // which doesn't work well on multo threaded webservers + // -> create directories one by one and set permissions + + // find existing path and missing path parts + $parts = array(); + $path = $pathname; + while (!file_exists($path)) { + array_unshift($parts, basename($path)); + $nextPath = dirname($path); + if ($nextPath === $path) { + break; + } + $path = $nextPath; + } + + // make all missing path parts + foreach ($parts as $part) { + $path.= \DIRECTORY_SEPARATOR . $part; + + // create a single directory, set and reset umask immediately + $umask = ($umask !== false) ? umask($umask) : false; + $res = mkdir($path, ($perm === false) ? 0777 : $perm, false); + if ($umask !== false) { + umask($umask); + } + + if (!$res) { + $oct = ($perm === false) ? '777' : decoct($perm); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException( + "mkdir('{$path}', 0{$oct}, false) failed" + ); + } + + if ($perm !== false && !chmod($path, $perm)) { + $oct = decoct($perm); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException( + "chmod('{$path}', 0{$oct}) failed" + ); + } + } + } + + ErrorHandler::stop(); + } + + /** + * Write content to a file + * + * @param string $file File complete path + * @param string $data Data to write + * @param boolean $nonBlocking Don't block script if file is locked + * @param boolean $wouldblock The optional argument is set to TRUE if the lock would block + * @return void + * @throws Exception\RuntimeException + */ + protected function putFileContent($file, $data, $nonBlocking = false, & $wouldblock = null) + { + $options = $this->getOptions(); + $locking = $options->getFileLocking(); + $nonBlocking = $locking && $nonBlocking; + $wouldblock = null; + + $umask = $options->getUmask(); + $perm = $options->getFilePermission(); + if ($umask !== false && $perm !== false) { + $perm = $perm & ~$umask; + } + + ErrorHandler::start(); + + // if locking and non blocking is enabled -> file_put_contents can't used + if ($locking && $nonBlocking) { + + $umask = ($umask !== false) ? umask($umask) : false; + + $fp = fopen($file, 'cb'); + + if ($umask) { + umask($umask); + } + + if (!$fp) { + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException( + "Error opening file '{$file}'", 0, $err + ); + } + + if ($perm !== false && !chmod($file, $perm)) { + fclose($fp); + $oct = decoct($perm); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException("chmod('{$file}', 0{$oct}) failed", 0, $err); + } + + if (!flock($fp, \LOCK_EX | \LOCK_NB, $wouldblock)) { + fclose($fp); + $err = ErrorHandler::stop(); + if ($wouldblock) { + return; + } else { + throw new Exception\RuntimeException("Error locking file '{$file}'", 0, $err); + } + } + + if (!fwrite($fp, $data)) { + flock($fp, \LOCK_UN); + fclose($fp); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException("Error writing file '{$file}'", 0, $err); + } + + if (!ftruncate($fp, strlen($data))) { + flock($fp, \LOCK_UN); + fclose($fp); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException("Error truncating file '{$file}'", 0, $err); + } + + flock($fp, \LOCK_UN); + fclose($fp); + + // else -> file_put_contents can be used + } else { + $flags = 0; + if ($locking) { + $flags = $flags | \LOCK_EX; + } + + $umask = ($umask !== false) ? umask($umask) : false; + + $rs = file_put_contents($file, $data, $flags); + + if ($umask) { + umask($umask); + } + + if ($rs === false) { + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException( + "Error writing file '{$file}'", 0, $err + ); + } + + if ($perm !== false && !chmod($file, $perm)) { + $oct = decoct($perm); + $err = ErrorHandler::stop(); + throw new Exception\RuntimeException("chmod('{$file}', 0{$oct}) failed", 0, $err); + } + } + + ErrorHandler::stop(); + } + + /** + * Unlink a file + * + * @param string $file + * @return void + * @throw RuntimeException + */ + protected function unlink($file) + { + ErrorHandler::start(); + $res = unlink($file); + $err = ErrorHandler::stop(); + + // only throw exception if file still exists after deleting + if (!$res && file_exists($file)) { + throw new Exception\RuntimeException( + "Error unlinking file '{$file}'; file still exists", 0, $err + ); + } + } +} diff --git a/src/Storage/Adapter/FilesystemIterator.php b/src/Storage/Adapter/FilesystemIterator.php new file mode 100644 index 000000000..142b680f2 --- /dev/null +++ b/src/Storage/Adapter/FilesystemIterator.php @@ -0,0 +1,179 @@ +storage = $storage; + $this->globIterator = new GlobIterator($path, GlobIterator::KEY_AS_FILENAME); + $this->prefix = $prefix; + $this->prefixLength = strlen($prefix); + } + + /** + * Get storage instance + * + * @return StorageInterface + */ + public function getStorage() + { + return $this->storage; + } + + /** + * Get iterator mode + * + * @return int Value of IteratorInterface::CURRENT_AS_* + */ + public function getMode() + { + return $this->mode; + } + + /** + * Set iterator mode + * + * @param int $mode + * @return ApcIterator Fluent interface + */ + public function setMode($mode) + { + $this->mode = (int) $mode; + return $this; + } + + /* Iterator */ + + /** + * Get current key, value or metadata. + * + * @return mixed + */ + public function current() + { + if ($this->mode == IteratorInterface::CURRENT_AS_SELF) { + return $this; + } + + $key = $this->key(); + + if ($this->mode == IteratorInterface::CURRENT_AS_VALUE) { + return $this->storage->getItem($key); + } elseif ($this->mode == IteratorInterface::CURRENT_AS_METADATA) { + return $this->storage->getMetadata($key); + } + + return $key; + } + + /** + * Get current key + * + * @return string + */ + public function key() + { + $filename = $this->globIterator->key(); + + // return without namespace prefix and file suffix + return substr($filename, $this->prefixLength, -4); + } + + /** + * Move forward to next element + * + * @return void + */ + public function next() + { + $this->globIterator->next(); + } + + /** + * Checks if current position is valid + * + * @return boolean + */ + public function valid() + { + try { + return $this->globIterator->valid(); + } catch (\LogicException $e) { + // @link https://bugs.php.net/bug.php?id=55701 + // GlobIterator throws LogicException with message + // 'The parent constructor was not called: the object is in an invalid state' + return false; + } + } + + /** + * Rewind the Iterator to the first element. + * + * @return void + */ + public function rewind() + { + return $this->globIterator->rewind(); + } +} diff --git a/src/Storage/Adapter/FilesystemOptions.php b/src/Storage/Adapter/FilesystemOptions.php new file mode 100644 index 000000000..1914561c8 --- /dev/null +++ b/src/Storage/Adapter/FilesystemOptions.php @@ -0,0 +1,462 @@ +filePermission = false; + $this->dirPermission = false; + } + + parent::__construct($options); + } + + /** + * Set cache dir + * + * @param string $cacheDir + * @return FilesystemOptions + * @throws Exception\InvalidArgumentException + */ + public function setCacheDir($cacheDir) + { + if ($cacheDir !== null) { + if (!is_dir($cacheDir)) { + throw new Exception\InvalidArgumentException( + "Cache directory '{$cacheDir}' not found or not a directory" + ); + } elseif (!is_writable($cacheDir)) { + throw new Exception\InvalidArgumentException( + "Cache directory '{$cacheDir}' not writable" + ); + } elseif (!is_readable($cacheDir)) { + throw new Exception\InvalidArgumentException( + "Cache directory '{$cacheDir}' not readable" + ); + } + + $cacheDir = rtrim(realpath($cacheDir), \DIRECTORY_SEPARATOR); + } else { + $cacheDir = sys_get_temp_dir(); + } + + $this->triggerOptionEvent('cache_dir', $cacheDir); + $this->cacheDir = $cacheDir; + return $this; + } + + /** + * Get cache dir + * + * @return null|string + */ + public function getCacheDir() + { + if ($this->cacheDir === null) { + $this->setCacheDir(null); + } + + return $this->cacheDir; + } + + /** + * Set clear stat cache + * + * @param bool $clearStatCache + * @return FilesystemOptions + */ + public function setClearStatCache($clearStatCache) + { + $clearStatCache = (bool) $clearStatCache; + $this->triggerOptionEvent('clear_stat_cache', $clearStatCache); + $this->clearStatCache = $clearStatCache; + return $this; + } + + /** + * Get clear stat cache + * + * @return bool + */ + public function getClearStatCache() + { + return $this->clearStatCache; + } + + /** + * Set dir level + * + * @param int $dirLevel + * @return FilesystemOptions + * @throws Exception\InvalidArgumentException + */ + public function setDirLevel($dirLevel) + { + $dirLevel = (int) $dirLevel; + if ($dirLevel < 0 || $dirLevel > 16) { + throw new Exception\InvalidArgumentException( + "Directory level '{$dirLevel}' must be between 0 and 16" + ); + } + $this->triggerOptionEvent('dir_level', $dirLevel); + $this->dirLevel = $dirLevel; + return $this; + } + + /** + * Get dir level + * + * @return int + */ + public function getDirLevel() + { + return $this->dirLevel; + } + + /** + * Set permission to create directories on unix systems + * + * @param false|string|int $dirPermission FALSE to disable explicit permission or an octal number + * @return FilesystemOptions + * @see setUmask + * @see setFilePermission + * @link http://php.net/manual/function.chmod.php + */ + public function setDirPermission($dirPermission) + { + if ($dirPermission !== false) { + if (is_string($dirPermission)) { + $dirPermission = octdec($dirPermission); + } else { + $dirPermission = (int) $dirPermission; + } + + // validate + if (($dirPermission & 0700) != 0700) { + throw new Exception\InvalidArgumentException( + 'Invalid directory permission: need permission to execute, read and write by owner' + ); + } + } + + if ($this->dirPermission !== $dirPermission) { + $this->triggerOptionEvent('dir_permission', $dirPermission); + $this->dirPermission = $dirPermission; + } + + return $this; + } + + /** + * Get permission to create directories on unix systems + * + * @return false|int + */ + public function getDirPermission() + { + return $this->dirPermission; + } + + /** + * Set file locking + * + * @param bool $fileLocking + * @return FilesystemOptions + */ + public function setFileLocking($fileLocking) + { + $fileLocking = (bool) $fileLocking; + $this->triggerOptionEvent('file_locking', $fileLocking); + $this->fileLocking = $fileLocking; + return $this; + } + + /** + * Get file locking + * + * @return bool + */ + public function getFileLocking() + { + return $this->fileLocking; + } + + /** + * Set permission to create files on unix systems + * + * @param false|string|int $filePermission FALSE to disable explicit permission or an octal number + * @return FilesystemOptions + * @see setUmask + * @see setDirPermission + * @link http://php.net/manual/function.chmod.php + */ + public function setFilePermission($filePermission) + { + if ($filePermission !== false) { + if (is_string($filePermission)) { + $filePermission = octdec($filePermission); + } else { + $filePermission = (int) $filePermission; + } + + // validate + if (($filePermission & 0600) != 0600) { + throw new Exception\InvalidArgumentException( + 'Invalid file permission: need permission to read and write by owner' + ); + } elseif ($filePermission & 0111) { + throw new Exception\InvalidArgumentException( + "Invalid file permission: Cache files shoudn't be executable" + ); + } + } + + if ($this->filePermission !== $filePermission) { + $this->triggerOptionEvent('file_permission', $filePermission); + $this->filePermission = $filePermission; + } + + return $this; + } + + /** + * Get permission to create files on unix systems + * + * @return false|int + */ + public function getFilePermission() + { + return $this->filePermission; + } + + /** + * Set namespace separator + * + * @param string $namespaceSeparator + * @return FilesystemOptions + */ + public function setNamespaceSeparator($namespaceSeparator) + { + $namespaceSeparator = (string) $namespaceSeparator; + $this->triggerOptionEvent('namespace_separator', $namespaceSeparator); + $this->namespaceSeparator = $namespaceSeparator; + return $this; + } + + /** + * Get namespace separator + * + * @return string + */ + public function getNamespaceSeparator() + { + return $this->namespaceSeparator; + } + + /** + * Set no atime + * + * @param bool $noAtime + * @return FilesystemOptions + */ + public function setNoAtime($noAtime) + { + $noAtime = (bool) $noAtime; + $this->triggerOptionEvent('no_atime', $noAtime); + $this->noAtime = $noAtime; + return $this; + } + + /** + * Get no atime + * + * @return bool + */ + public function getNoAtime() + { + return $this->noAtime; + } + + /** + * Set no ctime + * + * @param bool $noCtime + * @return FilesystemOptions + */ + public function setNoCtime($noCtime) + { + $noCtime = (bool) $noCtime; + $this->triggerOptionEvent('no_ctime', $noCtime); + $this->noCtime = $noCtime; + return $this; + } + + /** + * Get no ctime + * + * @return bool + */ + public function getNoCtime() + { + return $this->noCtime; + } + + /** + * Set the umask to create files and directories on unix systems + * + * Note: On multithreaded webservers it's better to explicit set file and dir permission. + * + * @param false|string|int $umask FALSE to disable umask or an octal number + * @return FilesystemOptions + * @see setFilePermission + * @see setDirPermission + * @link http://php.net/manual/function.umask.php + * @link http://en.wikipedia.org/wiki/Umask + */ + public function setUmask($umask) + { + if ($umask !== false) { + if (is_string($umask)) { + $umask = octdec($umask); + } else { + $umask = (int) $umask; + } + + // validate + if ($umask & 0700) { + throw new Exception\InvalidArgumentException( + 'Invalid umask: need permission to execute, read and write by owner' + ); + } + + // normalize + $umask = $umask & 0777; + } + + if ($this->umask !== $umask) { + $this->triggerOptionEvent('umask', $umask); + $this->umask = $umask; + } + + return $this; + } + + /** + * Get the umask to create files and directories on unix systems + * + * @return false|int + */ + public function getUmask() + { + return $this->umask; + } +} diff --git a/src/Storage/Adapter/KeyListIterator.php b/src/Storage/Adapter/KeyListIterator.php new file mode 100644 index 000000000..2239b0284 --- /dev/null +++ b/src/Storage/Adapter/KeyListIterator.php @@ -0,0 +1,176 @@ +storage = $storage; + $this->keys = $keys; + $this->count = count($keys); + } + + /** + * Get storage instance + * + * @return StorageInterface + */ + public function getStorage() + { + return $this->storage; + } + + /** + * Get iterator mode + * + * @return int Value of IteratorInterface::CURRENT_AS_* + */ + public function getMode() + { + return $this->mode; + } + + /** + * Set iterator mode + * + * @param int $mode + * @return KeyListIterator Fluent interface + */ + public function setMode($mode) + { + $this->mode = (int) $mode; + return $this; + } + + /** + * Get current key, value or metadata. + * + * @return mixed + */ + public function current() + { + if ($this->mode == IteratorInterface::CURRENT_AS_SELF) { + return $this; + } + + $key = $this->key(); + + if ($this->mode == IteratorInterface::CURRENT_AS_METADATA) { + return $this->storage->getMetadata($key); + } elseif ($this->mode == IteratorInterface::CURRENT_AS_VALUE) { + return $this->storage->getItem($key); + } + + return $key; + } + + /** + * Get current key + * + * @return string + */ + public function key() + { + return $this->keys[$this->position]; + } + + /** + * Checks if current position is valid + * + * @return boolean + */ + public function valid() + { + return $this->position < $this->count; + } + + /** + * Move forward to next element + * + * @return void + */ + public function next() + { + $this->position++; + } + + /** + * Rewind the Iterator to the first element. + * + * @return void + */ + public function rewind() + { + $this->position = 0; + } + + /** + * Count number of items + * + * @return int + */ + public function count() + { + return $this->count(); + } +} diff --git a/src/Storage/Adapter/Memcached.php b/src/Storage/Adapter/Memcached.php new file mode 100644 index 000000000..cbd77d8c8 --- /dev/null +++ b/src/Storage/Adapter/Memcached.php @@ -0,0 +1,618 @@ += 1.0.0'); + } + + parent::__construct($options); + + // It's ok to init the memcached instance as soon as possible because + // ext/memcached auto-connects to the server on first use + $this->memcached = new MemcachedResource(); + $options = $this->getOptions(); + + // set lib options + if (static::$extMemcachedMajorVersion > 1) { + $this->memcached->setOptions($options->getLibOptions()); + } else { + foreach ($options->getLibOptions() as $k => $v) { + $this->memcached->setOption($k, $v); + } + } + $this->memcached->setOption(MemcachedResource::OPT_PREFIX_KEY, $options->getNamespace()); + + $servers = $options->getServers(); + if (!$servers) { + $options->addServer('127.0.0.1', 11211); + $servers = $options->getServers(); + } + $this->memcached->addServers($servers); + + + + // get notified on change options + $memc = $this->memcached; + $memcMV = static::$extMemcachedMajorVersion; + $this->getEventManager()->attach('option', function ($event) use ($memc, $memcMV) { + $params = $event->getParams(); + + if (isset($params['lib_options'])) { + if ($memcMV > 1) { + $memc->setOptions($params['lib_options']); + } else { + foreach ($params['lib_options'] as $k => $v) { + $memc->setOption($k, $v); + } + } + } + + if (isset($params['namespace'])) { + $memc->setOption(MemcachedResource::OPT_PREFIX_KEY, $params['namespace']); + } + + // TODO: update on change/add server(s) + }); + } + + /* options */ + + /** + * Set options. + * + * @param array|Traversable|MemcachedOptions $options + * @return Memcached + * @see getOptions() + */ + public function setOptions($options) + { + if (!$options instanceof MemcachedOptions) { + $options = new MemcachedOptions($options); + } + + return parent::setOptions($options); + } + + /** + * Get options. + * + * @return MemcachedOptions + * @see setOptions() + */ + public function getOptions() + { + if (!$this->options) { + $this->setOptions(new MemcachedOptions()); + } + return $this->options; + } + + /* FlushableInterface */ + + /** + * Flush the whole storage + * + * @return boolean + */ + public function flush() + { + if (!$this->memcached->flush()) { + throw $this->getExceptionByResultCode($this->memcached->getResultCode()); + } + return true; + } + + /* TotalSpaceCapableInterface */ + + /** + * Get total space in bytes + * + * @return int|float + */ + public function getTotalSpace() + { + $stats = $this->memcached->getStats(); + if ($stats === false) { + throw new Exception\RuntimeException($this->memcached->getResultMessage()); + } + + $mem = array_pop($stats); + return $mem['limit_maxbytes']; + } + + /* AvailableSpaceCapableInterface */ + + /** + * Get available space in bytes + * + * @return int|float + */ + public function getAvailableSpace() + { + $stats = $this->memcached->getStats(); + if ($stats === false) { + throw new Exception\RuntimeException($this->memcached->getResultMessage()); + } + + $mem = array_pop($stats); + return $mem['limit_maxbytes'] - $mem['bytes']; + } + + /* reading */ + + /** + * Internal method to get an item. + * + * @param string $normalizedKey + * @param boolean $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null) + { + if (func_num_args() > 2) { + $result = $this->memcached->get($normalizedKey, null, $casToken); + } else { + $result = $this->memcached->get($normalizedKey); + } + + $success = true; + if ($result === false || $result === null) { + $rsCode = $this->memcached->getResultCode(); + if ($rsCode == MemcachedResource::RES_NOTFOUND) { + $result = null; + $success = false; + } elseif ($rsCode) { + $success = false; + throw $this->getExceptionByResultCode($rsCode); + } + } + + return $result; + } + + /** + * Internal method to get multiple items. + * + * @param array $normalizedKeys + * @return array Associative array of keys and values + * @throws Exception\ExceptionInterface + */ + protected function internalGetItems(array & $normalizedKeys) + { + $result = $this->memcached->getMulti($normalizedKeys); + if ($result === false) { + throw $this->getExceptionByResultCode($this->memcached->getResultCode()); + } + + return $result; + } + + /** + * Internal method to test if an item exists. + * + * @param string $normalizedKey + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalHasItem(& $normalizedKey) + { + $value = $this->memcached->get($normalizedKey); + if ($value === false || $value === null) { + $rsCode = $this->memcached->getResultCode(); + if ($rsCode == MemcachedResource::RES_SUCCESS) { + return true; + } elseif ($rsCode == MemcachedResource::RES_NOTFOUND) { + return false; + } else { + throw $this->getExceptionByResultCode($rsCode); + } + } + + return true; + } + + /** + * Internal method to test multiple items. + * + * @param array $normalizedKeys + * @return array Array of found keys + * @throws Exception\ExceptionInterface + */ + protected function internalHasItems(array & $normalizedKeys) + { + $result = $this->memcached->getMulti($normalizedKeys); + if ($result === false) { + throw $this->getExceptionByResultCode($this->memcached->getResultCode()); + } + + return array_keys($result); + } + + /** + * Get metadata of multiple items + * + * @param array $normalizedKeys + * @return array Associative array of keys and metadata + * @throws Exception\ExceptionInterface + */ + protected function internalGetMetadatas(array & $normalizedKeys) + { + $result = $this->memcached->getMulti($normalizedKeys); + if ($result === false) { + throw $this->getExceptionByResultCode($this->memcached->getResultCode()); + } + + foreach ($result as $key => & $value) { + $value = array(); + } + + return $result; + } + + /* writing */ + + /** + * Internal method to store an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalSetItem(& $normalizedKey, & $value) + { + $expiration = $this->expirationTime(); + if (!$this->memcached->set($normalizedKey, $value, $expiration)) { + throw $this->getExceptionByResultCode($this->memcached->getResultCode()); + } + + return true; + } + + /** + * Internal method to store multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalSetItems(array & $normalizedKeyValuePairs) + { + $expiration = $this->expirationTime(); + if (!$this->memcached->setMulti($normalizedKeyValuePairs, $expiration)) { + throw $this->getExceptionByResultCode($this->memcached->getResultCode()); + } + + return array(); + } + + /** + * Add an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalAddItem(& $normalizedKey, & $value) + { + $expiration = $this->expirationTime(); + if (!$this->memcached->add($normalizedKey, $value, $expiration)) { + if ($this->memcached->getResultCode() == MemcachedResource::RES_NOTSTORED) { + return false; + } + throw $this->getExceptionByResultCode($this->memcached->getResultCode()); + } + + return true; + } + + /** + * Internal method to replace an existing item. + * + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalReplaceItem(& $normalizedKey, & $value) + { + $expiration = $this->expirationTime(); + if (!$this->memcached->replace($normalizedKey, $value, $expiration)) { + if ($this->memcached->getResultCode() == MemcachedResource::RES_NOTSTORED) { + return false; + } + throw $this->getExceptionByResultCode($this->memcached->getResultCode()); + } + + return true; + } + + /** + * Internal method to set an item only if token matches + * + * @param mixed $token + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + * @see getItem() + * @see setItem() + */ + protected function internalCheckAndSetItem(& $token, & $normalizedKey, & $value) + { + $expiration = $this->expirationTime(); + $result = $this->memcached->cas($token, $normalizedKey, $value, $expiration); + + if ($result === false) { + $rsCode = $this->memcached->getResultCode(); + if ($rsCode !== 0 && $rsCode != MemcachedResource::RES_DATA_EXISTS) { + throw $this->getExceptionByResultCode($rsCode); + } + } + + + return $result; + } + + /** + * Internal method to remove an item. + * + * @param string $normalizedKey + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItem(& $normalizedKey) + { + $result = $this->memcached->delete($normalizedKey); + + if ($result === false) { + $rsCode = $this->memcached->getResultCode(); + if ($rsCode == MemcachedResource::RES_NOTFOUND) { + return false; + } elseif ($rsCode != MemcachedResource::RES_SUCCESS) { + throw $this->getExceptionByResultCode($rsCode); + } + } + + return true; + } + + /** + * Internal method to remove multiple items. + * + * @param array $normalizedKeys + * @return array Array of not removed keys + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItems(array & $normalizedKeys) + { + // support for removing multiple items at once has been added in ext/memcached-2.0.0 + if (static::$extMemcachedMajorVersion < 2) { + return parent::internalRemoveItems($normalizedKeys); + } + + $rsCodes = $this->memcached->deleteMulti($normalizedKeys); + + $missingKeys = array(); + foreach ($rsCodes as $key => $rsCode) { + if ($rsCode !== true && $rsCode != MemcachedResource::RES_SUCCESS) { + if ($rsCode != MemcachedResource::RES_NOTFOUND) { + throw $this->getExceptionByResultCode($rsCode); + } + $missingKeys[] = $key; + } + } + + return $missingKeys; + } + + /** + * Internal method to increment an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|boolean The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalIncrementItem(& $normalizedKey, & $value) + { + $value = (int)$value; + $newValue = $this->memcached->increment($normalizedKey, $value); + + if ($newValue === false) { + $rsCode = $this->memcached->getResultCode(); + + // initial value + if ($rsCode == MemcachedResource::RES_NOTFOUND) { + $newValue = $value; + $this->memcached->add($normalizedKey, $newValue, $this->expirationTime()); + $rsCode = $this->memcached->getResultCode(); + } + + if ($rsCode) { + throw $this->getExceptionByResultCode($rsCode); + } + } + + return $newValue; + } + + /** + * Internal method to decrement an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|boolean The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalDecrementItem(& $normalizedKey, & $value) + { + $value = (int)$value; + $newValue = $this->memcached->decrement($normalizedKey, $value); + + if ($newValue === false) { + $rsCode = $this->memcached->getResultCode(); + + // initial value + if ($rsCode == MemcachedResource::RES_NOTFOUND) { + $newValue = -$value; + $this->memcached->add($normalizedKey, $newValue, $this->expirationTime()); + $rsCode = $this->memcached->getResultCode(); + } + + if ($rsCode) { + throw $this->getExceptionByResultCode($rsCode); + } + } + + return $newValue; + } + + /* status */ + + /** + * Internal method to get capabilities of this adapter + * + * @return Capabilities + */ + protected function internalGetCapabilities() + { + if ($this->capabilities === null) { + $this->capabilityMarker = new stdClass(); + $this->capabilities = new Capabilities( + $this, + $this->capabilityMarker, + array( + 'supportedDatatypes' => array( + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => true, + 'object' => 'object', + 'resource' => false, + ), + 'supportedMetadata' => array(), + 'minTtl' => 1, + 'maxTtl' => 0, + 'staticTtl' => true, + 'ttlPrecision' => 1, + 'useRequestTime' => false, + 'expiredRead' => false, + 'maxKeyLength' => 255, + 'namespaceIsPrefix' => true, + ) + ); + } + + return $this->capabilities; + } + + /* internal */ + + /** + * Get expiration time by ttl + * + * Some storage commands involve sending an expiration value (relative to + * an item or to an operation requested by the client) to the server. In + * all such cases, the actual value sent may either be Unix time (number of + * seconds since January 1, 1970, as an integer), or a number of seconds + * starting from current time. In the latter case, this number of seconds + * may not exceed 60*60*24*30 (number of seconds in 30 days); if the + * expiration value is larger than that, the server will consider it to be + * real Unix time value rather than an offset from current time. + * + * @return int + */ + protected function expirationTime() + { + $ttl = $this->getOptions()->getTtl(); + if ($ttl > 2592000) { + return time() + $ttl; + } + return $ttl; + } + + /** + * Generate exception based of memcached result code + * + * @param int $code + * @return Exception\RuntimeException + * @throws Exception\InvalidArgumentException On success code + */ + protected function getExceptionByResultCode($code) + { + switch ($code) { + case MemcachedResource::RES_SUCCESS: + throw new Exception\InvalidArgumentException( + "The result code '{$code}' (SUCCESS) isn't an error" + ); + + default: + return new Exception\RuntimeException($this->memcached->getResultMessage()); + } + } +} diff --git a/src/Storage/Adapter/MemcachedOptions.php b/src/Storage/Adapter/MemcachedOptions.php new file mode 100644 index 000000000..89c627786 --- /dev/null +++ b/src/Storage/Adapter/MemcachedOptions.php @@ -0,0 +1,210 @@ + Hostname::ALLOW_ALL)); + if (!$hostNameValidator->isValid($host)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects a valid hostname: %s', + __METHOD__, + implode("\n", $hostNameValidator->getMessages()) + )); + } + + if (!is_numeric($port) || $port <= 0) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects a positive integer', __METHOD__ + )); + } + + $this->servers[] = array($host, $port); + return $this; + } + + /** + * Set Servers + * + * @param array $servers list of servers in [] = array($host, $port) + * @return MemcachedOptions + * @throws Exception\InvalidArgumentException + */ + public function setServers(array $servers) + { + foreach ($servers as $server) { + if (!isset($server[0])) { + throw new Exception\InvalidArgumentException('The servers array must contain a host value.'); + } + + if (!isset($server[1])) { + $this->addServer($server[0]); + } else { + $this->addServer($server[0], $server[1]); + } + } + + return $this; + } + + /** + * Get Servers + * + * @return array + */ + public function getServers() + { + return $this->servers; + } + + /** + * Set libmemcached options + * + * @param array $libOptions + * @return MemcachedOptions + * @link http://php.net/manual/memcached.constants.php + */ + public function setLibOptions(array $libOptions) + { + $normalizedOptions = array(); + foreach ($libOptions as $key => $value) { + $this->normalizeLibOptionKey($key); + $normalizedOptions[$key] = $value; + } + + $this->triggerOptionEvent('lib_options', $normalizedOptions); + $this->libOptions = array_merge($this->libOptions, $normalizedOptions); + + return $this; + } + + /** + * Set libmemcached option + * + * @param string|int $key + * @param mixed $value + * @return MemcachedOptions + * @link http://php.net/manual/memcached.constants.php + */ + public function setLibOption($key, $value) + { + $this->normalizeLibOptionKey($key); + $this->triggerOptionEvent('lib_options', array($key, $value)); + $this->libOptions[$key] = $value; + + return $this; + } + + /** + * Get libmemcached options + * + * @return array + * @link http://php.net/manual/memcached.constants.php + */ + public function getLibOptions() + { + return $this->libOptions; + } + + /** + * Get libmemcached option + * + * @return mixed + * @link http://php.net/manual/memcached.constants.php + */ + public function getLibOption($key) + { + $this->normalizeLibOptionKey($key); + if (isset($this->libOptions[$key])) { + return $this->libOptions[$key]; + } + return null; + } + + /** + * Normalize libmemcached option name into it's constant value + * + * @param string|int $key + * @throws Exception\InvalidArgumentException + */ + protected function normalizeLibOptionKey(& $key) + { + if (is_string($key)) { + $const = 'Memcached::OPT_' . str_replace(array(' ', '-'), '_', strtoupper($key)); + if (!defined($const)) { + throw new Exception\InvalidArgumentException("Unknown libmemcached option '{$key}' ({$const})"); + } + $key = constant($const); + } else { + $key = (int) $key; + } + } + +} diff --git a/src/Storage/Adapter/Memory.php b/src/Storage/Adapter/Memory.php new file mode 100644 index 000000000..a779f735e --- /dev/null +++ b/src/Storage/Adapter/Memory.php @@ -0,0 +1,747 @@ + => array( + * => array( + * 0 => + * 1 => + * ['tags' => ] + * ) + * ) + * ) + * + * @var array + */ + protected $data = array(); + + /** + * Set options. + * + * @param array|\Traversable|MemoryOptions $options + * @return Memory + * @see getOptions() + */ + public function setOptions($options) + { + if (!$options instanceof MemoryOptions) { + $options = new MemoryOptions($options); + } + + return parent::setOptions($options); + } + + /** + * Get options. + * + * @return MemoryOptions + * @see setOptions() + */ + public function getOptions() + { + if (!$this->options) { + $this->setOptions(new MemoryOptions()); + } + return $this->options; + } + + /* TotalSpaceCapableInterface */ + + /** + * Get total space in bytes + * + * @return int|float + */ + public function getTotalSpace() + { + return $this->getOptions()->getMemoryLimit(); + } + + /* AvailableSpaceCapableInterface */ + + /** + * Get available space in bytes + * + * @return int|float + */ + public function getAvailableSpace() + { + $total = $this->getOptions()->getMemoryLimit(); + $avail = $total - (float) memory_get_usage(true); + return ($avail > 0) ? $avail : 0; + } + + /* IterableInterface */ + + /** + * Get the storage iterator + * + * @return MemoryIterator + */ + public function getIterator() + { + $ns = $this->getOptions()->getNamespace(); + $keys = array(); + + if (isset($this->data[$ns])) { + foreach ($this->data[$ns] as $key => & $tmp) { + if ($this->internalHasItem($key)) { + $keys[] = $key; + } + } + } + + return new KeyListIterator($this, $keys); + } + + /* FlushableInterface */ + + /** + * Flush the whole storage + * + * @return boolean + */ + public function flush() + { + $this->data = array(); + return true; + } + + /* ClearExpiredInterface */ + + /** + * Remove expired items + * + * @return boolean + */ + public function clearExpired() + { + $ttl = $this->getOptions()->getTtl(); + if ($ttl <= 0) { + return true; + } + + $ns = $this->getOptions()->getNamespace(); + if (!isset($this->data[$ns])) { + return true; + } + + $data = & $this->data[$ns]; + foreach ($data as $key => & $item) { + if (microtime(true) >= $data[$key][1] + $ttl) { + unset($data[$key]); + } + } + + return true; + } + + /* ClearByPrefixInterface */ + + /** + * Remove items matching given prefix + * + * @param string $prefix + * @return boolean + */ + public function clearByPrefix($prefix) + { + $ns = $this->getOptions()->getNamespace(); + if (!isset($this->data[$ns])) { + return true; + } + + $prefixL = strlen($prefix); + $data = & $this->data[$ns]; + foreach ($data as $key => & $item) { + if (substr($key, 0, $prefixL) === $prefix) { + unset($data[$key]); + } + } + + return true; + } + + /* TaggableInterface */ + + /** + * Set tags to an item by given key. + * An empty array will remove all tags. + * + * @param string $key + * @param string[] $tags + * @return boolean + */ + public function setTags($key, array $tags) + { + $ns = $this->getOptions()->getNamespace(); + if (!$this->data[$ns]) { + return false; + } + + $data = & $this->data[$ns]; + if (isset($data[$key])) { + $data[$key]['tags'] = $tags; + return true; + } + + return false; + } + + /** + * Get tags of an item by given key + * + * @param string $key + * @return string[]|FALSE + */ + public function getTags($key) + { + $ns = $this->getOptions()->getNamespace(); + if (!$this->data[$ns]) { + return false; + } + + $data = & $this->data[$ns]; + if (!isset($data[$key])) { + return false; + } + + return isset($data[$key]['tags']) ? $data[$key]['tags'] : array(); + } + + /** + * Remove items matching given tags. + * + * If $disjunction only one of the given tags must match + * else all given tags must match. + * + * @param string[] $tags + * @param boolean $disjunction + * @return boolean + */ + public function clearByTags(array $tags, $disjunction = false) + { + $ns = $this->getOptions()->getNamespace(); + if (!$this->data[$ns]) { + return true; + } + + $tagCount = count($tags); + $data = & $this->data[$ns]; + foreach ($data as $key => & $item) { + if (isset($item['tags'])) { + $diff = array_diff($tags, $item['tags']); + if (($disjunction && count($diff) < $tagCount) || (!$disjunction && !$diff)) { + unset($data[$key]); + } + } + } + + return true; + } + + /* reading */ + + /** + * Internal method to get an item. + * + * @param string $normalizedKey + * @param boolean $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null) + { + $options = $this->getOptions(); + $ns = $options->getNamespace(); + $success = isset($this->data[$ns][$normalizedKey]); + if ($success) { + $data = & $this->data[$ns][$normalizedKey]; + $ttl = $options->getTtl(); + if ($ttl && microtime(true) >= ($data[1] + $ttl) ) { + $success = false; + } + } + + if (!$success) { + return null; + } + + $casToken = $data[0]; + return $data[0]; + } + + /** + * Internal method to get multiple items. + * + * @param array $normalizedKeys + * @return array Associative array of keys and values + * @throws Exception\ExceptionInterface + */ + protected function internalGetItems(array & $normalizedKeys) + { + $options = $this->getOptions(); + $ns = $options->getNamespace(); + if (!isset($this->data[$ns])) { + return array(); + } + + $data = & $this->data[$ns]; + $ttl = $options->getTtl(); + $now = microtime(true); + + $result = array(); + foreach ($normalizedKeys as $normalizedKey) { + if (isset($data[$normalizedKey])) { + if (!$ttl || $now < ($data[$normalizedKey][1] + $ttl) ) { + $result[$normalizedKey] = $data[$normalizedKey][0]; + } + } + } + + return $result; + } + + /** + * Internal method to test if an item exists. + * + * @param string $normalizedKey + * @param array $normalizedOptions + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalHasItem(& $normalizedKey) + { + $options = $this->getOptions(); + $ns = $options->getNamespace(); + if (!isset($this->data[$ns][$normalizedKey])) { + return false; + } + + // check if expired + $ttl = $options->getTtl(); + if ($ttl && microtime(true) >= ($this->data[$ns][$normalizedKey][1] + $ttl) ) { + return false; + } + + return true; + } + + /** + * Internal method to test multiple items. + * + * @param array $keys + * @return array Array of found keys + * @throws Exception\ExceptionInterface + */ + protected function internalHasItems(array & $normalizedKeys) + { + $options = $this->getOptions(); + $ns = $options->getNamespace(); + if (!isset($this->data[$ns])) { + return array(); + } + + $data = & $this->data[$ns]; + $ttl = $options->getTtl(); + $now = microtime(true); + + $result = array(); + foreach ($normalizedKeys as $normalizedKey) { + if (isset($data[$normalizedKey])) { + if (!$ttl || $now < ($data[$normalizedKey][1] + $ttl) ) { + $result[] = $normalizedKey; + } + } + } + + return $result; + } + + /** + * Get metadata of an item. + * + * @param string $normalizedKey + * @return array|boolean Metadata on success, false on failure + * @throws Exception\ExceptionInterface + * + * @triggers getMetadata.pre(PreEvent) + * @triggers getMetadata.post(PostEvent) + * @triggers getMetadata.exception(ExceptionEvent) + */ + protected function internalGetMetadata(& $normalizedKey) + { + if (!$this->internalHasItem($normalizedKey)) { + return false; + } + + $ns = $this->getOptions()->getNamespace(); + return array( + 'mtime' => $this->data[$ns][$normalizedKey][1], + ); + } + + /* writing */ + + /** + * Internal method to store an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalSetItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + + if (!$this->hasAvailableSpace()) { + $memoryLimit = $options->getMemoryLimit(); + throw new Exception\OutOfSpaceException( + "Memory usage exceeds limit ({$memoryLimit})." + ); + } + + $ns = $options->getNamespace(); + $this->data[$ns][$normalizedKey] = array($value, microtime(true)); + + return true; + } + + /** + * Internal method to store multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalSetItems(array & $normalizedKeyValuePairs) + { + $options = $this->getOptions(); + + if (!$this->hasAvailableSpace()) { + $memoryLimit = $options->getMemoryLimit(); + throw new Exception\OutOfSpaceException( + "Memory usage exceeds limit ({$memoryLimit})." + ); + } + + $ns = $options->getNamespace(); + if (!isset($this->data[$ns])) { + $this->data[$ns] = array(); + } + + $data = & $this->data[$ns]; + $now = microtime(true); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + $data[$normalizedKey] = array($value, $now); + } + + return array(); + } + + /** + * Add an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalAddItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + + if (!$this->hasAvailableSpace()) { + $memoryLimit = $options->getMemoryLimit(); + throw new Exception\OutOfSpaceException( + "Memory usage exceeds limit ({$memoryLimit})." + ); + } + + $ns = $options->getNamespace(); + if (isset($this->data[$ns][$normalizedKey])) { + return false; + } + + $this->data[$ns][$normalizedKey] = array($value, microtime(true)); + return true; + } + + /** + * Internal method to add multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalAddItems(array & $normalizedKeyValuePairs) + { + $options = $this->getOptions(); + + if (!$this->hasAvailableSpace()) { + $memoryLimit = $options->getMemoryLimit(); + throw new Exception\OutOfSpaceException( + "Memory usage exceeds limit ({$memoryLimit})." + ); + } + + $ns = $options->getNamespace(); + if (!isset($this->data[$ns])) { + $this->data[$ns] = array(); + } + + $result = array(); + $data = & $this->data[$ns]; + $now = microtime(true); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + if (isset($data[$normalizedKey])) { + $result[] = $normalizedKey; + } else { + $data[$normalizedKey] = array($value, $now); + } + } + + return $result; + } + + /** + * Internal method to replace an existing item. + * + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalReplaceItem(& $normalizedKey, & $value) + { + $ns = $this->getOptions()->getNamespace(); + if (!isset($this->data[$ns][$normalizedKey])) { + return false; + } + $this->data[$ns][$normalizedKey] = array($value, microtime(true)); + + return true; + } + + /** + * Internal method to replace multiple existing items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalReplaceItems(array & $normalizedKeyValuePairs) + { + $ns = $this->getOptions()->getNamespace(); + if (!isset($this->data[$ns])) { + return array_keys($normalizedKeyValuePairs); + } + + $result = array(); + $data = & $this->data[$ns]; + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + if (!isset($data[$normalizedKey])) { + $result[] = $normalizedKey; + } else { + $data[$normalizedKey] = array($value, microtime(true)); + } + } + + return $result; + } + + /** + * Internal method to reset lifetime of an item + * + * @param string $normalizedKey + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalTouchItem(& $normalizedKey) + { + $ns = $this->getOptions()->getNamespace(); + + if (!isset($this->data[$ns][$normalizedKey])) { + return false; + } + + $this->data[$ns][$normalizedKey][1] = microtime(true); + return true; + } + + /** + * Internal method to remove an item. + * + * @param string $normalizedKey + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItem(& $normalizedKey) + { + $ns = $this->getOptions()->getNamespace(); + if (!isset($this->data[$ns][$normalizedKey])) { + return false; + } + + unset($this->data[$ns][$normalizedKey]); + + // remove empty namespace + if (!$this->data[$ns]) { + unset($this->data[$ns]); + } + + return true; + } + + /** + * Internal method to increment an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|boolean The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalIncrementItem(& $normalizedKey, & $value) + { + $ns = $this->getOptions()->getNamespace(); + $data = & $this->data[$ns]; + if (isset($data[$normalizedKey])) { + $data[$normalizedKey][0]+= $value; + $data[$normalizedKey][1] = microtime(true); + $newValue = $data[$normalizedKey][0]; + } else { + // initial value + $newValue = $value; + $data[$normalizedKey] = array($newValue, microtime(true)); + } + + return $newValue; + } + + /** + * Internal method to decrement an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|boolean The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalDecrementItem(& $normalizedKey, & $value) + { + $ns = $this->getOptions()->getNamespace(); + $data = & $this->data[$ns]; + if (isset($data[$normalizedKey])) { + $data[$normalizedKey][0]-= $value; + $data[$normalizedKey][1] = microtime(true); + $newValue = $data[$normalizedKey][0]; + } else { + // initial value + $newValue = -$value; + $data[$normalizedKey] = array($newValue, microtime(true)); + } + + return $newValue; + } + + /* status */ + + /** + * Internal method to get capabilities of this adapter + * + * @return Capabilities + */ + protected function internalGetCapabilities() + { + if ($this->capabilities === null) { + $this->capabilityMarker = new stdClass(); + $this->capabilities = new Capabilities( + $this, + $this->capabilityMarker, + array( + 'supportedDatatypes' => array( + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => true, + 'object' => true, + 'resource' => true, + ), + 'supportedMetadata' => array('mtime'), + 'minTtl' => 1, + 'maxTtl' => PHP_INT_MAX, + 'staticTtl' => false, + 'ttlPrecision' => 0.05, + 'expiredRead' => true, + 'maxKeyLength' => 0, + 'namespaceIsPrefix' => false, + 'namespaceSeparator' => '', + ) + ); + } + + return $this->capabilities; + } + + /* internal */ + + /** + * Has space available to store items? + * + * @return boolean + */ + protected function hasAvailableSpace() + { + $total = $this->getOptions()->getMemoryLimit(); + + // check memory limit disabled + if ($total <= 0) { + return true; + } + + $free = $total - (float) memory_get_usage(true); + return ($free > 0); + } +} diff --git a/src/Storage/Adapter/MemoryOptions.php b/src/Storage/Adapter/MemoryOptions.php new file mode 100644 index 000000000..ce871fc12 --- /dev/null +++ b/src/Storage/Adapter/MemoryOptions.php @@ -0,0 +1,117 @@ +normalizeMemoryLimit($memoryLimit); + + if ($this->memoryLimit != $memoryLimit) { + $this->triggerOptionEvent('memory_limit', $memoryLimit); + $this->memoryLimit = $memoryLimit; + } + + return $this; + } + + /** + * Get memory limit + * + * If the used memory of PHP exceeds this limit an OutOfSpaceException + * will be thrown. + * + * @return int + */ + public function getMemoryLimit() + { + if ($this->memoryLimit === null) { + // By default use half of PHP's memory limit if possible + $memoryLimit = $this->normalizeMemoryLimit(ini_get('memory_limit')); + if ($memoryLimit >= 0) { + $this->memoryLimit = (int)($memoryLimit / 2); + } else { + // disable memory limit + $this->memoryLimit = 0; + } + } + + return $this->memoryLimit; + } + + /** + * Normalized a given value of memory limit into the number of bytes + * + * @param string|int $value + * @return int + */ + protected function normalizeMemoryLimit($value) + { + if (is_numeric($value)) { + return (int)$value; + } + + if (!preg_match('/(\-?\d+)\s*(\w*)/', ini_get('memory_limit'), $matches)) { + throw new Exception\InvalidArgumentException("Invalid memory limit '{$value}'"); + } + + $value = (int)$matches[1]; + if ($value <= 0) { + return 0; + } + + switch (strtoupper($matches[2])) { + case 'G': + $value*= 1024; + // Break intentionally omitted + + case 'M': + $value*= 1024; + // Break intentionally omitted + + case 'K': + $value*= 1024; + // Break intentionally omitted + } + + return $value; + } + +} diff --git a/src/Storage/Adapter/WinCache.php b/src/Storage/Adapter/WinCache.php new file mode 100644 index 000000000..aa78b4c1c --- /dev/null +++ b/src/Storage/Adapter/WinCache.php @@ -0,0 +1,511 @@ +options) { + $this->setOptions(new WinCacheOptions()); + } + return $this->options; + } + + /* TotalSpaceCapableInterface */ + + /** + * Get total space in bytes + * + * @return int|float + */ + public function getTotalSpace() + { + $mem = wincache_ucache_meminfo(); + return $mem['memory_total']; + } + + /* AvailableSpaceCapableInterface */ + + /** + * Get available space in bytes + * + * @return int|float + */ + public function getAvailableSpace() + { + $mem = wincache_ucache_meminfo(); + return $mem['memory_free']; + } + + /* FlushableInterface */ + + /** + * Flush the whole storage + * + * @return boolean + */ + public function flush() + { + return wincache_ucache_clear(); + } + + /* reading */ + + /** + * Internal method to get an item. + * + * @param string $normalizedKey + * @param boolean $success + * @param mixed $casToken + * @return mixed Data on success, null on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $result = wincache_ucache_get($internalKey, $success); + + if ($success) { + $casToken = $result; + } + + return $result; + } + + /** + * Internal method to get multiple items. + * + * @param array $normalizedKeys + * @return array Associative array of keys and values + * @throws Exception\ExceptionInterface + */ + protected function internalGetItems(array & $normalizedKeys) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKeys = array(); + foreach ($normalizedKeys as $normalizedKey) { + $internalKeys[] = $prefix . $normalizedKey; + } + + $fetch = wincache_ucache_get($internalKeys); + + // remove namespace prefix + $prefixL = strlen($prefix); + $result = array(); + foreach ($fetch as $internalKey => & $value) { + $result[ substr($internalKey, $prefixL) ] = & $value; + } + + return $result; + } + + /** + * Internal method to test if an item exists. + * + * @param string $normalizedKey + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalHasItem(& $normalizedKey) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + return wincache_ucache_exists($prefix . $normalizedKey); + } + + /** + * Get metadata of an item. + * + * @param string $normalizedKey + * @return array|boolean Metadata on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetMetadata(& $normalizedKey) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + + $info = wincache_ucache_info(true, $internalKey); + if (isset($info['ucache_entries'][1])) { + $metadata = $info['ucache_entries'][1]; + $this->normalizeMetadata($metadata); + return $metadata; + } else { + return false; + } + } + + /* writing */ + + /** + * Internal method to store an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalSetItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $ttl = $options->getTtl(); + + if (!wincache_ucache_set($internalKey, $value, $ttl)) { + $type = is_object($value) ? get_class($value) : gettype($value); + throw new Exception\RuntimeException( + "wincache_ucache_set('{$internalKey}', <{$type}>, {$ttl}) failed" + ); + } + + return true; + } + + /** + * Internal method to store multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalSetItems(array & $normalizedKeyValuePairs) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $prefixL = strlen($prefix); + + $internalKeyValuePairs = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + $internalKey = $prefix . $normalizedKey; + $internalKeyValuePairs[$internalKey] = $value; + } + + $result = wincache_ucache_set($internalKeyValuePairs, null, $options->getTtl()); + + // remove key prefic + foreach ($result as & $key) { + $key = substr($key, $prefixL); + } + + return $result; + } + + /** + * Add an item. + * + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalAddItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $ttl = $options->getTtl(); + + if (!wincache_ucache_add($internalKey, $value, $ttl)) { + $type = is_object($value) ? get_class($value) : gettype($value); + throw new Exception\RuntimeException( + "wincache_ucache_add('{$internalKey}', <{$type}>, {$ttl}) failed" + ); + } + + return true; + } + + /** + * Internal method to add multiple items. + * + * @param array $normalizedKeyValuePairs + * @return array Array of not stored keys + * @throws Exception\ExceptionInterface + */ + protected function internalAddItems(array & $normalizedKeyValuePairs) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $prefixL = strlen($prefix); + + $internalKeyValuePairs = array(); + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + $internalKey = $prefix . $normalizedKey; + $internalKeyValuePairs[$internalKey] = $value; + } + + $result = wincache_ucache_add($internalKeyValuePairs, null, $options->getTtl()); + + // remove key prefic + foreach ($result as & $key) { + $key = substr($key, $prefixL); + } + + return $result; + } + + /** + * Internal method to replace an existing item. + * + * @param string $normalizedKey + * @param mixed $value + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalReplaceItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + if (!wincache_ucache_exists($internalKey)) { + return false; + } + + $ttl = $options->getTtl(); + if (!wincache_ucache_set($internalKey, $value, $ttl)) { + $type = is_object($value) ? get_class($value) : gettype($value); + throw new Exception\RuntimeException( + "wincache_ucache_set('{$internalKey}', <{$type}>, {$ttl}) failed" + ); + } + + return true; + } + + /** + * Internal method to remove an item. + * + * @param string $normalizedKey + * @return boolean + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItem(& $normalizedKey) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + return wincache_ucache_delete($internalKey); + } + + /** + * Internal method to remove multiple items. + * + * @param array $normalizedKeys + * @return array Array of not removed keys + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItems(array & $normalizedKeys) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + + $internalKeys = array(); + foreach ($normalizedKeys as $normalizedKey) { + $internalKeys[] = $prefix . $normalizedKey; + } + + $result = wincache_ucache_delete($internalKeys); + if ($result === false) { + return $normalizedKeys; + } elseif ($result) { + // remove key prefix + $prefixL = strlen($prefix); + foreach ($result as & $key) { + $key = substr($key, $prefixL); + } + } + + return $result; + } + + /** + * Internal method to increment an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|boolean The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalIncrementItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + return wincache_ucache_inc($internalKey, (int)$value); + } + + /** + * Internal method to decrement an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|boolean The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalDecrementItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $prefix = $options->getNamespace() . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + return wincache_ucache_dec($internalKey, (int)$value); + } + + /* status */ + + /** + * Internal method to get capabilities of this adapter + * + * @return Capabilities + */ + protected function internalGetCapabilities() + { + if ($this->capabilities === null) { + $marker = new stdClass(); + $capabilities = new Capabilities( + $this, + $marker, + array( + 'supportedDatatypes' => array( + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => true, + 'object' => 'object', + 'resource' => false, + ), + 'supportedMetadata' => array( + 'internal_key', 'ttl', 'hits', 'size' + ), + 'minTtl' => 1, + 'maxTtl' => 0, + 'staticTtl' => true, + 'ttlPrecision' => 1, + 'useRequestTime' => false, + 'expiredRead' => false, + 'namespaceIsPrefix' => true, + 'namespaceSeparator' => $this->getOptions()->getNamespaceSeparator(), + ) + ); + + // update namespace separator on change option + $this->getEventManager()->attach('option', function ($event) use ($capabilities, $marker) { + $params = $event->getParams(); + + if (isset($params['namespace_separator'])) { + $capabilities->setNamespaceSeparator($marker, $params['namespace_separator']); + } + }); + + $this->capabilities = $capabilities; + $this->capabilityMarker = $marker; + } + + return $this->capabilities; + } + + /* internal */ + + /** + * Normalize metadata to work with WinCache + * + * @param array $metadata + * @return void + */ + protected function normalizeMetadata(array & $metadata) + { + $metadata['internal_key'] = $metadata['key_name']; + $metadata['hits'] = $metadata['hitcount']; + $metadata['ttl'] = $metadata['ttl_seconds']; + $metadata['size'] = $metadata['value_size']; + + unset( + $metadata['key_name'], + $metadata['hitcount'], + $metadata['ttl_seconds'], + $metadata['value_size'] + ); + } +} diff --git a/src/Storage/Adapter/WinCacheOptions.php b/src/Storage/Adapter/WinCacheOptions.php new file mode 100644 index 000000000..32569a628 --- /dev/null +++ b/src/Storage/Adapter/WinCacheOptions.php @@ -0,0 +1,54 @@ +triggerOptionEvent('namespace_separator', $namespaceSeparator); + $this->namespaceSeparator = $namespaceSeparator; + return $this; + } + + /** + * Get namespace separator + * + * @return string + */ + public function getNamespaceSeparator() + { + return $this->namespaceSeparator; + } +} diff --git a/src/Storage/Adapter/ZendServerDisk.php b/src/Storage/Adapter/ZendServerDisk.php new file mode 100644 index 000000000..e9fe81baa --- /dev/null +++ b/src/Storage/Adapter/ZendServerDisk.php @@ -0,0 +1,185 @@ +totalSpace !== null) { + $path = $this->getOptions()->getCacheDir(); + + ErrorHandler::start(); + $total = disk_total_space($path); + $error = ErrorHandler::stop(); + if ($total === false) { + throw new Exception\RuntimeException("Can't detect total space of '{$path}'", 0, $error); + } + } + return $this->totalSpace; + } + + /* AvailableSpaceCapableInterface */ + + /** + * Get available space in bytes + * + * @return int|float + */ + public function getAvailableSpace() + { + $path = $this->getOptions()->getCacheDir(); + + ErrorHandler::start(); + $avail = disk_free_space($path); + $error = ErrorHandler::stop(); + if ($avail === false) { + throw new Exception\RuntimeException("Can't detect free space of '{$path}'", 0, $error); + } + + return $avail; + } + + /* internal */ + + /** + * Store data into Zend Data Disk Cache + * + * @param string $internalKey + * @param mixed $value + * @param int $ttl + * @return void + * @throws Exception\RuntimeException + */ + protected function zdcStore($internalKey, $value, $ttl) + { + if (!zend_disk_cache_store($internalKey, $value, $ttl)) { + $valueType = gettype($value); + throw new Exception\RuntimeException( + "zend_disk_cache_store($internalKey, <{$valueType}>, {$ttl}) failed" + ); + } + } + + /** + * Fetch a single item from Zend Data Disk Cache + * + * @param string $internalKey + * @return mixed The stored value or FALSE if item wasn't found + * @throws Exception\RuntimeException + */ + protected function zdcFetch($internalKey) + { + return zend_disk_cache_fetch((string)$internalKey); + } + + /** + * Fetch multiple items from Zend Data Disk Cache + * + * @param array $internalKeys + * @return array All found items + * @throws Exception\RuntimeException + */ + protected function zdcFetchMulti(array $internalKeys) + { + $items = zend_disk_cache_fetch($internalKeys); + if ($items === false) { + throw new Exception\RuntimeException("zend_disk_cache_fetch() failed"); + } + return $items; + } + + /** + * Delete data from Zend Data Disk Cache + * + * @param string $internalKey + * @return boolean + * @throws Exception\RuntimeException + */ + protected function zdcDelete($internalKey) + { + return zend_disk_cache_delete($internalKey); + } +} diff --git a/src/Storage/Adapter/ZendServerShm.php b/src/Storage/Adapter/ZendServerShm.php new file mode 100644 index 000000000..63759e085 --- /dev/null +++ b/src/Storage/Adapter/ZendServerShm.php @@ -0,0 +1,144 @@ +, {$ttl}) failed" + ); + } + } + + /** + * Fetch a single item from Zend Data SHM Cache + * + * @param string $internalKey + * @return mixed The stored value or FALSE if item wasn't found + * @throws Exception\RuntimeException + */ + protected function zdcFetch($internalKey) + { + return zend_shm_cache_fetch((string)$internalKey); + } + + /** + * Fetch multiple items from Zend Data SHM Cache + * + * @param array $internalKeys + * @return array All found items + * @throws Exception\RuntimeException + */ + protected function zdcFetchMulti(array $internalKeys) + { + $items = zend_shm_cache_fetch($internalKeys); + if ($items === false) { + throw new Exception\RuntimeException("zend_shm_cache_fetch() failed"); + } + return $items; + } + + /** + * Delete data from Zend Data SHM Cache + * + * @param string $internalKey + * @return boolean + * @throws Exception\RuntimeException + */ + protected function zdcDelete($internalKey) + { + return zend_shm_cache_delete($internalKey); + } +} diff --git a/src/Storage/AdapterPluginManager.php b/src/Storage/AdapterPluginManager.php new file mode 100644 index 000000000..c10305dd8 --- /dev/null +++ b/src/Storage/AdapterPluginManager.php @@ -0,0 +1,78 @@ + 'Zend\Cache\Storage\Adapter\Apc', + 'filesystem' => 'Zend\Cache\Storage\Adapter\Filesystem', + 'memcached' => 'Zend\Cache\Storage\Adapter\Memcached', + 'memory' => 'Zend\Cache\Storage\Adapter\Memory', + 'sysvshm' => 'Zend\Cache\Storage\Adapter\SystemVShm', + 'systemvshm' => 'Zend\Cache\Storage\Adapter\SystemVShm', + 'sqlite' => 'Zend\Cache\Storage\Adapter\Sqlite', + 'dba' => 'Zend\Cache\Storage\Adapter\Dba', + 'wincache' => 'Zend\Cache\Storage\Adapter\WinCache', + 'xcache' => 'Zend\Cache\Storage\Adapter\XCache', + 'zendserverdisk' => 'Zend\Cache\Storage\Adapter\ZendServerDisk', + 'zendservershm' => 'Zend\Cache\Storage\Adapter\ZendServerShm', + ); + + /** + * Do not share by default + * + * @var array + */ + protected $shareByDefault = false; + + /** + * Validate the plugin + * + * Checks that the adapter loaded is an instance of StorageInterface. + * + * @param mixed $plugin + * @return void + * @throws Exception\RuntimeException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof StorageInterface) { + // we're okay + return; + } + + throw new Exception\RuntimeException(sprintf( + 'Plugin of type %s is invalid; must implement %s\StorageInterface', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__ + )); + } +} diff --git a/src/Storage/AvailableSpaceCapableInterface.php b/src/Storage/AvailableSpaceCapableInterface.php new file mode 100644 index 000000000..04d0eb224 --- /dev/null +++ b/src/Storage/AvailableSpaceCapableInterface.php @@ -0,0 +1,26 @@ +storage = $storage; + $this->marker = $marker; + $this->baseCapabilities = $baseCapabilities; + + foreach ($capabilities as $name => $value) { + $this->setCapability($marker, $name, $value); + } + } + + /** + * Get the storage adapter + * + * @return Adapter + */ + public function getAdapter() + { + return $this->storage; + } + + /** + * Get supported datatypes + * + * @return array + */ + public function getSupportedDatatypes() + { + return $this->getCapability('supportedDatatypes', array( + 'NULL' => false, + 'boolean' => false, + 'integer' => false, + 'double' => false, + 'string' => true, + 'array' => false, + 'object' => false, + 'resource' => false, + )); + } + + /** + * Set supported datatypes + * + * @param stdClass $marker + * @param array $datatypes + * @return Capabilities Fluent interface + */ + public function setSupportedDatatypes(stdClass $marker, array $datatypes) + { + $allTypes = array( + 'array', + 'boolean', + 'double', + 'integer', + 'NULL', + 'object', + 'resource', + 'string', + ); + + // check/normalize datatype values + foreach ($datatypes as $type => &$toType) { + if (!in_array($type, $allTypes)) { + throw new Exception\InvalidArgumentException("Unknown datatype '{$type}'"); + } + + if (is_string($toType)) { + $toType = strtolower($toType); + if (!in_array($toType, $allTypes)) { + throw new Exception\InvalidArgumentException("Unknown datatype '{$toType}'"); + } + } else { + $toType = (bool) $toType; + } + } + + // add missing datatypes as not supported + $missingTypes = array_diff($allTypes, array_keys($datatypes)); + foreach ($missingTypes as $type) { + $datatypes[type] = false; + } + + return $this->setCapability($marker, 'supportedDatatypes', $datatypes); + } + + /** + * Get supported metadata + * + * @return array + */ + public function getSupportedMetadata() + { + return $this->getCapability('supportedMetadata', array()); + } + + /** + * Set supported metadata + * + * @param stdClass $marker + * @param string[] $metadata + * @return Capabilities Fluent interface + */ + public function setSupportedMetadata(stdClass $marker, array $metadata) + { + foreach ($metadata as $name) { + if (!is_string($name)) { + throw new Exception\InvalidArgumentException('$metadata must be an array of strings'); + } + } + return $this->setCapability($marker, 'supportedMetadata', $metadata); + } + + /** + * Get minimum supported time-to-live + * + * @return int 0 means items never expire + */ + public function getMinTtl() + { + return $this->getCapability('minTtl', 0); + } + + /** + * Set minimum supported time-to-live + * + * @param stdClass $marker + * @param int $minTtl + * @return Capabilities Fluent interface + */ + public function setMinTtl(stdClass $marker, $minTtl) + { + $minTtl = (int) $minTtl; + if ($minTtl < 0) { + throw new Exception\InvalidArgumentException('$minTtl must be greater or equal 0'); + } + return $this->setCapability($marker, 'minTtl', $minTtl); + } + + /** + * Get maximum supported time-to-live + * + * @return int 0 means infinite + */ + public function getMaxTtl() + { + return $this->getCapability('maxTtl', 0); + } + + /** + * Set maximum supported time-to-live + * + * @param stdClass $marker + * @param int $maxTtl + * @return Capabilities Fluent interface + */ + public function setMaxTtl(stdClass $marker, $maxTtl) + { + $maxTtl = (int) $maxTtl; + if ($maxTtl < 0) { + throw new Exception\InvalidArgumentException('$maxTtl must be greater or equal 0'); + } + return $this->setCapability($marker, 'maxTtl', $maxTtl); + } + + /** + * Is the time-to-live handled static (on write) + * or dynamic (on read) + * + * @return boolean + */ + public function getStaticTtl() + { + return $this->getCapability('staticTtl', false); + } + + /** + * Set if the time-to-live handled static (on write) or dynamic (on read) + * + * @param stdClass $marker + * @param boolean $flag + * @return Capabilities Fluent interface + */ + public function setStaticTtl(stdClass $marker, $flag) + { + return $this->setCapability($marker, 'staticTtl', (bool)$flag); + } + + /** + * Get time-to-live precision + * + * @return float + */ + public function getTtlPrecision() + { + return $this->getCapability('ttlPrecision', 1); + } + + /** + * Set time-to-live precision + * + * @param stdClass $marker + * @param float $ttlPrecision + * @return Capabilities Fluent interface + */ + public function setTtlPrecision(stdClass $marker, $ttlPrecision) + { + $ttlPrecision = (float) $ttlPrecision; + if ($ttlPrecision <= 0) { + throw new Exception\InvalidArgumentException('$ttlPrecision must be greater than 0'); + } + return $this->setCapability($marker, 'ttlPrecision', $ttlPrecision); + } + + /** + * Get use request time + * + * @return boolean + */ + public function getUseRequestTime() + { + return $this->getCapability('useRequestTime', false); + } + + /** + * Set use request time + * + * @param stdClass $marker + * @param boolean $flag + * @return Capabilities Fluent interface + */ + public function setUseRequestTime(stdClass $marker, $flag) + { + return $this->setCapability($marker, 'useRequestTime', (bool)$flag); + } + + /** + * Get if expired items are readable + * + * @return boolean + */ + public function getExpiredRead() + { + return $this->getCapability('expiredRead', false); + } + + /** + * Set if expired items are readable + * + * @param stdClass $marker + * @param boolean $flag + * @return Capabilities Fluent interface + */ + public function setExpiredRead(stdClass $marker, $flag) + { + return $this->setCapability($marker, 'expiredRead', (bool)$flag); + } + + /** + * Get maximum key lenth + * + * @return int -1 means unknown, 0 means infinite + */ + public function getMaxKeyLength() + { + return $this->getCapability('maxKeyLength', -1); + } + + /** + * Set maximum key length + * + * @param stdClass $marker + * @param int $maxKeyLength + * @return Capabilities Fluent interface + */ + public function setMaxKeyLength(stdClass $marker, $maxKeyLength) + { + $maxKeyLength = (int) $maxKeyLength; + if ($maxKeyLength < -1) { + throw new Exception\InvalidArgumentException('$maxKeyLength must be greater or equal than -1'); + } + return $this->setCapability($marker, 'maxKeyLength', $maxKeyLength); + } + + /** + * Get if namespace support is implemented as prefix + * + * @return boolean + */ + public function getNamespaceIsPrefix() + { + return $this->getCapability('namespaceIsPrefix', true); + } + + /** + * Set if namespace support is implemented as prefix + * + * @param stdClass $marker + * @param boolean $flag + * @return Capabilities Fluent interface + */ + public function setNamespaceIsPrefix(stdClass $marker, $flag) + { + return $this->setCapability($marker, 'namespaceIsPrefix', (bool)$flag); + } + + /** + * Get namespace separator if namespace is implemented as prefix + * + * @return string + */ + public function getNamespaceSeparator() + { + return $this->getCapability('namespaceSeparator', ''); + } + + /** + * Set the namespace separator if namespace is implemented as prefix + * + * @param stdClass $marker + * @param string $separator + * @return Capabilities Fluent interface + */ + public function setNamespaceSeparator(stdClass $marker, $separator) + { + return $this->setCapability($marker, 'namespaceSeparator', (string) $separator); + } + + /** + * Get a capability + * + * @param string $property + * @param mixed $default + * @return mixed + */ + protected function getCapability($property, $default = null) + { + if ($this->$property !== null) { + return $this->$property; + } elseif ($this->baseCapabilities) { + $getMethod = 'get' . $property; + return $this->baseCapabilities->$getMethod(); + } + return $default; + } + + /** + * Change a capability + * + * @param stdClass $marker + * @param string $name + * @param mixed $value + * @return Capabilities Fluent interface + * @throws Exception\InvalidArgumentException + */ + protected function setCapability(stdClass $marker, $property, $value) + { + if ($this->marker !== $marker) { + throw new Exception\InvalidArgumentException('Invalid marker'); + } + + if ($this->$property !== $value) { + $this->$property = $value; + + // trigger event + if ($this->storage instanceof EventsCapableInterface) { + $this->storage->getEventManager()->trigger('capability', $this->storage, new ArrayObject(array( + $property => $value + ))); + } + } + + return $this; + } +} diff --git a/src/Storage/ClearByNamespaceInterface.php b/src/Storage/ClearByNamespaceInterface.php new file mode 100644 index 000000000..c1b1eb8d5 --- /dev/null +++ b/src/Storage/ClearByNamespaceInterface.php @@ -0,0 +1,27 @@ +setStorage($target); + } + + /** + * Alias of setTarget + * + * @param StorageInterface $storage + * @return Event + * @see Zend\EventManager\Event::setTarget() + */ + public function setStorage(StorageInterface $storage) + { + $this->target = $storage; + return $this; + } + + /** + * Alias of getTarget + * + * @return StorageInterface + */ + public function getStorage() + { + return $this->getTarget(); + } +} diff --git a/src/Storage/ExceptionEvent.php b/src/Storage/ExceptionEvent.php new file mode 100644 index 000000000..d9ac2942d --- /dev/null +++ b/src/Storage/ExceptionEvent.php @@ -0,0 +1,97 @@ +setException($exception); + } + + /** + * Set the exception to be thrown + * + * @param Exception $exception + * @return ExceptionEvent + */ + public function setException(Exception $exception) + { + $this->exception = $exception; + return $this; + } + + /** + * Get the exception to be thrown + * + * @return Exception + */ + public function getException() + { + return $this->exception; + } + + /** + * Throw the exception or use the result + * + * @param boolean $flag + * @return ExceptionEvent + */ + public function setThrowException($flag) + { + $this->throwException = (bool) $flag; + return $this; + } + + /** + * Throw the exception or use the result + * + * @return boolean + */ + public function getThrowException() + { + return $this->throwException; + } +} diff --git a/src/Storage/FlushableInterface.php b/src/Storage/FlushableInterface.php new file mode 100644 index 000000000..19d8055fc --- /dev/null +++ b/src/Storage/FlushableInterface.php @@ -0,0 +1,26 @@ +options = $options; + return $this; + } + + /** + * Get all pattern options + * + * @return PluginOptions + */ + public function getOptions() + { + if (null === $this->options) { + $this->setOptions(new PluginOptions()); + } + return $this->options; + } +} diff --git a/src/Storage/Plugin/ClearExpiredByFactor.php b/src/Storage/Plugin/ClearExpiredByFactor.php new file mode 100644 index 000000000..4b4d60a50 --- /dev/null +++ b/src/Storage/Plugin/ClearExpiredByFactor.php @@ -0,0 +1,104 @@ +handles[$index])) { + throw new Exception\LogicException('Plugin already attached'); + } + + $handles = array(); + $this->handles[$index] = & $handles; + + $callback = array($this, 'clearExpiredByFactor'); + $handles[] = $events->attach('setItem.post', $callback, $priority); + $handles[] = $events->attach('setItems.post', $callback, $priority); + $handles[] = $events->attach('addItem.post', $callback, $priority); + $handles[] = $events->attach('addItems.post', $callback, $priority); + + return $this; + } + + /** + * Detach + * + * @param EventManagerInterface $events + * @return ClearByFactor + * @throws Exception\LogicException + */ + public function detach(EventManagerInterface $events) + { + $index = spl_object_hash($events); + if (!isset($this->handles[$index])) { + throw new Exception\LogicException('Plugin not attached'); + } + + // detach all handles of this index + foreach ($this->handles[$index] as $handle) { + $events->detach($handle); + } + + // remove all detached handles + unset($this->handles[$index]); + + return $this; + } + + /** + * Clear expired items by factor after writing new item(s) + * + * @param PostEvent $event + * @return void + */ + public function clearExpiredByFactor(PostEvent $event) + { + $storage = $event->getStorage(); + if (!($storage instanceof ClearExpiredInterface)) { + return; + } + + $factor = $this->getOptions()->getClearingFactor(); + if ($factor && mt_rand(1, $factor) == 1) { + $storage->clearExpired(); + } + } +} diff --git a/src/Storage/Plugin/ExceptionHandler.php b/src/Storage/Plugin/ExceptionHandler.php new file mode 100644 index 000000000..b70fdd85c --- /dev/null +++ b/src/Storage/Plugin/ExceptionHandler.php @@ -0,0 +1,129 @@ +handles[$index])) { + throw new Exception\LogicException('Plugin already attached'); + } + + $callback = array($this, 'onException'); + $handles = array(); + $this->handles[$index] = & $handles; + + // read + $handles[] = $events->attach('getItem.exception', $callback, $priority); + $handles[] = $events->attach('getItems.exception', $callback, $priority); + + $handles[] = $events->attach('hasItem.exception', $callback, $priority); + $handles[] = $events->attach('hasItems.exception', $callback, $priority); + + $handles[] = $events->attach('getMetadata.exception', $callback, $priority); + $handles[] = $events->attach('getMetadatas.exception', $callback, $priority); + + // write + $handles[] = $events->attach('setItem.exception', $callback, $priority); + $handles[] = $events->attach('setItems.exception', $callback, $priority); + + $handles[] = $events->attach('addItem.exception', $callback, $priority); + $handles[] = $events->attach('addItems.exception', $callback, $priority); + + $handles[] = $events->attach('replaceItem.exception', $callback, $priority); + $handles[] = $events->attach('replaceItems.exception', $callback, $priority); + + $handles[] = $events->attach('touchItem.exception', $callback, $priority); + $handles[] = $events->attach('touchItems.exception', $callback, $priority); + + $handles[] = $events->attach('removeItem.exception', $callback, $priority); + $handles[] = $events->attach('removeItems.exception', $callback, $priority); + + $handles[] = $events->attach('checkAndSetItem.exception', $callback, $priority); + + // increment / decrement item(s) + $handles[] = $events->attach('incrementItem.exception', $callback, $priority); + $handles[] = $events->attach('incrementItems.exception', $callback, $priority); + + $handles[] = $events->attach('decrementItem.exception', $callback, $priority); + $handles[] = $events->attach('decrementItems.exception', $callback, $priority); + + return $this; + } + + /** + * Detach + * + * @param EventManagerInterface $events + * @return ExceptionHandler + * @throws Exception\LogicException + */ + public function detach(EventManagerInterface $events) + { + $index = spl_object_hash($events); + if (!isset($this->handles[$index])) { + throw new Exception\LogicException('Plugin not attached'); + } + + // detach all handles of this index + foreach ($this->handles[$index] as $handle) { + $events->detach($handle); + } + + // remove all detached handles + unset($this->handles[$index]); + + return $this; + } + + /** + * On exception + * + * @param ExceptionEvent $event + * @return void + */ + public function onException(ExceptionEvent $event) + { + $options = $this->getOptions(); + $callback = $options->getExceptionCallback(); + if ($callback) { + call_user_func($callback, $event->getException()); + } + + $event->setThrowException($options->getThrowExceptions()); + } +} diff --git a/src/Storage/Plugin/IgnoreUserAbort.php b/src/Storage/Plugin/IgnoreUserAbort.php new file mode 100644 index 000000000..86a71428d --- /dev/null +++ b/src/Storage/Plugin/IgnoreUserAbort.php @@ -0,0 +1,171 @@ +handles[$index])) { + throw new Exception\LogicException('Plugin already attached'); + } + + $handles = array(); + $this->handles[$index] = & $handles; + + $cbOnBefore = array($this, 'onBefore'); + $cbOnAfter = array($this, 'onAfter'); + + $handles[] = $events->attach('setItem.pre', $cbOnBefore, $priority); + $handles[] = $events->attach('setItem.post', $cbOnAfter, $priority); + $handles[] = $events->attach('setItem.exception', $cbOnAfter, $priority); + + $handles[] = $events->attach('setItems.pre', $cbOnBefore, $priority); + $handles[] = $events->attach('setItems.post', $cbOnAfter, $priority); + $handles[] = $events->attach('setItems.exception', $cbOnAfter, $priority); + + $handles[] = $events->attach('addItem.pre', $cbOnBefore, $priority); + $handles[] = $events->attach('addItem.post', $cbOnAfter, $priority); + $handles[] = $events->attach('addItem.exception', $cbOnAfter, $priority); + + $handles[] = $events->attach('addItems.pre', $cbOnBefore, $priority); + $handles[] = $events->attach('addItems.post', $cbOnAfter, $priority); + $handles[] = $events->attach('addItems.exception', $cbOnAfter, $priority); + + $handles[] = $events->attach('replaceItem.pre', $cbOnBefore, $priority); + $handles[] = $events->attach('replaceItem.post', $cbOnAfter, $priority); + $handles[] = $events->attach('replaceItem.exception', $cbOnAfter, $priority); + + $handles[] = $events->attach('replaceItems.pre', $cbOnBefore, $priority); + $handles[] = $events->attach('replaceItems.post', $cbOnAfter, $priority); + $handles[] = $events->attach('replaceItems.exception', $cbOnAfter, $priority); + + $handles[] = $events->attach('checkAndSetItem.pre', $cbOnBefore, $priority); + $handles[] = $events->attach('checkAndSetItem.post', $cbOnAfter, $priority); + $handles[] = $events->attach('checkAndSetItem.exception', $cbOnAfter, $priority); + + // increment / decrement item(s) + $handles[] = $events->attach('incrementItem.pre', $cbOnBefore, $priority); + $handles[] = $events->attach('incrementItem.post', $cbOnAfter, $priority); + $handles[] = $events->attach('incrementItem.exception', $cbOnAfter, $priority); + + $handles[] = $events->attach('incrementItems.pre', $cbOnBefore, $priority); + $handles[] = $events->attach('incrementItems.post', $cbOnAfter, $priority); + $handles[] = $events->attach('incrementItems.exception', $cbOnAfter, $priority); + + $handles[] = $events->attach('decrementItem.pre', $cbOnBefore, $priority); + $handles[] = $events->attach('decrementItem.post', $cbOnAfter, $priority); + $handles[] = $events->attach('decrementItem.exception', $cbOnAfter, $priority); + + $handles[] = $events->attach('decrementItems.pre', $cbOnBefore, $priority); + $handles[] = $events->attach('decrementItems.post', $cbOnAfter, $priority); + $handles[] = $events->attach('decrementItems.exception', $cbOnAfter, $priority); + + return $this; + } + + /** + * Detach + * + * @param EventManagerInterface $events + * @return Serializer + * @throws Exception\LogicException + */ + public function detach(EventManagerInterface $events) + { + $index = spl_object_hash($events); + if (!isset($this->handles[$index])) { + throw new Exception\LogicException('Plugin not attached'); + } + + // detach all handles of this index + foreach ($this->handles[$index] as $handle) { + $events->detach($handle); + } + + // remove all detached handles + unset($this->handles[$index]); + + return $this; + } + + /** + * Activate ignore_user_abort if not already done + * and save the target who activated it. + * + * @param Event $event + * @return void + */ + public function onBefore(Event $event) + { + if ($this->activatedTarget === null && !ignore_user_abort(true)) { + $this->activatedTarget = $event->getTarget(); + } + } + + /** + * Reset ignore_user_abort if it's activated and if it's the same target + * who activated it. + * + * If exit_on_abort is enabled and the connection has been aborted + * exit the script. + * + * @param Event $event + * @return void + */ + public function onAfter(Event $event) + { + if ($this->activatedTarget === $event->getTarget()) { + // exit if connection aborted + if ($this->getOptions()->getExitOnAbort() && connection_aborted()) { + exit; + } + + // reset ignore_user_abort + ignore_user_abort(false); + + // remove activated target + $this->activatedTarget = null; + } + } +} diff --git a/src/Storage/Plugin/OptimizeByFactor.php b/src/Storage/Plugin/OptimizeByFactor.php new file mode 100644 index 000000000..aa09ae1de --- /dev/null +++ b/src/Storage/Plugin/OptimizeByFactor.php @@ -0,0 +1,101 @@ +handles[$index])) { + throw new Exception\LogicException('Plugin already attached'); + } + + $handles = array(); + $this->handles[$index] = & $handles; + + $callback = array($this, 'optimizeByFactor'); + $handles[] = $events->attach('removeItem.post', $callback, $priority); + $handles[] = $events->attach('removeItems.post', $callback, $priority); + + return $this; + } + + /** + * Detach + * + * @param EventManagerInterface $events + * @return OptimizeByFactor + * @throws Exception\LogicException + */ + public function detach(EventManagerInterface $events) + { + $index = spl_object_hash($events); + if (!isset($this->handles[$index])) { + throw new Exception\LogicException('Plugin not attached'); + } + + // detach all handles of this index + foreach ($this->handles[$index] as $handle) { + $events->detach($handle); + } + + // remove all detached handles + unset($this->handles[$index]); + + return $this; + } + + /** + * Optimize by factor on a success _RESULT_ + * + * @param PostEvent $event + * @return void + */ + public function optimizeByFactor(PostEvent $event) + { + $storage = $event->getStorage(); + if (!($storage instanceof OptimizableInterface)) { + return; + } + + $factor = $this->getOptions()->getOptimizingFactor(); + if ($factor && mt_rand(1, $factor) == 1) { + $storage->optimize(); + } + } +} diff --git a/src/Storage/Plugin/PluginInterface.php b/src/Storage/Plugin/PluginInterface.php new file mode 100644 index 000000000..a657a5f7a --- /dev/null +++ b/src/Storage/Plugin/PluginInterface.php @@ -0,0 +1,36 @@ +clearingFactor = $this->normalizeFactor($clearingFactor); + return $this; + } + + /** + * Get automatic clearing factor + * + * Used by: + * - ClearExpiredByFactor + * + * @return int + */ + public function getClearingFactor() + { + return $this->clearingFactor; + } + + /** + * Set callback to call on intercepted exception + * + * Used by: + * - ExceptionHandler + * + * @param callable ExceptionCallback + * @return PluginOptions + */ + public function setExceptionCallback($exceptionCallback) + { + if ($exceptionCallback !== null && !is_callable($exceptionCallback, true)) { + throw new Exception\InvalidArgumentException('Not a valid callback'); + } + $this->exceptionCallback = $exceptionCallback; + return $this; + } + + /** + * Get callback to call on intercepted exception + * + * Used by: + * - ExceptionHandler + * + * @return null|callable + */ + public function getExceptionCallback() + { + return $this->exceptionCallback; + } + + /** + * Exit if connection aborted and ignore_user_abort is disabled. + * + * @param boolean $exitOnAbort + * @return PluginOptions + */ + public function setExitOnAbort($exitOnAbort) + { + $this->exitOnAbort = (bool) $exitOnAbort; + return $this; + } + + /** + * Exit if connection aborted and ignore_user_abort is disabled. + * + * @return boolean + */ + public function getExitOnAbort() + { + return $this->exitOnAbort; + } + + /** + * Set automatic optimizing factor + * + * Used by: + * - OptimizeByFactor + * + * @param int $optimizingFactor + * @return PluginOptions + */ + public function setOptimizingFactor($optimizingFactor) + { + $this->optimizingFactor = $this->normalizeFactor($optimizingFactor); + return $this; + } + + /** + * Set automatic optimizing factor + * + * Used by: + * - OptimizeByFactor + * + * @return int + */ + public function getOptimizingFactor() + { + return $this->optimizingFactor; + } + + /** + * Set serializer + * + * Used by: + * - Serializer + * + * @param string|SerializerAdapter $serializer + * @return Serializer + */ + public function setSerializer($serializer) + { + if (!is_string($serializer) && !$serializer instanceof SerializerAdapter) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects either a string serializer name or Zend\Serializer\Adapter\AdapterInterface instance; ' + . 'received "%s"', + __METHOD__, + (is_object($serializer) ? get_class($serializer) : gettype($serializer)) + )); + } + $this->serializer = $serializer; + return $this; + } + + /** + * Get serializer + * + * Used by: + * - Serializer + * + * @return SerializerAdapter + */ + public function getSerializer() + { + if (!$this->serializer instanceof SerializerAdapter) { + // use default serializer + if (!$this->serializer) { + $this->setSerializer(SerializerFactory::getDefaultAdapter()); + // instantiate by class name + serializer_options + } else { + $options = $this->getSerializerOptions(); + $this->setSerializer(SerializerFactory::factory($this->serializer, $options)); + } + } + return $this->serializer; + } + + /** + * Set configuration options for instantiating a serializer adapter + * + * Used by: + * - Serializer + * + * @param mixed $serializerOptions + * @return PluginOptions + */ + public function setSerializerOptions($serializerOptions) + { + $this->serializerOptions = $serializerOptions; + return $this; + } + + /** + * Get configuration options for instantiating a serializer adapter + * + * Used by: + * - Serializer + * + * @return array + */ + public function getSerializerOptions() + { + return $this->serializerOptions; + } + + /** + * Set flag indicating we should re-throw exceptions + * + * Used by: + * - ExceptionHandler + * + * @param bool $throwExceptions + * @return PluginOptions + */ + public function setThrowExceptions($throwExceptions) + { + $this->throwExceptions = (bool) $throwExceptions; + return $this; + } + + /** + * Should we re-throw exceptions? + * + * Used by: + * - ExceptionHandler + * + * @return bool + */ + public function getThrowExceptions() + { + return $this->throwExceptions; + } + + /** + * Normalize a factor + * + * Cast to int and ensure we have a value greater than zero. + * + * @param int $factor + * @return int + * @throws Exception\InvalidArgumentException + */ + protected function normalizeFactor($factor) + { + $factor = (int) $factor; + if ($factor < 0) { + throw new Exception\InvalidArgumentException( + "Invalid factor '{$factor}': must be greater or equal 0" + ); + } + return $factor; + } +} diff --git a/src/Storage/Plugin/Serializer.php b/src/Storage/Plugin/Serializer.php new file mode 100644 index 000000000..ec8c46ba8 --- /dev/null +++ b/src/Storage/Plugin/Serializer.php @@ -0,0 +1,315 @@ +handles[$index])) { + throw new Exception\LogicException('Plugin already attached'); + } + + $handles = array(); + $this->handles[$index] = & $handles; + + // The higher the priority the sooner the plugin will be called on pre events + // but the later it will be called on post events. + $prePriority = $priority; + $postPriority = -$priority; + + // read + $handles[] = $events->attach('getItem.post', array($this, 'onReadItemPost'), $postPriority); + $handles[] = $events->attach('getItems.post', array($this, 'onReadItemsPost'), $postPriority); + + // write + $handles[] = $events->attach('setItem.pre', array($this, 'onWriteItemPre'), $prePriority); + $handles[] = $events->attach('setItems.pre', array($this, 'onWriteItemsPre'), $prePriority); + + $handles[] = $events->attach('addItem.pre', array($this, 'onWriteItemPre'), $prePriority); + $handles[] = $events->attach('addItems.pre', array($this, 'onWriteItemsPre'), $prePriority); + + $handles[] = $events->attach('replaceItem.pre', array($this, 'onWriteItemPre'), $prePriority); + $handles[] = $events->attach('replaceItems.pre', array($this, 'onWriteItemsPre'), $prePriority); + + $handles[] = $events->attach('checkAndSetItem.pre', array($this, 'onWriteItemPre'), $prePriority); + + // increment / decrement item(s) + $handles[] = $events->attach('incrementItem.pre', array($this, 'onIncrementItemPre'), $prePriority); + $handles[] = $events->attach('incrementItems.pre', array($this, 'onIncrementItemsPre'), $prePriority); + + $handles[] = $events->attach('decrementItem.pre', array($this, 'onDecrementItemPre'), $prePriority); + $handles[] = $events->attach('decrementItems.pre', array($this, 'onDecrementItemsPre'), $prePriority); + + // overwrite capabilities + $handles[] = $events->attach('getCapabilities.post', array($this, 'onGetCapabilitiesPost'), $postPriority); + + return $this; + } + + /** + * Detach + * + * @param EventManagerInterface $events + * @return Serializer + * @throws Exception\LogicException + */ + public function detach(EventManagerInterface $events) + { + $index = spl_object_hash($events); + if (!isset($this->handles[$index])) { + throw new Exception\LogicException('Plugin not attached'); + } + + // detach all handles of this index + foreach ($this->handles[$index] as $handle) { + $events->detach($handle); + } + + // remove all detached handles + unset($this->handles[$index]); + + return $this; + } + + /** + * On read item post + * + * @param PostEvent $event + * @return void + */ + public function onReadItemPost(PostEvent $event) + { + $serializer = $this->getOptions()->getSerializer(); + $result = $event->getResult(); + $result = $serializer->unserialize($result); + $event->setResult($result); + } + + /** + * On read items post + * + * @param PostEvent $event + * @return void + */ + public function onReadItemsPost(PostEvent $event) + { + $serializer = $this->getOptions()->getSerializer(); + $result = $event->getResult(); + foreach ($result as &$value) { + $value = $serializer->unserialize($value); + } + $event->setResult($result); + } + + /** + * On write item pre + * + * @param Event $event + * @return void + */ + public function onWriteItemPre(Event $event) + { + $serializer = $this->getOptions()->getSerializer(); + $params = $event->getParams(); + $params['value'] = $serializer->serialize($params['value']); + } + + /** + * On write items pre + * + * @param Event $event + * @return void + */ + public function onWriteItemsPre(Event $event) + { + $serializer = $this->getOptions()->getSerializer(); + $params = $event->getParams(); + foreach ($params['keyValuePairs'] as &$value) { + $value = $serializer->serialize($value); + } + } + + /** + * On increment item pre + * + * @param Event $event + * @return mixed + */ + public function onIncrementItemPre(Event $event) + { + $storage = $event->getTarget(); + $params = $event->getParams(); + $casToken = null; + $success = null; + $oldValue = $storage->getItem($params['key'], $success, $casToken); + $newValue = $oldValue + $params['value']; + + if ($success) { + $storage->checkAndSetItem($casToken, $params['key'], $oldValue + $params['value']); + $result = $newValue; + } else { + $result = false; + } + + $event->stopPropagation(true); + return $result; + } + + /** + * On increment items pre + * + * @param Event $event + * @return mixed + */ + public function onIncrementItemsPre(Event $event) + { + $storage = $event->getTarget(); + $params = $event->getParams(); + $keyValuePairs = $storage->getItems(array_keys($params['keyValuePairs'])); + foreach ($params['keyValuePairs'] as $key => & $value) { + if (isset($keyValuePairs[$key])) { + $keyValuePairs[$key]+= $value; + } else { + $keyValuePairs[$key] = $value; + } + } + + $failedKeys = $storage->setItems($keyValuePairs); + $result = array(); + foreach ($failedKeys as $failedKey) { + unset($keyValuePairs[$failedKey]); + } + + $event->stopPropagation(true); + return $keyValuePairs; + } + + /** + * On decrement item pre + * + * @param Event $event + * @return mixed + */ + public function onDecrementItemPre(Event $event) + { + $storage = $event->getTarget(); + $params = $event->getParams(); + $success = null; + $casToken = null; + $oldValue = $storage->getItem($params['key'], $success, $casToken); + $newValue = $oldValue - $params['value']; + + if ($success) { + $storage->checkAndSetItem($casToken, $params['key'], $oldValue + $params['value']); + $result = $newValue; + } else { + $result = false; + } + + $event->stopPropagation(true); + return $result; + } + + /** + * On decrement items pre + * + * @param Event $event + * @return mixed + */ + public function onDecrementItemsPre(Event $event) + { + $storage = $event->getTarget(); + $params = $event->getParams(); + $keyValuePairs = $storage->getItems(array_keys($params['keyValuePairs'])); + foreach ($params['keyValuePairs'] as $key => &$value) { + if (isset($keyValuePairs[$key])) { + $keyValuePairs[$key]-= $value; + } else { + $keyValuePairs[$key] = -$value; + } + } + + $failedKeys = $storage->setItems($keyValuePairs); + foreach ($failedKeys as $failedKey) { + unset($keyValuePairs[$failedKey]); + } + + $event->stopPropagation(true); + return $keyValuePairs; + } + + /** + * On get capabilities + * + * @param PostEvent $event + * @return void + */ + public function onGetCapabilitiesPost(PostEvent $event) + { + $baseCapabilities = $event->getResult(); + $index = spl_object_hash($baseCapabilities); + + if (!isset($this->capabilities[$index])) { + $this->capabilities[$index] = new Capabilities( + $baseCapabilities->getAdapter(), + new stdClass(), // marker + array('supportedDatatypes' => array( + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => true, + 'object' => 'object', + 'resource' => false, + )), + $baseCapabilities + ); + } + + $event->setResult($this->capabilities[$index]); + } +} diff --git a/src/Storage/PluginManager.php b/src/Storage/PluginManager.php new file mode 100644 index 000000000..d7746383c --- /dev/null +++ b/src/Storage/PluginManager.php @@ -0,0 +1,71 @@ + 'Zend\Cache\Storage\Plugin\ClearExpiredByFactor', + 'exceptionhandler' => 'Zend\Cache\Storage\Plugin\ExceptionHandler', + 'ignoreuserabort' => 'Zend\Cache\Storage\Plugin\IgnoreUserAbort', + 'optimizebyfactor' => 'Zend\Cache\Storage\Plugin\OptimizeByFactor', + 'serializer' => 'Zend\Cache\Storage\Plugin\Serializer', + ); + + /** + * Do not share by default + * + * @var array + */ + protected $shareByDefault = false; + + /** + * Validate the plugin + * + * Checks that the plugin loaded is an instance of Plugin\PluginInterface. + * + * @param mixed $plugin + * @return void + * @throws Exception\RuntimeException if invalid + */ + public function validatePlugin($plugin) + { + if ($plugin instanceof Plugin\PluginInterface) { + // we're okay + return; + } + + throw new Exception\RuntimeException(sprintf( + 'Plugin of type %s is invalid; must implement %s\Plugin\PluginInterface', + (is_object($plugin) ? get_class($plugin) : gettype($plugin)), + __NAMESPACE__ + )); + } +} diff --git a/src/Storage/PostEvent.php b/src/Storage/PostEvent.php new file mode 100644 index 000000000..e627da69d --- /dev/null +++ b/src/Storage/PostEvent.php @@ -0,0 +1,66 @@ +setResult($result); + } + + /** + * Set the result/return value + * + * @param mixed $value + * @return PostEvent + */ + public function setResult(& $value) + { + $this->result = & $value; + return $this; + } + + /** + * Get the result/return value + * + * @return mixed + */ + public function & getResult() + { + return $this->result; + } +} diff --git a/src/Storage/StorageInterface.php b/src/Storage/StorageInterface.php new file mode 100644 index 000000000..8a5df0bcb --- /dev/null +++ b/src/Storage/StorageInterface.php @@ -0,0 +1,250 @@ + $v) { + $pluginPrio = 1; // default priority + + if (is_string($k)) { + if (!is_array($v)) { + throw new Exception\InvalidArgumentException( + "'plugins.{$k}' needs to be an array" + ); + } + $pluginName = $k; + $pluginOptions = $v; + } elseif (is_array($v)) { + if (!isset($v['name'])) { + throw new Exception\InvalidArgumentException("Invalid plugins[{$k}] or missing plugins[{$k}].name"); + } + $pluginName = (string) $v['name']; + + if (isset($v['options'])) { + $pluginOptions = $v['options']; + } else { + $pluginOptions = array(); + } + + if (isset($v['priority'])) { + $pluginPrio = $v['priority']; + } + } else { + $pluginName = $v; + $pluginOptions = array(); + } + + $plugin = static::pluginFactory($pluginName, $pluginOptions); + $adapter->addPlugin($plugin, $pluginPrio); + } + } + + return $adapter; + } + + /** + * Instantiate a storage adapter + * + * @param string|Storage\StorageInterface $adapterName + * @param array|Traversable|Storage\Adapter\AdapterOptions $options + * @return Storage\StorageInterface + * @throws Exception\RuntimeException + */ + public static function adapterFactory($adapterName, $options = array()) + { + if ($adapterName instanceof Storage\StorageInterface) { + // $adapterName is already an adapter object + $adapter = $adapterName; + } else { + $adapter = static::getAdapterPluginManager()->get($adapterName); + } + + if ($options) { + $adapter->setOptions($options); + } + + return $adapter; + } + + /** + * Get the adapter plugin manager + * + * @return Storage\AdapterPluginManager + */ + public static function getAdapterPluginManager() + { + if (static::$adapters === null) { + static::$adapters = new Storage\AdapterPluginManager(); + } + return static::$adapters; + } + + /** + * Change the adapter plugin manager + * + * @param Storage\AdapterPluginManager $adapters + * @return void + */ + public static function setAdapterPluginManager(Storage\AdapterPluginManager $adapters) + { + static::$adapters = $adapters; + } + + /** + * Resets the internal adapter plugin manager + * + * @return void + */ + public static function resetAdapterPluginManager() + { + static::$adapters = null; + } + + /** + * Instantiate a storage plugin + * + * @param string|Storage\Plugin\PluginInterface $pluginName + * @param array|Traversable|Storage\Plugin\PluginOptions $options + * @return Storage\Plugin\PluginInterface + * @throws Exception\RuntimeException + */ + public static function pluginFactory($pluginName, $options = array()) + { + if ($pluginName instanceof Storage\Plugin\PluginInterface) { + // $pluginName is already an plugin object + $plugin = $pluginName; + } else { + $plugin = static::getPluginManager()->get($pluginName); + } + + if (!$options instanceof Storage\Plugin\PluginOptions) { + $options = new Storage\Plugin\PluginOptions($options); + } + + if ($options) { + $plugin->setOptions($options); + } + + return $plugin; + } + + /** + * Get the plugin manager + * + * @return Storage\PluginManager + */ + public static function getPluginManager() + { + if (static::$plugins === null) { + static::$plugins = new Storage\PluginManager(); + } + return static::$plugins; + } + + /** + * Change the plugin manager + * + * @param Storage\PluginManager $plugins + * @return void + */ + public static function setPluginManager(Storage\PluginManager $plugins) + { + static::$plugins = $plugins; + } + + /** + * Resets the internal plugin manager + * + * @return void + */ + public static function resetPluginManager() + { + static::$plugins = null; + } +} diff --git a/test/Pattern/CallbackCacheTest.php b/test/Pattern/CallbackCacheTest.php new file mode 100644 index 000000000..7cd8fbf81 --- /dev/null +++ b/test/Pattern/CallbackCacheTest.php @@ -0,0 +1,168 @@ +_storage = new Cache\Storage\Adapter\Memory(array( + 'memory_limit' => 0 + )); + $this->_options = new Cache\Pattern\PatternOptions(array( + 'storage' => $this->_storage, + )); + $this->_pattern = new Cache\Pattern\CallbackCache(); + $this->_pattern->setOptions($this->_options); + + parent::setUp(); + } + + public function tearDown() + { + parent::tearDown(); + } + + public function testCallEnabledCacheOutputByDefault() + { + $this->_testCall( + __NAMESPACE__ . '\TestCallbackCache::bar', + array('testCallEnabledCacheOutputByDefault', 'arg2') + ); + } + + public function testCallDisabledCacheOutput() + { + $options = $this->_pattern->getOptions(); + $options->setCacheOutput(false); + $this->_testCall( + __NAMESPACE__ . '\TestCallbackCache::bar', + array('testCallDisabledCacheOutput', 'arg2') + ); + } + + public function testMagicFunctionCall() + { + $this->_testCall( + __NAMESPACE__ . '\bar', + array('testMagicFunctionCall', 'arg2') + ); + } + + public function testGenerateKey() + { + $callback = __NAMESPACE__ . '\TestCallbackCache::emptyMethod'; + $args = array('arg1', 2, 3.33, null); + + $generatedKey = $this->_pattern->generateKey($callback, $args); + $usedKey = null; + $this->_options->getStorage()->getEventManager()->attach('setItem.pre', function ($event) use (&$usedKey) { + $params = $event->getParams(); + $usedKey = $params['key']; + }); + + $this->_pattern->call($callback, $args); + $this->assertEquals($generatedKey, $usedKey); + } + + public function testCallInvalidCallbackException() + { + $this->setExpectedException('Zend\Cache\Exception\InvalidArgumentException'); + $this->_pattern->call(1); + } + + public function testCallUnknownCallbackException() + { + $this->setExpectedException('Zend\Cache\Exception\InvalidArgumentException'); + $this->_pattern->call('notExiststingFunction'); + } + + /** + * Running tests calling ZendTest\Cache\Pattern\TestCallbackCache::bar + * using different callbacks resulting in this method call + */ + protected function _testCall($callback, array $args) + { + $returnSpec = 'foobar_return(' . implode(', ', $args) . ') : '; + $outputSpec = 'foobar_output(' . implode(', ', $args) . ') : '; + + // first call - not cached + $firstCounter = TestCallbackCache::$fooCounter + 1; + + ob_start(); + ob_implicit_flush(false); + $return = $this->_pattern->call($callback, $args); + $data = ob_get_clean(); + + $this->assertEquals($returnSpec . $firstCounter, $return); + $this->assertEquals($outputSpec . $firstCounter, $data); + + // second call - cached + ob_start(); + ob_implicit_flush(false); + $return = $this->_pattern->call($callback, $args); + $data = ob_get_clean(); + + $this->assertEquals($returnSpec . $firstCounter, $return); + $options = $this->_pattern->getOptions(); + if ($options->getCacheOutput()) { + $this->assertEquals($outputSpec . $firstCounter, $data); + } else { + $this->assertEquals('', $data); + } + } +} diff --git a/test/Pattern/CaptureCacheTest.php b/test/Pattern/CaptureCacheTest.php new file mode 100644 index 000000000..994f54de2 --- /dev/null +++ b/test/Pattern/CaptureCacheTest.php @@ -0,0 +1,132 @@ +_umask = umask(); + + $this->_tmpCacheDir = @tempnam(sys_get_temp_dir(), 'zend_cache_test_'); + if (!$this->_tmpCacheDir) { + $err = error_get_last(); + $this->fail("Can't create temporary cache directory-file: {$err['message']}"); + } elseif (!@unlink($this->_tmpCacheDir)) { + $err = error_get_last(); + $this->fail("Can't remove temporary cache directory-file: {$err['message']}"); + } elseif (!@mkdir($this->_tmpCacheDir, 0777)) { + $err = error_get_last(); + $this->fail("Can't create temporary cache directory: {$err['message']}"); + } + + $this->_options = new Cache\Pattern\PatternOptions(array( + 'public_dir' => $this->_tmpCacheDir + )); + $this->_pattern = new Cache\Pattern\CaptureCache(); + $this->_pattern->setOptions($this->_options); + + parent::setUp(); + } + + public function tearDown() + { + $this->_removeRecursive($this->_tmpCacheDir); + + if ($this->_umask != umask()) { + umask($this->_umask); + $this->fail("Umask wasn't reset"); + } + + parent::tearDown(); + } + + protected function _removeRecursive($dir) + { + if (file_exists($dir)) { + $dirIt = new \DirectoryIterator($dir); + foreach ($dirIt as $entry) { + $fname = $entry->getFilename(); + if ($fname == '.' || $fname == '..') { + continue; + } + + if ($entry->isFile()) { + unlink($entry->getPathname()); + } else { + $this->_removeRecursive($entry->getPathname()); + } + } + + rmdir($dir); + } + } + + public function testSetThrowsLogicExceptionOnMissingPublicDir() + { + $captureCache = new Cache\Pattern\CaptureCache(); + + $this->setExpectedException('Zend\Cache\Exception\LogicException'); + $captureCache->set('content', '/pageId'); + } + + public function testSetWithNormalPageId() + { + $this->_pattern->set('content', '/dir1/dir2/file'); + $this->assertTrue(file_exists($this->_tmpCacheDir . '/dir1/dir2/file')); + $this->assertSame(file_get_contents($this->_tmpCacheDir . '/dir1/dir2/file'), 'content'); + } + + public function testSetWithIndexFilename() + { + $this->_options->setIndexFilename('test.html'); + + $this->_pattern->set('content', '/dir1/dir2/'); + $this->assertTrue(file_exists($this->_tmpCacheDir . '/dir1/dir2/test.html')); + $this->assertSame(file_get_contents($this->_tmpCacheDir . '/dir1/dir2/test.html'), 'content'); + } + + public function testGetThrowsLogicExceptionOnMissingPublicDir() + { + $captureCache = new Cache\Pattern\CaptureCache(); + + $this->setExpectedException('Zend\Cache\Exception\LogicException'); + $captureCache->get('/pageId'); + } + + public function testHasThrowsLogicExceptionOnMissingPublicDir() + { + $captureCache = new Cache\Pattern\CaptureCache(); + + $this->setExpectedException('Zend\Cache\Exception\LogicException'); + $captureCache->has('/pageId'); + } + + public function testRemoveThrowsLogicExceptionOnMissingPublicDir() + { + $captureCache = new Cache\Pattern\CaptureCache(); + + $this->setExpectedException('Zend\Cache\Exception\LogicException'); + $captureCache->remove('/pageId'); + } +} diff --git a/test/Pattern/ClassCacheTest.php b/test/Pattern/ClassCacheTest.php new file mode 100644 index 000000000..bf70833ec --- /dev/null +++ b/test/Pattern/ClassCacheTest.php @@ -0,0 +1,133 @@ +_storage = new Cache\Storage\Adapter\Memory(array( + 'memory_limit' => 0 + )); + $this->_options = new Cache\Pattern\PatternOptions(array( + 'class' => __NAMESPACE__ . '\TestClassCache', + 'storage' => $this->_storage, + )); + $this->_pattern = new Cache\Pattern\ClassCache(); + $this->_pattern->setOptions($this->_options); + + parent::setUp(); + } + + public function tearDown() + { + parent::tearDown(); + } + + public function testCallEnabledCacheOutputByDefault() + { + $this->_testCall( + 'bar', + array('testCallEnabledCacheOutputByDefault', 'arg2') + ); + } + + public function testCallDisabledCacheOutput() + { + $this->_options->setCacheOutput(false); + $this->_testCall( + 'bar', + array('testCallDisabledCacheOutput', 'arg2') + ); + } + + public function testGenerateKey() + { + $args = array('arg1', 2, 3.33, null); + + $generatedKey = $this->_pattern->generateKey('emptyMethod', $args); + $usedKey = null; + $this->_options->getStorage()->getEventManager()->attach('setItem.pre', function ($event) use (&$usedKey) { + $params = $event->getParams(); + $usedKey = $params['key']; + }); + + $this->_pattern->call('emptyMethod', $args); + $this->assertEquals($generatedKey, $usedKey); + } + + protected function _testCall($method, array $args) + { + $returnSpec = 'foobar_return(' . implode(', ', $args) . ') : '; + $outputSpec = 'foobar_output(' . implode(', ', $args) . ') : '; + + // first call - not cached + $firstCounter = TestClassCache::$fooCounter + 1; + + ob_start(); + ob_implicit_flush(false); + $return = call_user_func_array(array($this->_pattern, $method), $args); + $data = ob_get_clean(); + + $this->assertEquals($returnSpec . $firstCounter, $return); + $this->assertEquals($outputSpec . $firstCounter, $data); + + // second call - cached + ob_start(); + ob_implicit_flush(false); + $return = call_user_func_array(array($this->_pattern, $method), $args); + $data = ob_get_clean(); + + $this->assertEquals($returnSpec . $firstCounter, $return); + if ($this->_options->getCacheOutput()) { + $this->assertEquals($outputSpec . $firstCounter, $data); + } else { + $this->assertEquals('', $data); + } + } +} diff --git a/test/Pattern/CommonPatternTest.php b/test/Pattern/CommonPatternTest.php new file mode 100644 index 000000000..693fe44ec --- /dev/null +++ b/test/Pattern/CommonPatternTest.php @@ -0,0 +1,55 @@ +assertInstanceOf( + 'Zend\Cache\Pattern\PatternInterface', + $this->_pattern, + 'Internal pattern instance is needed for tests' + ); + } + + public function tearDown() + { + unset($this->_pattern); + } + + public function testOptionNamesValid() + { + $options = $this->_pattern->getOptions(); + $this->assertInstanceOf('Zend\Cache\Pattern\PatternOptions', $options); + } + + public function testOptionsGetAndSetDefault() + { + $options = $this->_pattern->getOptions(); + $this->_pattern->setOptions($options); + $this->assertSame($options, $this->_pattern->getOptions()); + } +} diff --git a/test/Pattern/ObjectCacheTest.php b/test/Pattern/ObjectCacheTest.php new file mode 100644 index 000000000..ca07bb976 --- /dev/null +++ b/test/Pattern/ObjectCacheTest.php @@ -0,0 +1,173 @@ +_storage = new Cache\Storage\Adapter\Memory(array( + 'memory_limit' => 0 + )); + $this->_options = new Cache\Pattern\PatternOptions(array( + 'object' => new $class(), + 'storage' => $this->_storage, + )); + $this->_pattern = new Cache\Pattern\ObjectCache(); + $this->_pattern->setOptions($this->_options); + + parent::setUp(); + } + + public function tearDown() + { + parent::tearDown(); + } + + public function testCallEnabledCacheOutputByDefault() + { + $this->_testCall( + 'bar', + array('testCallEnabledCacheOutputByDefault', 'arg2') + ); + } + + public function testCallDisabledCacheOutput() + { + $this->_options->setCacheOutput(false); + $this->_testCall( + 'bar', + array('testCallDisabledCacheOutput', 'arg2') + ); + } + + public function testCallInvoke() + { + $this->_options->setCacheOutput(false); + $this->_testCall('__invoke', array('arg1', 'arg2')); + } + + public function testGenerateKey() + { + $args = array('arg1', 2, 3.33, null); + + $generatedKey = $this->_pattern->generateKey('emptyMethod', $args); + $usedKey = null; + $this->_options->getStorage()->getEventManager()->attach('setItem.pre', function ($event) use (&$usedKey) { + $params = $event->getParams(); + $usedKey = $params['key']; + }); + + $this->_pattern->call('emptyMethod', $args); + $this->assertEquals($generatedKey, $usedKey); + } + + public function testSetProperty() + { + $this->_pattern->property = 'testSetProperty'; + $this->assertEquals('testSetProperty', $this->_options->getObject()->property); + } + + public function testGetProperty() + { + $this->assertEquals($this->_options->getObject()->property, $this->_pattern->property); + } + + public function testIssetProperty() + { + $this->assertTrue(isset($this->_pattern->property)); + $this->assertFalse(isset($this->_pattern->unknownProperty)); + } + + public function testUnsetProperty() + { + unset($this->_pattern->property); + $this->assertFalse(isset($this->_pattern->property)); + } + + protected function _testCall($method, array $args) + { + $returnSpec = 'foobar_return(' . implode(', ', $args) . ') : '; + $outputSpec = 'foobar_output(' . implode(', ', $args) . ') : '; + $callback = array($this->_pattern, $method); + + // first call - not cached + $firstCounter = TestObjectCache::$fooCounter + 1; + + ob_start(); + ob_implicit_flush(false); + $return = call_user_func_array($callback, $args); + $data = ob_get_contents(); + ob_end_clean(); + + $this->assertEquals($returnSpec . $firstCounter, $return); + $this->assertEquals($outputSpec . $firstCounter, $data); + + // second call - cached + ob_start(); + ob_implicit_flush(false); + $return = call_user_func_array($callback, $args); + $data = ob_get_contents(); + ob_end_clean(); + + $this->assertEquals($returnSpec . $firstCounter, $return); + if ($this->_options->getCacheOutput()) { + $this->assertEquals($outputSpec . $firstCounter, $data); + } else { + $this->assertEquals('', $data); + } + } +} diff --git a/test/Pattern/OutputCacheTest.php b/test/Pattern/OutputCacheTest.php new file mode 100644 index 000000000..b59168a4c --- /dev/null +++ b/test/Pattern/OutputCacheTest.php @@ -0,0 +1,104 @@ +_storage = new Cache\Storage\Adapter\Memory(array( + 'memory_limit' => 0 + )); + $this->_options = new Cache\Pattern\PatternOptions(array( + 'storage' => $this->_storage, + )); + $this->_pattern = new Cache\Pattern\OutputCache(); + $this->_pattern->setOptions($this->_options); + + // used to reset the level on tearDown + $this->_obLevel = ob_get_level(); + + parent::setUp(); + } + + public function tearDown() + { + if ($this->_obLevel > ob_get_Level()) { + for ($i = ob_get_level(); $i < $this->_obLevel; $i++) { + ob_start(); + } + $this->fail("Nesting level of output buffering to often ended"); + } elseif ($this->_obLevel < ob_get_level()) { + for ($i = ob_get_level(); $i > $this->_obLevel; $i--) { + ob_end_clean(); + } + $this->fail("Nesting level of output buffering not well restored"); + } + + parent::tearDown(); + } + + public function testStartEndCacheMiss() + { + $output = 'foobar'; + $key = 'testStartEndCacheMiss'; + + ob_start(); + $this->assertFalse($this->_pattern->start($key)); + echo $output; + $this->assertTrue($this->_pattern->end()); + $data = ob_get_clean(); + + $this->assertEquals($output, $data); + } + + public function testStartEndCacheHit() + { + $output = 'foobar'; + $key = 'testStartEndCacheHit'; + + // fill cache + $this->_pattern->getOptions()->getStorage()->setItem($key, $output); + + ob_start(); + $this->assertTrue($this->_pattern->start($key)); + $data = ob_get_clean(); + + $this->assertSame($output, $data); + } + + public function testThrowMissingKeyException() + { + $this->setExpectedException('Zend\Cache\Exception\MissingKeyException'); + $this->_pattern->start(''); // empty key + } +} diff --git a/test/PatternFactoryTest.php b/test/PatternFactoryTest.php new file mode 100644 index 000000000..79608a46a --- /dev/null +++ b/test/PatternFactoryTest.php @@ -0,0 +1,57 @@ +assertInstanceOf('Zend\Cache\PatternPluginManager', $plugins); + } + + public function testChangePluginManager() + { + $plugins = new Cache\PatternPluginManager(); + Cache\PatternFactory::setPluginManager($plugins); + $this->assertSame($plugins, Cache\PatternFactory::getPluginManager()); + } + + public function testFactory() + { + $pattern1 = Cache\PatternFactory::factory('capture'); + $this->assertInstanceOf('Zend\Cache\Pattern\CaptureCache', $pattern1); + + $pattern2 = Cache\PatternFactory::factory('capture'); + $this->assertInstanceOf('Zend\Cache\Pattern\CaptureCache', $pattern2); + + $this->assertNotSame($pattern1, $pattern2); + } +} diff --git a/test/Storage/Adapter/AbstractAdapterTest.php b/test/Storage/Adapter/AbstractAdapterTest.php new file mode 100644 index 000000000..90e106982 --- /dev/null +++ b/test/Storage/Adapter/AbstractAdapterTest.php @@ -0,0 +1,763 @@ +_options = new Cache\Storage\Adapter\AdapterOptions(); + } + + public function testGetOptions() + { + $this->_storage = $this->getMockForAbstractAdapter(); + + $options = $this->_storage->getOptions(); + $this->assertInstanceOf('Zend\Cache\Storage\Adapter\AdapterOptions', $options); + $this->assertInternalType('boolean', $options->getWritable()); + $this->assertInternalType('boolean', $options->getReadable()); + $this->assertInternalType('integer', $options->getTtl()); + $this->assertInternalType('string', $options->getNamespace()); + $this->assertInternalType('string', $options->getKeyPattern()); + } + + public function testSetWritable() + { + $this->_options->setWritable(true); + $this->assertTrue($this->_options->getWritable()); + + $this->_options->setWritable(false); + $this->assertFalse($this->_options->getWritable()); + } + + public function testSetReadable() + { + $this->_options->setReadable(true); + $this->assertTrue($this->_options->getReadable()); + + $this->_options->setReadable(false); + $this->assertFalse($this->_options->getReadable()); + } + + public function testSetTtl() + { + $this->_options->setTtl('123'); + $this->assertSame(123, $this->_options->getTtl()); + } + + public function testSetTtlThrowsInvalidArgumentException() + { + $this->setExpectedException('Zend\Cache\Exception\InvalidArgumentException'); + $this->_options->setTtl(-1); + } + + public function testGetDefaultNamespaceNotEmpty() + { + $ns = $this->_options->getNamespace(); + $this->assertNotEmpty($ns); + } + + public function testSetNamespace() + { + $this->_options->setNamespace('new_namespace'); + $this->assertSame('new_namespace', $this->_options->getNamespace()); + } + + public function testSetNamespace0() + { + $this->_options->setNamespace('0'); + $this->assertSame('0', $this->_options->getNamespace()); + } + + public function testSetKeyPattern() + { + $this->_options->setKeyPattern('/^[key]+$/Di'); + $this->assertEquals('/^[key]+$/Di', $this->_options->getKeyPattern()); + } + + public function testUnsetKeyPattern() + { + $this->_options->setKeyPattern(null); + $this->assertSame('', $this->_options->getKeyPattern()); + } + + public function testSetKeyPatternThrowsExceptionOnInvalidPattern() + { + $this->setExpectedException('Zend\Cache\Exception\InvalidArgumentException'); + $this->_options->setKeyPattern('#'); + } + + public function testPluginRegistry() + { + $this->_storage = $this->getMockForAbstractAdapter(); + + $plugin = new \ZendTest\Cache\Storage\TestAsset\MockPlugin(); + + // no plugin registered + $this->assertFalse($this->_storage->hasPlugin($plugin)); + $this->assertEquals(0, count($this->_storage->getPluginRegistry())); + $this->assertEquals(0, count($plugin->getHandles())); + + // register a plugin + $this->assertSame($this->_storage, $this->_storage->addPlugin($plugin)); + $this->assertTrue($this->_storage->hasPlugin($plugin)); + $this->assertEquals(1, count($this->_storage->getPluginRegistry())); + + // test registered callback handles + $handles = $plugin->getHandles(); + $this->assertEquals(1, count($handles)); + $this->assertEquals(count($plugin->getEventCallbacks()), count(current($handles))); + + // test unregister a plugin + $this->assertSame($this->_storage, $this->_storage->removePlugin($plugin)); + $this->assertFalse($this->_storage->hasPlugin($plugin)); + $this->assertEquals(0, count($this->_storage->getPluginRegistry())); + $this->assertEquals(0, count($plugin->getHandles())); + } + + public function testInternalTriggerPre() + { + $this->_storage = $this->getMockForAbstractAdapter(); + + $plugin = new \ZendTest\Cache\Storage\TestAsset\MockPlugin(); + $this->_storage->addPlugin($plugin); + + $params = new \ArrayObject(array( + 'key' => 'key1', + 'value' => 'value1' + )); + + // call protected method + $method = new \ReflectionMethod(get_class($this->_storage), 'triggerPre'); + $method->setAccessible(true); + $rsCollection = $method->invoke($this->_storage, 'setItem', $params); + $this->assertInstanceOf('Zend\EventManager\ResponseCollection', $rsCollection); + + // test called event + $calledEvents = $plugin->getCalledEvents(); + $this->assertEquals(1, count($calledEvents)); + + $event = current($calledEvents); + $this->assertInstanceOf('Zend\Cache\Storage\Event', $event); + $this->assertEquals('setItem.pre', $event->getName()); + $this->assertSame($this->_storage, $event->getTarget()); + $this->assertSame($params, $event->getParams()); + } + + public function testInternalTriggerPost() + { + $this->_storage = $this->getMockForAbstractAdapter(); + + $plugin = new \ZendTest\Cache\Storage\TestAsset\MockPlugin(); + $this->_storage->addPlugin($plugin); + + $params = new \ArrayObject(array( + 'key' => 'key1', + 'value' => 'value1' + )); + $result = true; + + // call protected method + $method = new \ReflectionMethod(get_class($this->_storage), 'triggerPost'); + $method->setAccessible(true); + $result = $method->invokeArgs($this->_storage, array('setItem', $params, &$result)); + + // test called event + $calledEvents = $plugin->getCalledEvents(); + $this->assertEquals(1, count($calledEvents)); + $event = current($calledEvents); + + // return value of triggerPost and the called event should be the same + $this->assertSame($result, $event->getResult()); + + $this->assertInstanceOf('Zend\Cache\Storage\PostEvent', $event); + $this->assertEquals('setItem.post', $event->getName()); + $this->assertSame($this->_storage, $event->getTarget()); + $this->assertSame($params, $event->getParams()); + $this->assertSame($result, $event->getResult()); + } + + public function testInternalTriggerExceptionThrowRuntimeException() + { + $this->_storage = $this->getMockForAbstractAdapter(); + + $plugin = new \ZendTest\Cache\Storage\TestAsset\MockPlugin(); + $this->_storage->addPlugin($plugin); + + $result = null; + $params = new \ArrayObject(array( + 'key' => 'key1', + 'value' => 'value1' + )); + + // call protected method + $method = new \ReflectionMethod(get_class($this->_storage), 'triggerException'); + $method->setAccessible(true); + + $this->setExpectedException('Zend\Cache\Exception\RuntimeException', 'test'); + $method->invokeArgs($this->_storage, array('setItem', $params, & $result, new Exception\RuntimeException('test'))); + } + + public function testGetItemCallsInternalGetItem() + { + $this->_storage = $this->getMockForAbstractAdapter(array('internalGetItem')); + + $key = 'key1'; + $result = 'value1'; + + $this->_storage + ->expects($this->once()) + ->method('internalGetItem') + ->with($this->equalTo($key)) + ->will($this->returnValue($result)); + + $rs = $this->_storage->getItem($key); + $this->assertEquals($result, $rs); + } + + public function testGetItemsCallsInternalGetItems() + { + $this->_storage = $this->getMockForAbstractAdapter(array('internalGetItems')); + + $keys = array('key1', 'key2'); + $result = array('key2' => 'value2'); + + $this->_storage + ->expects($this->once()) + ->method('internalGetItems') + ->with($this->equalTo($keys)) + ->will($this->returnValue($result)); + + $rs = $this->_storage->getItems($keys); + $this->assertEquals($result, $rs); + } + + public function testInternalGetItemsCallsInternalGetItemForEachKey() + { + $this->markTestSkipped( + "This test doesn't work because of an issue with PHPUnit: " + . 'https://github.com/sebastianbergmann/phpunit-mock-objects/issues/81' + ); + + $this->_storage = $this->getMockForAbstractAdapter(array('internalGetItem')); + + $items = array('key1' => 'value1', 'notFound' => false, 'key2' => 'value2'); + $result = array('key1' => 'value1', 'key2' => 'value2'); + + $i = 0; // method call counter + foreach ($items as $k => $v) { + $this->_storage->expects($this->at($i++)) + ->method('internalGetItem') + ->with( + $this->equalTo($k), + $this->equalTo(null), + $this->equalTo(null) + ) + ->will($this->returnCallback(function ($k, & $success, & $casToken) use ($items) { + if ($items[$k]) { + $success = true; + return $items[$k]; + } else { + $success = false; + return null; + } + })); + } + + $rs = $this->_storage->getItems(array_keys($items), $options); + $this->assertEquals($result, $rs); + } + + public function testHasItemCallsInternalHasItem() + { + $this->_storage = $this->getMockForAbstractAdapter(array('internalHasItem')); + + $key = 'key1'; + $result = true; + + $this->_storage + ->expects($this->once()) + ->method('internalHasItem') + ->with($this->equalTo($key)) + ->will($this->returnValue($result)); + + $rs = $this->_storage->hasItem($key); + $this->assertSame($result, $rs); + } + + public function testHasItemsCallsInternalHasItems() + { + $this->_storage = $this->getMockForAbstractAdapter(array('internalHasItems')); + + $keys = array('key1', 'key2'); + $result = array('key2'); + + $this->_storage + ->expects($this->once()) + ->method('internalHasItems') + ->with($this->equalTo($keys)) + ->will($this->returnValue($result)); + + $rs = $this->_storage->hasItems($keys); + $this->assertEquals($result, $rs); + } + + public function testInternalHasItemsCallsInternalHasItem() + { + $this->_storage = $this->getMockForAbstractAdapter(array('internalHasItem')); + + $items = array('key1' => true, 'key2' => false); + $result = array('key1'); + + $i = 0; // method call counter + foreach ($items as $k => $v) { + $this->_storage + ->expects($this->at($i++)) + ->method('internalHasItem') + ->with($this->equalTo($k)) + ->will($this->returnValue($v)); + } + + $rs = $this->_storage->hasItems(array_keys($items)); + $this->assertEquals($result, $rs); + } + + public function testGetMetadataCallsInternalGetMetadata() + { + $this->_storage = $this->getMockForAbstractAdapter(array('internalGetMetadata')); + + $key = 'key1'; + $result = array(); + + $this->_storage + ->expects($this->once()) + ->method('internalGetMetadata') + ->with($this->equalTo($key)) + ->will($this->returnValue($result)); + + $rs = $this->_storage->getMetadata($key); + $this->assertSame($result, $rs); + } + +/* + public function testGetMetadatas() + { + $options = array('ttl' => 123); + $items = array( + 'key1' => array('meta1' => 1), + 'dKey1' => false, + 'key2' => array('meta2' => 2), + ); + + $i = 0; + foreach ($items as $k => $v) { + $this->storage->expects($this->at($i++)) + ->method('getMetadata') + ->with($this->equalTo($k), $this->equalTo($options)) + ->will($this->returnValue($v)); + } + + $rs = $this->storage->getMetadatas(array_keys($items), $options); + + // remove missing items from array to test + $expected = $items; + foreach ($expected as $key => $value) { + if (false === $value) { + unset($expected[$key]); + } + } + + $this->assertEquals($expected, $rs); + } + + public function testSetItems() + { + $options = array('ttl' => 123); + $items = array( + 'key1' => 'value1', + 'key2' => 'value2' + ); + + $this->storage->expects($this->exactly(count($items))) + ->method('setItem') + ->with($this->stringContains('key'), $this->stringContains('value'), $this->equalTo($options)) + ->will($this->returnValue(true)); + + $this->assertTrue($this->storage->setItems($items, $options)); + } + + public function testSetItemsFail() + { + $options = array('ttl' => 123); + $items = array( + 'key1' => 'value1', + 'key2' => 'value2', + 'key3' => 'value3', + ); + + $this->storage->expects($this->exactly(count($items))) + ->method('setItem') + ->with($this->stringContains('key'), $this->stringContains('value'), $this->equalTo($options)) + ->will($this->returnValue(false)); + + $this->assertFalse($this->storage->setItems($items, $options)); + } + + public function testAddItems() + { + $options = array('ttl' => 123); + $items = array( + 'key1' => 'value1', + 'key2' => 'value2' + ); + + // add -> has -> get -> set + $this->storage->expects($this->exactly(count($items))) + ->method('getItem') + ->with($this->stringContains('key'), $this->equalTo($options)) + ->will($this->returnValue(false)); + $this->storage->expects($this->exactly(count($items))) + ->method('setItem') + ->with($this->stringContains('key'), $this->stringContains('value'), $this->equalTo($options)) + ->will($this->returnValue(true)); + + $this->assertTrue($this->storage->addItems($items, $options)); + } + + public function testAddItemsFail() + { + $options = array('ttl' => 123); + $items = array( + 'key1' => 'value1', + 'key2' => 'value2', + 'key3' => 'value3', + ); + + // add -> has -> get -> set + $this->storage->expects($this->exactly(count($items))) + ->method('getItem') + ->with($this->stringContains('key'), $this->equalTo($options)) + ->will($this->returnValue(false)); + $this->storage->expects($this->exactly(count($items))) + ->method('setItem') + ->with($this->stringContains('key'), $this->stringContains('value'), $this->equalTo($options)) + ->will($this->returnValue(false)); + + $this->assertFalse($this->storage->addItems($items, $options)); + } + + public function testReplaceItems() + { + $options = array('ttl' => 123); + $items = array( + 'key1' => 'value1', + 'key2' => 'value2' + ); + + // replace -> has -> get -> set + $this->storage->expects($this->exactly(count($items))) + ->method('getItem') + ->with($this->stringContains('key'), $this->equalTo($options)) + ->will($this->returnValue(true)); + $this->storage->expects($this->exactly(count($items))) + ->method('setItem') + ->with($this->stringContains('key'), $this->stringContains('value'), $this->equalTo($options)) + ->will($this->returnValue(true)); + + $this->assertTrue($this->storage->replaceItems($items, $options)); + } + + public function testReplaceItemsFail() + { + $options = array('ttl' => 123); + $items = array( + 'key1' => 'value1', + 'key2' => 'value2', + 'key3' => 'value3', + ); + + // replace -> has -> get -> set + $this->storage->expects($this->exactly(count($items))) + ->method('getItem') + ->with($this->stringContains('key'), $this->equalTo($options)) + ->will($this->returnValue(true)); + $this->storage->expects($this->exactly(count($items))) + ->method('setItem') + ->with($this->stringContains('key'), $this->stringContains('value'), $this->equalTo($options)) + ->will($this->returnValue(false)); + + $this->assertFalse($this->storage->replaceItems($items, $options)); + } + + public function testRemoveItems() + { + $options = array('ttl' => 123); + $keys = array('key1', 'key2'); + + foreach ($keys as $i => $key) { + $this->storage->expects($this->at($i)) + ->method('removeItem') + ->with($this->equalTo($key), $this->equalTo($options)) + ->will($this->returnValue(true)); + } + + $this->assertTrue($this->storage->removeItems($keys, $options)); + } + + public function testRemoveItemsFail() + { + $options = array('ttl' => 123); + $items = array('key1', 'key2', 'key3'); + + $this->storage->expects($this->at(0)) + ->method('removeItem') + ->with($this->equalTo('key1'), $this->equalTo($options)) + ->will($this->returnValue(true)); + $this->storage->expects($this->at(1)) + ->method('removeItem') + ->with($this->equalTo('key2'), $this->equalTo($options)) + ->will($this->returnValue(false)); // -> fail + $this->storage->expects($this->at(2)) + ->method('removeItem') + ->with($this->equalTo('key3'), $this->equalTo($options)) + ->will($this->returnValue(true)); + + $this->assertFalse($this->storage->removeItems($items, $options)); + } +*/ + // TODO: incrementItem[s] + decrementItem[s] + // TODO: touchItem[s] + + public function testPreEventsCanChangeArguments() + { + // getItem(s) + $this->checkPreEventCanChangeArguments('getItem', array( + 'key' => 'key' + ), array( + 'key' => 'changedKey', + )); + + $this->checkPreEventCanChangeArguments('getItems', array( + 'keys' => array('key') + ), array( + 'keys' => array('changedKey'), + )); + + // hasItem(s) + $this->checkPreEventCanChangeArguments('hasItem', array( + 'key' => 'key' + ), array( + 'key' => 'changedKey', + )); + + $this->checkPreEventCanChangeArguments('hasItems', array( + 'keys' => array('key'), + ), array( + 'keys' => array('changedKey'), + )); + + // getMetadata(s) + $this->checkPreEventCanChangeArguments('getMetadata', array( + 'key' => 'key' + ), array( + 'key' => 'changedKey', + )); + + $this->checkPreEventCanChangeArguments('getMetadatas', array( + 'keys' => array('key'), + ), array( + 'keys' => array('changedKey'), + )); + + // setItem(s) + $this->checkPreEventCanChangeArguments('setItem', array( + 'key' => 'key', + 'value' => 'value', + ), array( + 'key' => 'changedKey', + 'value' => 'changedValue', + )); + + $this->checkPreEventCanChangeArguments('setItems', array( + 'keyValuePairs' => array('key' => 'value'), + ), array( + 'keyValuePairs' => array('changedKey' => 'changedValue'), + )); + + // addItem(s) + $this->checkPreEventCanChangeArguments('addItem', array( + 'key' => 'key', + 'value' => 'value', + ), array( + 'key' => 'changedKey', + 'value' => 'changedValue', + )); + + $this->checkPreEventCanChangeArguments('addItems', array( + 'keyValuePairs' => array('key' => 'value'), + ), array( + 'keyValuePairs' => array('changedKey' => 'changedValue'), + )); + + // replaceItem(s) + $this->checkPreEventCanChangeArguments('replaceItem', array( + 'key' => 'key', + 'value' => 'value', + ), array( + 'key' => 'changedKey', + 'value' => 'changedValue', + )); + + $this->checkPreEventCanChangeArguments('replaceItems', array( + 'keyValuePairs' => array('key' => 'value'), + ), array( + 'keyValuePairs' => array('changedKey' => 'changedValue'), + )); + + // CAS + $this->checkPreEventCanChangeArguments('checkAndSetItem', array( + 'token' => 'token', + 'key' => 'key', + 'value' => 'value', + ), array( + 'token' => 'changedToken', + 'key' => 'changedKey', + 'value' => 'changedValue', + )); + + // touchItem(s) + $this->checkPreEventCanChangeArguments('touchItem', array( + 'key' => 'key', + ), array( + 'key' => 'changedKey', + )); + + $this->checkPreEventCanChangeArguments('touchItems', array( + 'keys' => array('key'), + ), array( + 'keys' => array('changedKey'), + )); + + // removeItem(s) + $this->checkPreEventCanChangeArguments('removeItem', array( + 'key' => 'key', + ), array( + 'key' => 'changedKey', + )); + + $this->checkPreEventCanChangeArguments('removeItems', array( + 'keys' => array('key'), + ), array( + 'keys' => array('changedKey'), + )); + + // incrementItem(s) + $this->checkPreEventCanChangeArguments('incrementItem', array( + 'key' => 'key', + 'value' => 1 + ), array( + 'key' => 'changedKey', + 'value' => 2, + )); + + $this->checkPreEventCanChangeArguments('incrementItems', array( + 'keyValuePairs' => array('key' => 1), + ), array( + 'keyValuePairs' => array('changedKey' => 2), + )); + + // decrementItem(s) + $this->checkPreEventCanChangeArguments('decrementItem', array( + 'key' => 'key', + 'value' => 1 + ), array( + 'key' => 'changedKey', + 'value' => 2, + )); + + $this->checkPreEventCanChangeArguments('decrementItems', array( + 'keyValuePairs' => array('key' => 1), + ), array( + 'keyValuePairs' => array('changedKey' => 2), + )); + } + + protected function checkPreEventCanChangeArguments($method, array $args, array $expectedArgs) + { + $internalMethod = 'internal' . ucfirst($method); + $eventName = $method . '.pre'; + + // init mock + $this->_storage = $this->getMockForAbstractAdapter(array($internalMethod)); + $this->_storage->getEventManager()->attach($eventName, function ($event) use ($expectedArgs) { + $params = $event->getParams(); + foreach ($expectedArgs as $k => $v) { + $params[$k] = $v; + } + }); + + // set expected arguments of internal method call + $tmp = $this->_storage->expects($this->once())->method($internalMethod); + $equals = array(); + foreach ($expectedArgs as $v) { + $equals[] = $this->equalTo($v); + } + call_user_func_array(array($tmp, 'with'), $equals); + + // run + call_user_func_array(array($this->_storage, $method), $args); + } + + /** + * Generates a mock of the abstract storage adapter by mocking all abstract and the given methods + * Also sets the adapter options + * + * @param array $methods + * @return \Zend\Cache\Storage\Adapter\AbstractAdapter + */ + protected function getMockForAbstractAdapter(array $methods = array()) + { + $class = 'Zend\Cache\Storage\Adapter\AbstractAdapter'; + + if (!$methods) { + $adapter = $this->getMockForAbstractClass($class); + } else { + $reflection = new \ReflectionClass('Zend\Cache\Storage\Adapter\AbstractAdapter'); + foreach ($reflection->getMethods() as $method) { + if ($method->isAbstract()) { + $methods[] = $method->getName(); + } + } + $adapter = $this->getMockBuilder($class)->setMethods(array_unique($methods))->getMock(); + } + + $adapter->setOptions($this->_options); + return $adapter; + } +} diff --git a/test/Storage/Adapter/AbstractDbaTest.php b/test/Storage/Adapter/AbstractDbaTest.php new file mode 100644 index 000000000..1dae323f3 --- /dev/null +++ b/test/Storage/Adapter/AbstractDbaTest.php @@ -0,0 +1,69 @@ +fail("Expected exception Zend\Cache\Exception\ExtensionNotLoadedException"); + } catch (Cache\Exception\ExtensionNotLoadedException $e) { + $this->markTestSkipped("Missing ext/dba"); + } + } + + if (!in_array($this->handler, dba_handlers())) { + try { + new Cache\Storage\Adapter\DbaOptions(array('handler' => $this->handler)); + $this->fail("Expected exception Zend\Cache\Exception\ExtensionNotLoadedException"); + } catch (Cache\Exception\ExtensionNotLoadedException $e) { + $this->markTestSkipped("Missing ext/dba handler '{$this->handler}'"); + } + } + + $this->temporaryDbaFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('zfcache_dba_'); + $this->_options = new Cache\Storage\Adapter\DbaOptions(array( + 'pathname' => $this->temporaryDbaFile, + 'handler' => $this->handler, + )); + + $this->_storage = new Cache\Storage\Adapter\Dba(); + $this->_storage->setOptions($this->_options); + + parent::setUp(); + } + + public function tearDown() + { + $this->_storage = null; + + if (file_exists($this->temporaryDbaFile)) { + unlink($this->temporaryDbaFile); + } + + parent::tearDown(); + } +} diff --git a/test/Storage/Adapter/AbstractZendServerTest.php b/test/Storage/Adapter/AbstractZendServerTest.php new file mode 100644 index 000000000..51645dd2b --- /dev/null +++ b/test/Storage/Adapter/AbstractZendServerTest.php @@ -0,0 +1,111 @@ +_options = new AdapterOptions(); + $this->_storage = $this->getMockForAbstractClass('Zend\Cache\Storage\Adapter\AbstractZendServer'); + $this->_storage->setOptions($this->_options); + $this->_storage->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($this->_options)); + } + + public function testGetOptions() + { + $options = $this->_storage->getOptions(); + $this->assertInstanceOf('Zend\Cache\Storage\Adapter\AdapterOptions', $options); + $this->assertInternalType('boolean', $options->getWritable()); + $this->assertInternalType('boolean', $options->getReadable()); + $this->assertInternalType('integer', $options->getTtl()); + $this->assertInternalType('string', $options->getNamespace()); + $this->assertInternalType('string', $options->getKeyPattern()); + } + + public function testGetItem() + { + $this->_options->setNamespace('ns'); + + $this->_storage->expects($this->once()) + ->method('zdcFetch') + ->with($this->equalTo('ns' . AbstractZendServer::NAMESPACE_SEPARATOR . 'key')) + ->will($this->returnValue('value')); + + $this->assertEquals('value', $this->_storage->getItem('key')); + } + + public function testGetMetadata() + { + $this->_options->setNamespace('ns'); + + $this->_storage->expects($this->once()) + ->method('zdcFetch') + ->with($this->equalTo('ns' . AbstractZendServer::NAMESPACE_SEPARATOR . 'key')) + ->will($this->returnValue('value')); + + $this->assertEquals(array(), $this->_storage->getMetadata('key')); + } + + public function testHasItem() + { + $this->_options->setNamespace('ns'); + + $this->_storage->expects($this->once()) + ->method('zdcFetch') + ->with($this->equalTo('ns' . AbstractZendServer::NAMESPACE_SEPARATOR . 'key')) + ->will($this->returnValue('value')); + + $this->assertEquals(true, $this->_storage->hasItem('key')); + } + + public function testSetItem() + { + $this->_options->setTtl(10); + $this->_options->setNamespace('ns'); + + $this->_storage->expects($this->once()) + ->method('zdcStore') + ->with( + $this->equalTo('ns' . AbstractZendServer::NAMESPACE_SEPARATOR . 'key'), + $this->equalTo('value'), + $this->equalTo(10) + ) + ->will($this->returnValue(true)); + + $this->assertEquals(true, $this->_storage->setItem('key', 'value')); + } + + public function testRemoveItem() + { + $this->_options->setNamespace('ns'); + + $this->_storage->expects($this->once()) + ->method('zdcDelete') + ->with($this->equalTo('ns' . AbstractZendServer::NAMESPACE_SEPARATOR . 'key')) + ->will($this->returnValue(true)); + + $this->assertEquals(true, $this->_storage->removeItem('key')); + } +} diff --git a/test/Storage/Adapter/ApcTest.php b/test/Storage/Adapter/ApcTest.php new file mode 100644 index 000000000..25621604a --- /dev/null +++ b/test/Storage/Adapter/ApcTest.php @@ -0,0 +1,82 @@ +markTestSkipped("Skipped by TestConfiguration (TESTS_ZEND_CACHE_APC_ENABLED)"); + } + + if (version_compare('3.1.6', phpversion('apc')) > 0) { + try { + new Cache\Storage\Adapter\Apc(); + $this->fail("Expected exception Zend\Cache\Exception\ExtensionNotLoadedException"); + } catch (Cache\Exception\ExtensionNotLoadedException $e) { + $this->markTestSkipped("Missing ext/apc >= 3.1.6"); + } + } + + $enabled = (bool) ini_get('apc.enabled'); + if (PHP_SAPI == 'cli') { + $enabled = $enabled && (bool) ini_get('apc.enable_cli'); + } + + if (!$enabled) { + try { + new Cache\Storage\Adapter\Apc(); + $this->fail("Expected exception Zend\Cache\Exception\ExtensionNotLoadedException"); + } catch (Cache\Exception\ExtensionNotLoadedException $e) { + $this->markTestSkipped("ext/apc is disabled - see 'apc.enabled' and 'apc.enable_cli'"); + } + } + + // needed on test expirations + $this->iniUseRequestTime = ini_get('apc.use_request_time'); + ini_set('apc.use_request_time', 0); + + $this->_options = new Cache\Storage\Adapter\ApcOptions(); + $this->_storage = new Cache\Storage\Adapter\Apc(); + $this->_storage->setOptions($this->_options); + + parent::setUp(); + } + + public function tearDown() + { + if (function_exists('apc_clear_cache')) { + apc_clear_cache('user'); + } + + // reset ini configurations + ini_set('apc.use_request_time', $this->iniUseRequestTime); + + parent::tearDown(); + } +} diff --git a/test/Storage/Adapter/CommonAdapterTest.php b/test/Storage/Adapter/CommonAdapterTest.php new file mode 100644 index 000000000..5e318775e --- /dev/null +++ b/test/Storage/Adapter/CommonAdapterTest.php @@ -0,0 +1,1036 @@ +assertInstanceOf( + 'Zend\Cache\Storage\StorageInterface', + $this->_storage, + 'Storage adapter instance is needed for tests' + ); + $this->assertInstanceOf( + 'Zend\Cache\Storage\Adapter\AdapterOptions', + $this->_options, + 'Options instance is needed for tests' + ); + } + + public function tearDown() + { + // be sure the error handler has been stopped + if (ErrorHandler::started()) { + ErrorHandler::stop(); + $this->fail('ErrorHandler not stopped'); + } + } + + public function testOptionNamesValid() + { + $options = $this->_storage->getOptions()->toArray(); + foreach ($options as $name => $value) { + $this->assertRegExp( + '/^[a-z]+[a-z0-9_]*[a-z0-9]+$/', + $name, + "Invalid option name '{$name}'" + ); + } + } + + public function testGettersAndSettersOfOptionsExists() + { + $options = $this->_storage->getOptions(); + foreach ($options->toArray() as $option => $value) { + if ($option == 'adapter') { + // Skip this, as it's a "special" value + continue; + } + $method = ucwords(str_replace('_', ' ', $option)); + $method = str_replace(' ', '', $method); + + $this->assertTrue( + method_exists($options, 'set' . $method), + "Missing method 'set'{$method}" + ); + + $this->assertTrue( + method_exists($options, 'get' . $method), + "Missing method 'get'{$method}" + ); + } + } + + public function testOptionsGetAndSetDefault() + { + $options = $this->_storage->getOptions(); + $this->_storage->setOptions($options); + $this->assertSame($options, $this->_storage->getOptions()); + } + + public function testOptionsFluentInterface() + { + $options = $this->_storage->getOptions(); + foreach ($options->toArray() as $option => $value) { + $method = ucwords(str_replace('_', ' ', $option)); + $method = 'set' . str_replace(' ', '', $method); + $this->assertSame( + $options, + $options->{$method}($value), + "Method '{$method}' doesn't implement the fluent interface" + ); + } + + $this->assertSame( + $this->_storage, + $this->_storage->setOptions($options), + "Method 'setOptions' doesn't implement the fluent interface" + ); + } + + public function testGetCapabilities() + { + $capabilities = $this->_storage->getCapabilities(); + $this->assertInstanceOf('Zend\Cache\Storage\Capabilities', $capabilities); + } + + public function testDatatypesCapability() + { + $capabilities = $this->_storage->getCapabilities(); + $datatypes = $capabilities->getSupportedDatatypes(); + $this->assertInternalType('array', $datatypes); + + foreach ($datatypes as $sourceType => $targetType) { + $this->assertContains( + $sourceType, $this->_phpDatatypes, + "Unknown source type '{$sourceType}'" + ); + if (is_string($targetType)) { + $this->assertContains( + $targetType, $this->_phpDatatypes, + "Unknown target type '{$targetType}'" + ); + } else { + $this->assertInternalType( + 'bool', $targetType, + "Target type must be a string or boolean" + ); + } + } + } + + public function testSupportedMetadataCapability() + { + $capabilities = $this->_storage->getCapabilities(); + $metadata = $capabilities->getSupportedMetadata(); + $this->assertInternalType('array', $metadata); + + foreach ($metadata as $property) { + $this->assertInternalType('string', $property); + } + } + + public function testTtlCapabilities() + { + $capabilities = $this->_storage->getCapabilities(); + + $this->assertInternalType('integer', $capabilities->getMaxTtl()); + $this->assertGreaterThanOrEqual(0, $capabilities->getMaxTtl()); + + $this->assertInternalType('bool', $capabilities->getStaticTtl()); + + $this->assertInternalType('numeric', $capabilities->getTtlPrecision()); + $this->assertGreaterThan(0, $capabilities->getTtlPrecision()); + + $this->assertInternalType('bool', $capabilities->getExpiredRead()); + } + + public function testKeyCapabilities() + { + $capabilities = $this->_storage->getCapabilities(); + + $this->assertInternalType('integer', $capabilities->getMaxKeyLength()); + $this->assertGreaterThanOrEqual(-1, $capabilities->getMaxKeyLength()); + + $this->assertInternalType('bool', $capabilities->getNamespaceIsPrefix()); + + $this->assertInternalType('string', $capabilities->getNamespaceSeparator()); + } + + public function testHasItemReturnsTrueOnValidItem() + { + $this->assertTrue($this->_storage->setItem('key', 'value')); + $this->assertTrue($this->_storage->hasItem('key')); + } + + public function testHasItemReturnsFalseOnMissingItem() + { + $this->assertFalse($this->_storage->hasItem('key')); + } + + public function testHasItemReturnsFalseOnExpiredItem() + { + $capabilities = $this->_storage->getCapabilities(); + + if ($capabilities->getMinTtl() === 0) { + $this->markTestSkipped("Adapter doesn't support item expiration"); + } + + $ttl = $capabilities->getTtlPrecision(); + $this->_options->setTtl($ttl); + + $this->assertTrue($this->_storage->setItem('key', 'value')); + + // wait until the item expired + $wait = $ttl + $capabilities->getTtlPrecision(); + usleep($wait * 2000000); + + $this->assertFalse($this->_storage->hasItem('key')); + } + + public function testHasItemNonReadable() + { + $this->assertTrue($this->_storage->setItem('key', 'value')); + + $this->_options->setReadable(false); + $this->assertFalse($this->_storage->hasItem('key')); + } + + public function testHasItemsReturnsKeysOfFoundItems() + { + $this->assertTrue($this->_storage->setItem('key1', 'value1')); + $this->assertTrue($this->_storage->setItem('key2', 'value2')); + + $result = $this->_storage->hasItems(array('missing', 'key1', 'key2')); + sort($result); + + $exprectedResult = array('key1', 'key2'); + $this->assertEquals($exprectedResult, $result); + } + + public function testHasItemsReturnsEmptyArrayIfNonReadable() + { + $this->assertTrue($this->_storage->setItem('key', 'value')); + + $this->_options->setReadable(false); + $this->assertEquals(array(), $this->_storage->hasItems(array('key'))); + } + + public function testGetItemReturnsNullOnMissingItem() + { + $this->assertNull($this->_storage->getItem('unknwon')); + } + + public function testGetItemSetsSuccessFlag() + { + $success = null; + + // $success = false on get missing item + $this->_storage->getItem('unknown', $success); + $this->assertFalse($success); + + // $success = true on get valid item + $this->_storage->setItem('test', 'test'); + $this->_storage->getItem('test', $success); + $this->assertTrue($success); + } + + public function testGetItemReturnsNullOnExpiredItem() + { + $capabilities = $this->_storage->getCapabilities(); + + if ($capabilities->getMinTtl() === 0) { + $this->markTestSkipped("Adapter doesn't support item expiration"); + } + + if ($capabilities->getUseRequestTime()) { + $this->markTestSkipped("Can't test get expired item if request time will be used"); + } + + $ttl = $capabilities->getTtlPrecision(); + $this->_options->setTtl($ttl); + + $this->_storage->setItem('key', 'value'); + + // wait until expired + $wait = $ttl + $capabilities->getTtlPrecision(); + usleep($wait * 2000000); + + $this->assertNull($this->_storage->getItem('key')); + } + + public function testGetItemReturnsNullIfNonReadable() + { + $this->_options->setReadable(false); + + $this->assertTrue($this->_storage->setItem('key', 'value')); + $this->assertNull($this->_storage->getItem('key')); + } + + public function testGetItemsReturnsKeyValuePairsOfFoundItems() + { + $this->assertTrue($this->_storage->setItem('key1', 'value1')); + $this->assertTrue($this->_storage->setItem('key2', 'value2')); + + $result = $this->_storage->getItems(array('missing', 'key1', 'key2')); + ksort($result); + + $exprectedResult = array( + 'key1' => 'value1', + 'key2' => 'value2', + ); + $this->assertEquals($exprectedResult, $result); + } + + public function testGetItemsReturnsEmptyArrayIfNonReadable() + { + $this->_options->setReadable(false); + + $this->assertTrue($this->_storage->setItem('key', 'value')); + $this->assertEquals(array(), $this->_storage->getItems(array('key'))); + } + + public function testGetMetadata() + { + $capabilities = $this->_storage->getCapabilities(); + $supportedMetadatas = $capabilities->getSupportedMetadata(); + + $this->assertTrue($this->_storage->setItem('key', 'value')); + $metadata = $this->_storage->getMetadata('key'); + + $this->assertInternalType('array', $metadata); + foreach ($supportedMetadatas as $supportedMetadata) { + $this->assertArrayHasKey($supportedMetadata, $metadata); + } + } + + public function testGetMetadataReturnsFalseOnMissingItem() + { + $this->assertFalse($this->_storage->getMetadata('unknown')); + } + + public function testGetMetadataReturnsFalseIfNonReadable() + { + $this->_options->setReadable(false); + + $this->assertTrue($this->_storage->setItem('key', 'value')); + $this->assertFalse($this->_storage->getMetadata('key')); + } + + public function testGetMetadatas() + { + $capabilities = $this->_storage->getCapabilities(); + $supportedMetadatas = $capabilities->getSupportedMetadata(); + + $items = array( + 'key1' => 'value1', + 'key2' => 'value2' + ); + $this->assertSame(array(), $this->_storage->setItems($items)); + + $metadatas = $this->_storage->getMetadatas(array_keys($items)); + $this->assertInternalType('array', $metadatas); + $this->assertSame(count($items), count($metadatas)); + foreach ($metadatas as $k => $metadata) { + $this->assertInternalType('array', $metadata); + foreach ($supportedMetadatas as $supportedMetadata) { + $this->assertArrayHasKey($supportedMetadata, $metadata); + } + } + } + + public function testGetMetadatasReturnsEmptyArrayIfNonReadable() + { + $this->_options->setReadable(false); + + $this->assertTrue($this->_storage->setItem('key', 'value')); + $this->assertEquals(array(), $this->_storage->getMetadatas(array('key'))); + } + + public function testSetGetHasAndRemoveItem() + { + $this->assertTrue($this->_storage->setItem('key', 'value')); + $this->assertEquals('value', $this->_storage->getItem('key')); + $this->assertTrue($this->_storage->hasItem('key')); + + $this->assertTrue($this->_storage->removeItem('key')); + $this->assertFalse($this->_storage->hasItem('key')); + $this->assertNull($this->_storage->getItem('key')); + } + + public function testSetGetHasAndRemoveItems() + { + $items = array( + 'key1' => 'value1', + 'key2' => 'value2', + 'key3' => 'value3', + ); + + $this->assertSame(array(), $this->_storage->setItems($items)); + + $rs = $this->_storage->getItems(array_keys($items)); + $this->assertInternalType('array', $rs); + foreach ($items as $key => $value) { + $this->assertArrayHasKey($key, $rs); + $this->assertEquals($value, $rs[$key]); + } + + $rs = $this->_storage->hasItems(array_keys($items)); + $this->assertInternalType('array', $rs); + $this->assertEquals(count($items), count($rs)); + foreach ($items as $key => $value) { + $this->assertContains($key, $rs); + } + + $this->assertSame(array('missing'), $this->_storage->removeItems(array('missing', 'key1', 'key3'))); + unset($items['key1'], $items['key3']); + + $rs = $this->_storage->getItems(array_keys($items)); + $this->assertInternalType('array', $rs); + foreach ($items as $key => $value) { + $this->assertArrayHasKey($key, $rs); + $this->assertEquals($value, $rs[$key]); + } + + $rs = $this->_storage->hasItems(array_keys($items)); + $this->assertInternalType('array', $rs); + $this->assertEquals(count($items), count($rs)); + foreach ($items as $key => $value) { + $this->assertContains($key, $rs); + } + } + + public function testSetGetHasAndRemoveItemWithNamespace() + { + // write "key" to default namespace + $this->_options->setNamespace('defaultns1'); + $this->assertTrue( $this->_storage->setItem('key', 'defaultns1') ); + + // write "key" to an other default namespace + $this->_options->setNamespace('defaultns2'); + $this->assertTrue( $this->_storage->setItem('key', 'defaultns2') ); + + // test value of defaultns2 + $this->assertTrue($this->_storage->hasItem('key')); + $this->assertEquals('defaultns2', $this->_storage->getItem('key') ); + + // test value of defaultns1 + $this->_options->setNamespace('defaultns1'); + $this->assertTrue($this->_storage->hasItem('key')); + $this->assertEquals('defaultns1', $this->_storage->getItem('key') ); + + // remove item of defaultns1 + $this->_options->setNamespace('defaultns1'); + $this->assertTrue($this->_storage->removeItem('key')); + $this->assertFalse($this->_storage->hasItem('key')); + + // remove item of defaultns2 + $this->_options->setNamespace('defaultns2'); + $this->assertTrue($this->_storage->removeItem('key')); + $this->assertFalse($this->_storage->hasItem('key')); + } + + public function testSetGetHasAndRemoveItemsWithNamespace() + { + $items = array( + 'key1' => 'value1', + 'key2' => 'value2', + 'key3' => 'value3', + ); + + $this->_options->setNamespace('defaultns1'); + $this->assertSame(array(), $this->_storage->setItems($items)); + + $this->_options->setNamespace('defaultns2'); + $this->assertSame(array(), $this->_storage->hasItems(array_keys($items))); + + $this->_options->setNamespace('defaultns1'); + $rs = $this->_storage->getItems(array_keys($items)); + $this->assertInternalType('array', $rs); + foreach ($items as $key => $value) { + $this->assertArrayHasKey($key, $rs); + $this->assertEquals($value, $rs[$key]); + } + + $rs = $this->_storage->hasItems(array_keys($items)); + $this->assertInternalType('array', $rs); + $this->assertEquals(count($items), count($rs)); + foreach ($items as $key => $value) { + $this->assertContains($key, $rs); + } + + // remove the first and the last item + $this->assertSame(array('missing'), $this->_storage->removeItems(array('missing', 'key1', 'key3'))); + unset($items['key1'], $items['key3']); + + $rs = $this->_storage->getItems(array_keys($items)); + $this->assertInternalType('array', $rs); + foreach ($items as $key => $value) { + $this->assertArrayHasKey($key, $rs); + $this->assertEquals($value, $rs[$key]); + } + + $rs = $this->_storage->hasItems(array_keys($items)); + $this->assertInternalType('array', $rs); + $this->assertEquals(count($items), count($rs)); + foreach ($items as $key => $value) { + $this->assertContains($key, $rs); + } + } + + public function testSetAndGetExpiredItem() + { + $capabilities = $this->_storage->getCapabilities(); + + if ($capabilities->getMinTtl() === 0) { + $this->markTestSkipped("Adapter doesn't support item expiration"); + } + + $ttl = $capabilities->getTtlPrecision(); + $this->_options->setTtl($ttl); + + $this->_storage->setItem('key', 'value'); + + // wait until expired + $wait = $ttl + $capabilities->getTtlPrecision(); + usleep($wait * 2000000); + + if (!$capabilities->getUseRequestTime()) { + $this->assertNull($this->_storage->getItem('key')); + } else { + $this->assertEquals('value', $this->_storage->getItem('key')); + } + + $this->_options->setTtl(0); + if ($capabilities->getExpiredRead()) { + $this->assertEquals('value', $this->_storage->getItem('key')); + } else { + $this->assertNull($this->_storage->getItem('key')); + } + } + + public function testSetAndGetExpiredItems() + { + $capabilities = $this->_storage->getCapabilities(); + + if ($capabilities->getMinTtl() === 0) { + $this->markTestSkipped("Adapter doesn't support item expiration"); + } + + $ttl = $capabilities->getTtlPrecision(); + $this->_options->setTtl($ttl); + + $items = array( + 'key1' => 'value1', + 'key2' => 'value2', + 'key3' => 'value3' + ); + $this->assertSame(array(), $this->_storage->setItems($items)); + + // wait until expired + $wait = $ttl + $capabilities->getTtlPrecision(); + usleep($wait * 2000000); + + $rs = $this->_storage->getItems(array_keys($items)); + if (!$capabilities->getUseRequestTime()) { + $this->assertEquals(array(), $rs); + } else { + ksort($rs); + $this->assertEquals($items, $rs); + } + + $this->_options->setTtl(0); + if ($capabilities->getExpiredRead()) { + $rs = $this->_storage->getItems(array_keys($items)); + ksort($rs); + $this->assertEquals($items, $rs); + } + } + + public function testSetAndGetItemOfDifferentTypes() + { + $capabilities = $this->_storage->getCapabilities(); + + $types = array( + 'NULL' => null, + 'boolean' => true, + 'integer' => 12345, + 'double' => 123.45, + 'string' => 'string', // already tested + 'array' => array('one', 'tow' => 'two', 'three' => array('four' => 'four')), + 'object' => new \stdClass(), + 'resource' => fopen(__FILE__, 'r'), + ); + $types['object']->one = 'one'; + $types['object']->two = new \stdClass(); + $types['object']->two->three = 'three'; + + foreach ($capabilities->getSupportedDatatypes() as $sourceType => $targetType) { + if ($targetType === false) { + continue; + } + + $value = $types[$sourceType]; + $this->assertTrue($this->_storage->setItem('key', $value), "Failed to set type '{$sourceType}'"); + + if ($targetType === true) { + $this->assertSame($value, $this->_storage->getItem('key')); + } elseif (is_string($targetType)) { + settype($value, $targetType); + $this->assertEquals($value, $this->_storage->getItem('key')); + } + } + } + + public function testSetItemReturnsFalseIfNonWritable() + { + $this->_options->setWritable(false); + + $this->assertFalse($this->_storage->setItem('key', 'value')); + $this->assertFalse($this->_storage->hasItem('key')); + } + + public function testAddNewItem() + { + $this->assertTrue($this->_storage->addItem('key', 'value')); + $this->assertTrue($this->_storage->hasItem('key')); + } + + public function testAddItemReturnsFalseIfItemAlreadyExists() + { + $this->assertTrue($this->_storage->setItem('key', 'value')); + $this->assertFalse($this->_storage->addItem('key', 'newValue')); + } + + public function testAddItemReturnsFalseIfNonWritable() + { + $this->_options->setWritable(false); + + $this->assertFalse($this->_storage->addItem('key', 'value')); + $this->assertFalse($this->_storage->hasItem('key')); + } + + public function testAddItemsReturnsFailedKeys() + { + $this->assertTrue($this->_storage->setItem('key1', 'value1')); + + $failedKeys = $this->_storage->addItems(array( + 'key1' => 'XYZ', + 'key2' => 'value2', + )); + + $this->assertSame(array('key1'), $failedKeys); + $this->assertSame('value1', $this->_storage->getItem('key1')); + $this->assertTrue($this->_storage->hasItem('key2')); + } + + public function testReplaceExistingItem() + { + $this->assertTrue($this->_storage->setItem('key', 'value')); + $this->assertTrue($this->_storage->replaceItem('key', 'anOtherValue')); + $this->assertEquals('anOtherValue', $this->_storage->getItem('key')); + } + + public function testReplaceItemReturnsFalseOnMissingItem() + { + $this->assertFalse($this->_storage->replaceItem('missingKey', 'value')); + } + + public function testReplaceItemReturnsFalseIfNonWritable() + { + $this->_storage->setItem('key', 'value'); + $this->_options->setWritable(false); + + $this->assertFalse($this->_storage->replaceItem('key', 'newvalue')); + $this->assertEquals('value', $this->_storage->getItem('key')); + } + + public function testReplaceItemsReturnsFailedKeys() + { + $this->assertTrue($this->_storage->setItem('key1', 'value1')); + + $failedKeys = $this->_storage->replaceItems(array( + 'key1' => 'XYZ', + 'key2' => 'value2', + )); + + $this->assertSame(array('key2'), $failedKeys); + $this->assertSame('XYZ', $this->_storage->getItem('key1')); + $this->assertFalse($this->_storage->hasItem('key2')); + } + + public function testRemoveItemReturnsFalseOnMissingItem() + { + $this->assertFalse($this->_storage->removeItem('missing')); + } + + public function testRemoveItemsReturnsMissingKeys() + { + $this->_storage->setItem('key', 'value'); + $this->assertSame(array('missing'), $this->_storage->removeItems(array('key', 'missing'))); + } + + public function testCheckAndSetItem() + { + $this->assertTrue($this->_storage->setItem('key', 'value')); + + $success = null; + $casToken = null; + $this->assertEquals('value', $this->_storage->getItem('key', $success, $casToken)); + $this->assertNotNull($casToken); + + $this->assertTrue($this->_storage->checkAndSetItem($casToken, 'key', 'newValue')); + $this->assertFalse($this->_storage->checkAndSetItem($casToken, 'key', 'failedValue')); + $this->assertEquals('newValue', $this->_storage->getItem('key')); + } + + public function testIncrementItem() + { + $this->assertTrue($this->_storage->setItem('counter', 10)); + $this->assertEquals(15, $this->_storage->incrementItem('counter', 5)); + $this->assertEquals(15, $this->_storage->getItem('counter')); + } + + public function testIncrementItemInitialValue() + { + $this->assertEquals(5, $this->_storage->incrementItem('counter', 5)); + $this->assertEquals(5, $this->_storage->getItem('counter')); + } + + public function testIncrementItemReturnsFalseIfNonWritable() + { + $this->_storage->setItem('key', 10); + $this->_options->setWritable(false); + + $this->assertFalse($this->_storage->incrementItem('key', 5)); + $this->assertEquals(10, $this->_storage->getItem('key')); + } + + public function testIncrementItemsResturnsKeyValuePairsOfWrittenItems() + { + $this->assertTrue($this->_storage->setItem('key1', 10)); + + $result = $this->_storage->incrementItems(array( + 'key1' => 10, + 'key2' => 10, + )); + ksort($result); + + $this->assertSame(array( + 'key1' => 20, + 'key2' => 10, + ), $result); + } + + public function testIncrementItemsReturnsEmptyArrayIfNonWritable() + { + $this->_storage->setItem('key', 10); + $this->_options->setWritable(false); + + $this->assertSame(array(), $this->_storage->incrementItems(array('key' => 5))); + $this->assertEquals(10, $this->_storage->getItem('key')); + } + + public function testDecrementItem() + { + $this->assertTrue($this->_storage->setItem('counter', 30)); + $this->assertEquals(25, $this->_storage->decrementItem('counter', 5)); + $this->assertEquals(25, $this->_storage->getItem('counter')); + } + + public function testDecrementItemInitialValue() + { + $this->assertEquals(-5, $this->_storage->decrementItem('counter', 5)); + $this->assertEquals(-5, $this->_storage->getItem('counter')); + } + + public function testDecrementItemReturnsFalseIfNonWritable() + { + $this->_storage->setItem('key', 10); + $this->_options->setWritable(false); + + $this->assertFalse($this->_storage->decrementItem('key', 5)); + $this->assertEquals(10, $this->_storage->getItem('key')); + } + + public function testDecrementItemsReturnsEmptyArrayIfNonWritable() + { + $this->_storage->setItem('key', 10); + $this->_options->setWritable(false); + + $this->assertSame(array(), $this->_storage->decrementItems(array('key' => 5))); + $this->assertEquals(10, $this->_storage->getItem('key')); + } + + public function testTouchItem() + { + $capabilities = $this->_storage->getCapabilities(); + + if ($capabilities->getMinTtl() === 0) { + $this->markTestSkipped("Adapter doesn't support item expiration"); + } + + $this->_options->setTtl(2 * $capabilities->getTtlPrecision()); + + $this->assertTrue($this->_storage->setItem('key', 'value')); + + // sleep 1 times before expire to touch the item + usleep($capabilities->getTtlPrecision() * 1000000); + $this->assertTrue($this->_storage->touchItem('key')); + + usleep($capabilities->getTtlPrecision() * 1000000); + $this->assertTrue($this->_storage->hasItem('key')); + + if (!$capabilities->getUseRequestTime()) { + usleep($capabilities->getTtlPrecision() * 2000000); + $this->assertFalse($this->_storage->hasItem('key')); + } + } + + public function testTouchItemReturnsFalseOnMissingItem() + { + $this->assertFalse($this->_storage->touchItem('missing')); + } + + public function testTouchItemReturnsFalseIfNonWritable() + { + $this->_options->setWritable(false); + + $this->assertFalse($this->_storage->touchItem('key')); + } + + public function testTouchItemsReturnsGivenKeysIfNonWritable() + { + $this->_options->setWritable(false); + $this->assertSame(array('key'), $this->_storage->touchItems(array('key'))); + } + + public function testOptimize() + { + if (!($this->_storage instanceof OptimizableInterface)) { + $this->markTestSkipped("Storage doesn't implement OptimizableInterface"); + } + + $this->assertTrue($this->_storage->optimize()); + } + + public function testIterator() + { + if (!$this->_storage instanceof IterableInterface) { + $this->markTestSkipped("Storage doesn't implement IterableInterface"); + } + + $items = array( + 'key1' => 'value1', + 'key2' => 'value2', + ); + $this->assertSame(array(), $this->_storage->setItems($items)); + + // check iterator aggregate + $iterator = $this->_storage->getIterator(); + $this->assertInstanceOf('Zend\Cache\Storage\IteratorInterface', $iterator); + $this->assertSame(IteratorInterface::CURRENT_AS_KEY, $iterator->getMode()); + + // check mode CURRENT_AS_KEY + $iterator = $this->_storage->getIterator(); + $iterator->setMode(IteratorInterface::CURRENT_AS_KEY); + $keys = iterator_to_array($iterator, false); + sort($keys); + $this->assertSame(array_keys($items), $keys); + + // check mode CURRENT_AS_VALUE + $iterator = $this->_storage->getIterator(); + $iterator->setMode(IteratorInterface::CURRENT_AS_VALUE); + $result = iterator_to_array($iterator, true); + ksort($result); + $this->assertSame($items, $result); + } + + public function testFlush() + { + if (!($this->_storage instanceof FlushableInterface)) { + $this->markTestSkipped("Storage doesn't implement OptimizableInterface"); + } + + $this->assertSame(array(), $this->_storage->setItems(array( + 'key1' => 'value1', + 'key2' => 'value2', + ))); + + $this->assertTrue($this->_storage->flush()); + $this->assertFalse($this->_storage->hasItem('key1')); + $this->assertFalse($this->_storage->hasItem('key2')); + } + + public function testClearByPrefix() + { + if (!($this->_storage instanceof ClearByPrefixInterface)) { + $this->markTestSkipped("Storage doesn't implement ClearByPrefixInterface"); + } + + $this->assertSame(array(), $this->_storage->setItems(array( + 'key1' => 'value1', + 'key2' => 'value2', + 'test' => 'value', + ))); + + $this->assertTrue($this->_storage->clearByPrefix('key')); + $this->assertFalse($this->_storage->hasItem('key1')); + $this->assertFalse($this->_storage->hasItem('key2')); + $this->assertTrue($this->_storage->hasItem('test')); + } + + public function testClearByNamespace() + { + if (!($this->_storage instanceof ClearByNamespaceInterface)) { + $this->markTestSkipped("Storage doesn't implement ClearByNamespaceInterface"); + } + + // write 2 items of 2 different namespaces + $this->_options->setNamespace('ns1'); + $this->assertTrue($this->_storage->setItem('key1', 'value1')); + $this->_options->setNamespace('ns2'); + $this->assertTrue($this->_storage->setItem('key2', 'value2')); + + // clear unknown namespace should return true but clear nothing + $this->assertTrue($this->_storage->clearByNamespace('unknown')); + $this->_options->setNamespace('ns1'); + $this->assertTrue($this->_storage->hasItem('key1')); + $this->_options->setNamespace('ns2'); + $this->assertTrue($this->_storage->hasItem('key2')); + + // clear "ns1" + $this->assertTrue($this->_storage->clearByNamespace('ns1')); + $this->_options->setNamespace('ns1'); + $this->assertFalse($this->_storage->hasItem('key1')); + $this->_options->setNamespace('ns2'); + $this->assertTrue($this->_storage->hasItem('key2')); + + // clear "ns2" + $this->assertTrue($this->_storage->clearByNamespace('ns2')); + $this->_options->setNamespace('ns1'); + $this->assertFalse($this->_storage->hasItem('key1')); + $this->_options->setNamespace('ns2'); + $this->assertFalse($this->_storage->hasItem('key2')); + } + + public function testClearExpired() + { + if (!($this->_storage instanceof ClearExpiredInterface)) { + $this->markTestSkipped("Storage doesn't implement ClearExpiredInterface"); + } + + $capabilities = $this->_storage->getCapabilities(); + $ttl = $capabilities->getTtlPrecision(); + $this->_options->setTtl($ttl); + + $this->assertTrue($this->_storage->setItem('key1', 'value1')); + + // wait until the first item expired + $wait = $ttl + $capabilities->getTtlPrecision(); + usleep($wait * 2000000); + + $this->assertTrue($this->_storage->setItem('key2', 'value2')); + + $this->assertTrue($this->_storage->clearExpired()); + + if ($capabilities->getUseRequestTime()) { + $this->assertTrue($this->_storage->hasItem('key1')); + } else { + $this->assertFalse($this->_storage->hasItem('key1', array('ttl' => 0))); + } + + $this->assertTrue($this->_storage->hasItem('key2')); + } + + public function testTagable() + { + if (!($this->_storage instanceof TaggableInterface)) { + $this->markTestSkipped("Storage doesn't implement TaggableInterface"); + } + + $this->assertSame(array(), $this->_storage->setItems(array( + 'key1' => 'value1', + 'key2' => 'value2', + 'key3' => 'value3', + ))); + + $this->assertTrue($this->_storage->setTags('key1', array('tag1a', 'tag1b'))); + $this->assertTrue($this->_storage->setTags('key2', array('tag2a', 'tag2b'))); + $this->assertTrue($this->_storage->setTags('key3', array('tag3a', 'tag3b'))); + $this->assertFalse($this->_storage->setTags('missing', array('tag'))); + + // return tags + $tags = $this->_storage->getTags('key1'); + $this->assertInternalType('array', $tags); + sort($tags); + $this->assertSame(array('tag1a', 'tag1b'), $tags); + + // this should remove nothing + $this->assertTrue($this->_storage->clearByTags(array('tag1a', 'tag2a'))); + $this->assertTrue($this->_storage->hasItem('key1')); + $this->assertTrue($this->_storage->hasItem('key2')); + $this->assertTrue($this->_storage->hasItem('key3')); + + // this should remove key1 and key2 + $this->assertTrue($this->_storage->clearByTags(array('tag1a', 'tag2b'), true)); + $this->assertFalse($this->_storage->hasItem('key1')); + $this->assertFalse($this->_storage->hasItem('key2')); + $this->assertTrue($this->_storage->hasItem('key3')); + + // this should remove key3 + $this->assertTrue($this->_storage->clearByTags(array('tag3a', 'tag3b'), true)); + $this->assertFalse($this->_storage->hasItem('key1')); + $this->assertFalse($this->_storage->hasItem('key2')); + $this->assertFalse($this->_storage->hasItem('key3')); + } +} diff --git a/test/Storage/Adapter/DbaDb2Test.php b/test/Storage/Adapter/DbaDb2Test.php new file mode 100644 index 000000000..d137785b7 --- /dev/null +++ b/test/Storage/Adapter/DbaDb2Test.php @@ -0,0 +1,24 @@ +_umask = umask(); + + $this->_tmpCacheDir = @tempnam(sys_get_temp_dir(), 'zend_cache_test_'); + if (!$this->_tmpCacheDir) { + $err = error_get_last(); + $this->fail("Can't create temporary cache directory-file: {$err['message']}"); + } elseif (!@unlink($this->_tmpCacheDir)) { + $err = error_get_last(); + $this->fail("Can't remove temporary cache directory-file: {$err['message']}"); + } elseif (!@mkdir($this->_tmpCacheDir, 0777)) { + $err = error_get_last(); + $this->fail("Can't create temporary cache directory: {$err['message']}"); + } + + $this->_options = new Cache\Storage\Adapter\FilesystemOptions(array( + 'cache_dir' => $this->_tmpCacheDir, + )); + $this->_storage = new Cache\Storage\Adapter\Filesystem(); + $this->_storage->setOptions($this->_options); + + parent::setUp(); + } + + public function tearDown() + { + $this->_removeRecursive($this->_tmpCacheDir); + + if ($this->_umask != umask()) { + umask($this->_umask); + $this->fail("Umask wasn't reset"); + } + + parent::tearDown(); + } + + protected function _removeRecursive($dir) + { + if (file_exists($dir)) { + $dirIt = new \DirectoryIterator($dir); + foreach ($dirIt as $entry) { + $fname = $entry->getFilename(); + if ($fname == '.' || $fname == '..') { + continue; + } + + if ($entry->isFile()) { + unlink($entry->getPathname()); + } else { + $this->_removeRecursive($entry->getPathname()); + } + } + + rmdir($dir); + } + } + + public function testNormalizeCacheDir() + { + $cacheDir = $cacheDirExpected = realpath(sys_get_temp_dir()); + + if (DIRECTORY_SEPARATOR != '/') { + $cacheDir = str_replace(DIRECTORY_SEPARATOR, '/', $cacheDir); + } + + $firstSlash = strpos($cacheDir, '/'); + $cacheDir = substr($cacheDir, 0, $firstSlash + 1) + . '..//../' + . substr($cacheDir, $firstSlash) + . '///'; + + $this->_options->setCacheDir($cacheDir); + $cacheDir = $this->_options->getCacheDir(); + + $this->assertEquals($cacheDirExpected, $cacheDir); + } + + public function testSetCacheDirToSystemsTempDirWithNull() + { + $this->_options->setCacheDir(null); + $this->assertEquals(sys_get_temp_dir(), $this->_options->getCacheDir()); + } + + public function testSetCacheDirNoDirectoryException() + { + $this->setExpectedException('Zend\Cache\Exception\InvalidArgumentException'); + $this->_options->setCacheDir(__FILE__); + } + + public function testSetCacheDirNotWritableException() + { + if (substr(PHP_OS, 0, 3) == 'WIN') { + $this->markTestSkipped("Not testable on windows"); + } else { + @exec('whoami 2>&1', $out, $ret); + if ($ret) { + $err = error_get_last(); + $this->markTestSkipped("Not testable: {$err['message']}"); + } elseif (isset($out[0]) && $out[0] == 'root') { + $this->markTestSkipped("Not testable as root"); + } + } + + $this->setExpectedException('Zend\Cache\Exception\InvalidArgumentException'); + + // create a not writable temporaty directory + $testDir = tempnam(sys_get_temp_dir(), 'ZendTest'); + unlink($testDir); mkdir($testDir); chmod($testDir, 0557); + + try { + $this->_options->setCacheDir($testDir); + } catch (\Exception $e) { + rmdir($testDir); + throw $e; + } + } + + public function testSetCacheDirNotReadableException() + { + if (substr(PHP_OS, 0, 3) == 'WIN') { + $this->markTestSkipped("Not testable on windows"); + } else { + @exec('whoami 2>&1', $out, $ret); + if ($ret) { + $this->markTestSkipped("Not testable: " . implode("\n", $out)); + } elseif (isset($out[0]) && $out[0] == 'root') { + $this->markTestSkipped("Not testable as root"); + } + } + + $this->setExpectedException('Zend\Cache\Exception\InvalidArgumentException'); + + // create a not readable temporaty directory + $testDir = tempnam(sys_get_temp_dir(), 'ZendTest'); + unlink($testDir); mkdir($testDir); chmod($testDir, 0337); + + try { + $this->_options->setCacheDir($testDir); + } catch (\Exception $e) { + rmdir($testDir); + throw $e; + } + } + + public function testSetFilePermissionThrowsExceptionIfNotWritable() + { + $this->setExpectedException('Zend\Cache\Exception\InvalidArgumentException'); + $this->_options->setFilePermission(0466); + } + + public function testSetFilePermissionThrowsExceptionIfNotReadable() + { + $this->setExpectedException('Zend\Cache\Exception\InvalidArgumentException'); + $this->_options->setFilePermission(0266); + } + + public function testSetFilePermissionThrowsExceptionIfExecutable() + { + $this->setExpectedException('Zend\Cache\Exception\InvalidArgumentException'); + $this->_options->setFilePermission(0661); + } + + public function testSetNoAtimeChangesAtimeOfMetadataCapability() + { + $capabilities = $this->_storage->getCapabilities(); + + $this->_options->setNoAtime(false); + $this->assertContains('atime', $capabilities->getSupportedMetadata()); + + $this->_options->setNoAtime(true); + $this->assertNotContains('atime', $capabilities->getSupportedMetadata()); + } + + public function testSetNoCtimeChangesCtimeOfMetadataCapability() + { + $capabilities = $this->_storage->getCapabilities(); + + $this->_options->setNoCtime(false); + $this->assertContains('ctime', $capabilities->getSupportedMetadata()); + + $this->_options->setNoCtime(true); + $this->assertNotContains('ctime', $capabilities->getSupportedMetadata()); + } + + public function testSetDirPermissionThrowsExceptionIfNotWritable() + { + $this->setExpectedException('Zend\Cache\Exception\InvalidArgumentException'); + $this->_options->setDirPermission(0577); + } + + public function testSetDirPermissionThrowsExceptionIfNotReadable() + { + $this->setExpectedException('Zend\Cache\Exception\InvalidArgumentException'); + $this->_options->setDirPermission(0377); + } + + public function testSetDirPermissionThrowsExceptionIfNotExecutable() + { + $this->setExpectedException('Zend\Cache\Exception\InvalidArgumentException'); + $this->_options->setDirPermission(0677); + } + + public function testSetDirLevelInvalidException() + { + $this->setExpectedException('Zend\Cache\Exception\InvalidArgumentException'); + $this->_options->setDirLevel(17); // must between 0-16 + } + + public function testSetUmask() + { + $this->_options->setUmask(023); + $this->assertSame(023, $this->_options->getUmask()); + + $this->_options->setUmask(false); + $this->assertFalse($this->_options->getUmask()); + } + + public function testSetUmaskThrowsExceptionIfNotWritable() + { + $this->setExpectedException('Zend\Cache\Exception\InvalidArgumentException'); + $this->_options->setUmask(0300); + } + + public function testSetUmaskThrowsExceptionIfNotReadable() + { + $this->setExpectedException('Zend\Cache\Exception\InvalidArgumentException'); + $this->_options->setUmask(0200); + } + + public function testSetUmaskThrowsExceptionIfNotExecutable() + { + $this->setExpectedException('Zend\Cache\Exception\InvalidArgumentException'); + $this->_options->setUmask(0100); + } + + public function testGetMetadataWithCtime() + { + $this->_options->setNoCtime(false); + + $this->assertTrue($this->_storage->setItem('test', 'v')); + + $meta = $this->_storage->getMetadata('test'); + $this->assertInternalType('array', $meta); + + $expectedCtime = filectime($meta['filespec'] . '.dat'); + $this->assertEquals($expectedCtime, $meta['ctime']); + } + + public function testGetMetadataWithAtime() + { + $this->_options->setNoAtime(false); + + $this->assertTrue($this->_storage->setItem('test', 'v')); + + $meta = $this->_storage->getMetadata('test'); + $this->assertInternalType('array', $meta); + + $expectedAtime = fileatime($meta['filespec'] . '.dat'); + $this->assertEquals($expectedAtime, $meta['atime']); + } +} diff --git a/test/Storage/Adapter/MemcachedTest.php b/test/Storage/Adapter/MemcachedTest.php new file mode 100644 index 000000000..ecdac3ecc --- /dev/null +++ b/test/Storage/Adapter/MemcachedTest.php @@ -0,0 +1,97 @@ +markTestSkipped("Skipped by TestConfiguration (TESTS_ZEND_CACHE_MEMCACHED_ENABLED)"); + } + + if (!extension_loaded('memcached')) { + $this->markTestSkipped("Memcached extension is not loaded"); + } + + $this->_options = new Cache\Storage\Adapter\MemcachedOptions(); + if (defined('TESTS_ZEND_CACHE_MEMCACHED_HOST') && defined('TESTS_ZEND_CACHE_MEMCACHED_PORT')) { + $this->_options->addServer(TESTS_ZEND_CACHE_MEMCACHED_HOST, TESTS_ZEND_CACHE_MEMCACHED_PORT); + } elseif (defined('TESTS_ZEND_CACHE_MEMCACHED_HOST')) { + $this->_options->addServer(TESTS_ZEND_CACHE_MEMCACHED_HOST); + } + + $this->_storage = new Cache\Storage\Adapter\Memcached($this->_options); + + parent::setUp(); + } + + public function testOptionsAddServer() + { + $options = new Cache\Storage\Adapter\MemcachedOptions(); + $options->addServer('127.0.0.1', 11211); + $options->addServer('localhost'); + $options->addServer('domain.com', 11215); + + $servers = array( + array('127.0.0.1', 11211), + array('localhost', 11211), + array('domain.com', 11215), + ); + + $this->assertEquals($options->getServers(), $servers); + $memcached = new Cache\Storage\Adapter\Memcached($options); + $this->assertEquals($memcached->getOptions()->getServers(), $servers); + } + + public function testOptionsSetServers() + { + $options = new Cache\Storage\Adapter\MemcachedOptions(); + $servers = array( + array('127.0.0.1', 12345), + array('localhost', 54321), + array('domain.com') + ); + + $options->setServers($servers); + $servers[2][1] = 11211; + $this->assertEquals($options->getServers(), $servers); + + $memcached = new Cache\Storage\Adapter\Memcached($options); + $this->assertEquals($memcached->getOptions()->getServers(), $servers); + } + + public function testNoOptionsSetsDefaultServer() + { + $memcached = new Cache\Storage\Adapter\Memcached(); + + $this->assertEquals($memcached->getOptions()->getServers(), array(array('127.0.0.1', 11211))); + } + + public function tearDown() + { + if ($this->_storage) { + $this->_storage->flush(); + } + + parent::tearDown(); + } +} diff --git a/test/Storage/Adapter/MemoryTest.php b/test/Storage/Adapter/MemoryTest.php new file mode 100644 index 000000000..e404080b9 --- /dev/null +++ b/test/Storage/Adapter/MemoryTest.php @@ -0,0 +1,41 @@ +_options = new Cache\Storage\Adapter\MemoryOptions(); + $this->_storage = new Cache\Storage\Adapter\Memory(); + $this->_storage->setOptions($this->_options); + + parent::setUp(); + } + + public function testThrowOutOfSpaceException() + { + $this->_options->setMemoryLimit(memory_get_usage(true) - 8); + + $this->setExpectedException('Zend\Cache\Exception\OutOfSpaceException'); + $this->_storage->addItem('test', 'test'); + } +} diff --git a/test/Storage/Adapter/WinCacheTest.php b/test/Storage/Adapter/WinCacheTest.php new file mode 100644 index 000000000..8d3fd39c8 --- /dev/null +++ b/test/Storage/Adapter/WinCacheTest.php @@ -0,0 +1,61 @@ +markTestSkipped("Skipped by TestConfiguration (TESTS_ZEND_CACHE_WINCACHE_ENABLED)"); + } + + if (!extension_loaded('wincache')) { + $this->markTestSkipped("WinCache extension is not loaded"); + } + + $enabled = ini_get('wincache.ucenabled'); + if (PHP_SAPI == 'cli') { + $enabled = $enabled && (bool) ini_get('wincache.enablecli'); + } + + if (!$enabled) { + throw new Exception\ExtensionNotLoadedException( + "WinCache is disabled - see 'wincache.ucenabled' and 'wincache.enablecli'" + ); + } + + $this->_options = new Cache\Storage\Adapter\WinCacheOptions(); + $this->_storage = new Cache\Storage\Adapter\WinCache(); + $this->_storage->setOptions($this->_options); + + parent::setUp(); + } + + public function tearDown() + { + if (function_exists('wincache_ucache_clear')) { + wincache_ucache_clear(); + } + + parent::tearDown(); + } +} diff --git a/test/Storage/Adapter/ZendServerDiskTest.php b/test/Storage/Adapter/ZendServerDiskTest.php new file mode 100644 index 000000000..ff32b5148 --- /dev/null +++ b/test/Storage/Adapter/ZendServerDiskTest.php @@ -0,0 +1,53 @@ +markTestSkipped("Skipped by TestConfiguration (TESTS_ZEND_CACHE_ZEND_SERVER_ENABLED)"); + } + + if (!function_exists('zend_disk_cache_store') || PHP_SAPI == 'cli') { + try { + new Cache\Storage\Adapter\ZendServerDisk(); + $this->fail("Missing expected ExtensionNotLoadedException"); + } catch (Exception\ExtensionNotLoadedException $e) { + $this->markTestSkipped($e->getMessage()); + } + } + + $this->_options = new Cache\Storage\Adapter\AdapterOptions(); + $this->_storage = new Cache\Storage\Adapter\ZendServerDisk($this->_options); + parent::setUp(); + } + + public function tearDown() + { + if (function_exists('zend_disk_cache_clear')) { + zend_disk_cache_clear(); + } + + parent::tearDown(); + } +} diff --git a/test/Storage/Adapter/ZendServerShmTest.php b/test/Storage/Adapter/ZendServerShmTest.php new file mode 100644 index 000000000..32cb1e358 --- /dev/null +++ b/test/Storage/Adapter/ZendServerShmTest.php @@ -0,0 +1,58 @@ +markTestSkipped("Skipped by TestConfiguration (TESTS_ZEND_CACHE_ZEND_SERVER_ENABLED)"); + } + + if (strtolower(PHP_SAPI) == 'cli') { + $this->markTestSkipped('Zend Server SHM does not work in CLI environment'); + return; + } + + if (!function_exists('zend_shm_cache_store')) { + try { + new Cache\Storage\Adapter\ZendServerShm(); + $this->fail("Missing expected ExtensionNotLoadedException"); + } catch (Exception\ExtensionNotLoadedException $e) { + $this->markTestSkipped($e->getMessage()); + } + } + + $this->_options = new Cache\Storage\Adapter\AdapterOptions(); + $this->_storage = new Cache\Storage\Adapter\ZendServerShm($this->_options); + parent::setUp(); + } + + public function tearDown() + { + if (function_exists('zend_shm_cache_clear')) { + zend_shm_cache_clear(); + } + + parent::tearDown(); + } +} diff --git a/test/Storage/CapabilitiesTest.php b/test/Storage/CapabilitiesTest.php new file mode 100644 index 000000000..7ed5f5eb7 --- /dev/null +++ b/test/Storage/CapabilitiesTest.php @@ -0,0 +1,99 @@ +_marker = new \stdClass(); + $this->_adapter = new MemoryAdapter(); + + $this->_baseCapabilities = new Capabilities($this->_adapter, $this->_marker); + $this->_capabilities = new Capabilities($this->_adapter, $this->_marker, array(), $this->_baseCapabilities); + } + + public function testGetAdapter() + { + $this->assertSame($this->_adapter, $this->_capabilities->getAdapter()); + $this->assertSame($this->_adapter, $this->_baseCapabilities->getAdapter()); + } + + public function testSetAndGetCapability() + { + $this->_capabilities->setMaxTtl($this->_marker, 100); + $this->assertEquals(100, $this->_capabilities->getMaxTtl()); + } + + public function testGetCapabilityByBaseCapabilities() + { + $this->_baseCapabilities->setMaxTtl($this->_marker, 100); + $this->assertEquals(100, $this->_capabilities->getMaxTtl()); + } + + public function testTriggerCapabilityEvent() + { + $em = $this->_capabilities->getAdapter()->getEventManager(); + $event = null; + $em->attach('capability', function ($eventArg) use (&$event) { + $event = $eventArg; + }); + + $this->_capabilities->setMaxTtl($this->_marker, 100); + + $this->assertInstanceOf('Zend\EventManager\Event', $event); + $this->assertEquals('capability', $event->getName()); + $this->assertSame($this->_adapter, $event->getTarget()); + + $params = $event->getParams(); + $this->assertInstanceOf('ArrayObject', $params); + $this->assertTrue(isset($params ['maxTtl'])); + $this->assertEquals(100, $params['maxTtl']); + } +} diff --git a/test/Storage/Plugin/ClearExpiredByFactorTest.php b/test/Storage/Plugin/ClearExpiredByFactorTest.php new file mode 100644 index 000000000..cad736fa8 --- /dev/null +++ b/test/Storage/Plugin/ClearExpiredByFactorTest.php @@ -0,0 +1,95 @@ +_adapter = new ClearExpiredMockAdapter(); + $this->_options = new Cache\Storage\Plugin\PluginOptions(array( + 'clearing_factor' => 1, + )); + $this->_plugin = new Cache\Storage\Plugin\ClearExpiredByFactor(); + $this->_plugin->setOptions($this->_options); + + parent::setUp(); + } + + public function testAddPlugin() + { + $this->_adapter->addPlugin($this->_plugin); + + // check attached callbacks + $expectedListeners = array( + 'setItem.post' => 'clearExpiredByFactor', + 'setItems.post' => 'clearExpiredByFactor', + 'addItem.post' => 'clearExpiredByFactor', + 'addItems.post' => 'clearExpiredByFactor', + ); + foreach ($expectedListeners as $eventName => $expectedCallbackMethod) { + $listeners = $this->_adapter->getEventManager()->getListeners($eventName); + + // event should attached only once + $this->assertSame(1, $listeners->count()); + + // check expected callback method + $cb = $listeners->top()->getCallback(); + $this->assertArrayHasKey(0, $cb); + $this->assertSame($this->_plugin, $cb[0]); + $this->assertArrayHasKey(1, $cb); + $this->assertSame($expectedCallbackMethod, $cb[1]); + } + } + + public function testRemovePlugin() + { + $this->_adapter->addPlugin($this->_plugin); + $this->_adapter->removePlugin($this->_plugin); + + // no events should be attached + $this->assertEquals(0, count($this->_adapter->getEventManager()->getEvents())); + } + + public function testClearExpiredByFactor() + { + $adapter = $this->getMock(get_class($this->_adapter), array('clearExpired')); + $this->_options->setClearingFactor(1); + + // test clearByNamespace will be called + $adapter + ->expects($this->once()) + ->method('clearExpired') + ->will($this->returnValue(true)); + + // call event callback + $result = true; + $event = new PostEvent('setItem.post', $adapter, new ArrayObject(array( + 'options' => array(), + )), $result); + $this->_plugin->clearExpiredByFactor($event); + + $this->assertTrue($event->getResult()); + } +} diff --git a/test/Storage/Plugin/CommonPluginTest.php b/test/Storage/Plugin/CommonPluginTest.php new file mode 100644 index 000000000..144ee069f --- /dev/null +++ b/test/Storage/Plugin/CommonPluginTest.php @@ -0,0 +1,45 @@ +_plugin->getOptions(); + $this->assertInstanceOf('Zend\Cache\Storage\Plugin\PluginOptions', $options); + } + + public function testOptionsGetAndSetDefault() + { + $options = $this->_plugin->getOptions(); + $this->_plugin->setOptions($options); + $this->assertSame($options, $this->_plugin->getOptions()); + } +} diff --git a/test/Storage/Plugin/ExceptionHandlerTest.php b/test/Storage/Plugin/ExceptionHandlerTest.php new file mode 100644 index 000000000..16e414ec4 --- /dev/null +++ b/test/Storage/Plugin/ExceptionHandlerTest.php @@ -0,0 +1,138 @@ +_adapter = new MockAdapter(); + $this->_options = new Cache\Storage\Plugin\PluginOptions(); + $this->_plugin = new Cache\Storage\Plugin\ExceptionHandler(); + $this->_plugin->setOptions($this->_options); + + parent::setUp(); + } + + public function testAddPlugin() + { + $this->_adapter->addPlugin($this->_plugin); + + // check attached callbacks + $expectedListeners = array( + 'getItem.exception' => 'onException', + 'getItems.exception' => 'onException', + + 'hasItem.exception' => 'onException', + 'hasItems.exception' => 'onException', + + 'getMetadata.exception' => 'onException', + 'getMetadatas.exception' => 'onException', + + 'setItem.exception' => 'onException', + 'setItems.exception' => 'onException', + + 'addItem.exception' => 'onException', + 'addItems.exception' => 'onException', + + 'replaceItem.exception' => 'onException', + 'replaceItems.exception' => 'onException', + + 'touchItem.exception' => 'onException', + 'touchItems.exception' => 'onException', + + 'removeItem.exception' => 'onException', + 'removeItems.exception' => 'onException', + + 'checkAndSetItem.exception' => 'onException', + + 'incrementItem.exception' => 'onException', + 'incrementItems.exception' => 'onException', + + 'decrementItem.exception' => 'onException', + 'decrementItems.exception' => 'onException', + ); + foreach ($expectedListeners as $eventName => $expectedCallbackMethod) { + $listeners = $this->_adapter->getEventManager()->getListeners($eventName); + + // event should attached only once + $this->assertSame(1, $listeners->count()); + + // check expected callback method + $cb = $listeners->top()->getCallback(); + $this->assertArrayHasKey(0, $cb); + $this->assertSame($this->_plugin, $cb[0]); + $this->assertArrayHasKey(1, $cb); + $this->assertSame($expectedCallbackMethod, $cb[1]); + } + } + + public function testRemovePlugin() + { + $this->_adapter->addPlugin($this->_plugin); + $this->_adapter->removePlugin($this->_plugin); + + // no events should be attached + $this->assertEquals(0, count($this->_adapter->getEventManager()->getEvents())); + } + + public function testOnExceptionCallCallback() + { + $expectedException = new \Exception(); + $callbackCalled = false; + + $this->_options->setExceptionCallback(function ($exception) use ($expectedException, &$callbackCalled) { + $callbackCalled = ($exception === $expectedException); + }); + + // run onException + $result = null; + $event = new ExceptionEvent('getItem.exception', $this->_adapter, new ArrayObject(array( + 'key' => 'key', + 'options' => array() + )), $result, $expectedException); + $this->_plugin->onException($event); + + $this->assertTrue( + $callbackCalled, + "Expected callback wasn't called or the expected exception wasn't the first argument" + ); + } + + public function testDontThrowException() + { + $this->_options->setThrowExceptions(false); + + // run onException + $result = 'test'; + $event = new ExceptionEvent('getItem.exception', $this->_adapter, new ArrayObject(array( + 'key' => 'key', + 'options' => array() + )), $result, new \Exception()); + $this->_plugin->onException($event); + + $this->assertFalse($event->getThrowException()); + $this->assertSame('test', $event->getResult()); + } +} diff --git a/test/Storage/Plugin/IgnoreUserAbortTest.php b/test/Storage/Plugin/IgnoreUserAbortTest.php new file mode 100644 index 000000000..e97d687c2 --- /dev/null +++ b/test/Storage/Plugin/IgnoreUserAbortTest.php @@ -0,0 +1,116 @@ +_adapter = $this->getMockForAbstractClass('Zend\Cache\Storage\Adapter\AbstractAdapter'); + $this->_options = new Cache\Storage\Plugin\PluginOptions(); + $this->_plugin = new Cache\Storage\Plugin\IgnoreUserAbort(); + $this->_plugin->setOptions($this->_options); + } + + public function testAddPlugin() + { + $this->_adapter->addPlugin($this->_plugin); + + // check attached callbacks + $expectedListeners = array( + 'setItem.pre' => 'onBefore', + 'setItem.post' => 'onAfter', + 'setItem.exception' => 'onAfter', + + 'setItems.pre' => 'onBefore', + 'setItems.post' => 'onAfter', + 'setItems.exception' => 'onAfter', + + 'addItem.pre' => 'onBefore', + 'addItem.post' => 'onAfter', + 'addItem.exception' => 'onAfter', + + 'addItems.pre' => 'onBefore', + 'addItems.post' => 'onAfter', + 'addItems.exception' => 'onAfter', + + 'replaceItem.pre' => 'onBefore', + 'replaceItem.post' => 'onAfter', + 'replaceItem.exception' => 'onAfter', + + 'replaceItems.pre' => 'onBefore', + 'replaceItems.post' => 'onAfter', + 'replaceItems.exception' => 'onAfter', + + 'checkAndSetItem.pre' => 'onBefore', + 'checkAndSetItem.post' => 'onAfter', + 'checkAndSetItem.exception' => 'onAfter', + + 'incrementItem.pre' => 'onBefore', + 'incrementItem.post' => 'onAfter', + 'incrementItem.exception' => 'onAfter', + + 'incrementItems.pre' => 'onBefore', + 'incrementItems.post' => 'onAfter', + 'incrementItems.exception' => 'onAfter', + + 'decrementItem.pre' => 'onBefore', + 'decrementItem.post' => 'onAfter', + 'decrementItem.exception' => 'onAfter', + + 'decrementItems.pre' => 'onBefore', + 'decrementItems.post' => 'onAfter', + 'decrementItems.exception' => 'onAfter', + ); + foreach ($expectedListeners as $eventName => $expectedCallbackMethod) { + $listeners = $this->_adapter->getEventManager()->getListeners($eventName); + + // event should attached only once + $this->assertSame(1, $listeners->count()); + + // check expected callback method + $cb = $listeners->top()->getCallback(); + $this->assertArrayHasKey(0, $cb); + $this->assertSame($this->_plugin, $cb[0]); + $this->assertArrayHasKey(1, $cb); + $this->assertSame($expectedCallbackMethod, $cb[1]); + } + } + + public function testRemovePlugin() + { + $this->_adapter->addPlugin($this->_plugin); + $this->_adapter->removePlugin($this->_plugin); + + // no events should be attached + $this->assertEquals(0, count($this->_adapter->getEventManager()->getEvents())); + } +} diff --git a/test/Storage/Plugin/OptimizeByFactorTest.php b/test/Storage/Plugin/OptimizeByFactorTest.php new file mode 100644 index 000000000..99563d80c --- /dev/null +++ b/test/Storage/Plugin/OptimizeByFactorTest.php @@ -0,0 +1,90 @@ +_adapter = new OptimizableMockAdapter(); + $this->_options = new Cache\Storage\Plugin\PluginOptions(array( + 'optimizing_factor' => 1, + )); + $this->_plugin = new Cache\Storage\Plugin\OptimizeByFactor(); + $this->_plugin->setOptions($this->_options); + } + + public function testAddPlugin() + { + $this->_adapter->addPlugin($this->_plugin); + + // check attached callbacks + $expectedListeners = array( + 'removeItem.post' => 'optimizeByFactor', + 'removeItems.post' => 'optimizeByFactor', + ); + foreach ($expectedListeners as $eventName => $expectedCallbackMethod) { + $listeners = $this->_adapter->getEventManager()->getListeners($eventName); + + // event should attached only once + $this->assertSame(1, $listeners->count()); + + // check expected callback method + $cb = $listeners->top()->getCallback(); + $this->assertArrayHasKey(0, $cb); + $this->assertSame($this->_plugin, $cb[0]); + $this->assertArrayHasKey(1, $cb); + $this->assertSame($expectedCallbackMethod, $cb[1]); + } + } + + public function testRemovePlugin() + { + $this->_adapter->addPlugin($this->_plugin); + $this->_adapter->removePlugin($this->_plugin); + + // no events should be attached + $this->assertEquals(0, count($this->_adapter->getEventManager()->getEvents())); + } + + public function testOptimizeByFactor() + { + $adapter = $this->getMock(get_class($this->_adapter), array('optimize')); + + // test optimize will be called + $adapter + ->expects($this->once()) + ->method('optimize'); + + // call event callback + $result = true; + $event = new PostEvent('removeItem.post', $adapter, new ArrayObject(array( + 'options' => array() + )), $result); + + $this->_plugin->optimizeByFactor($event); + + $this->assertTrue($event->getResult()); + } +} diff --git a/test/Storage/Plugin/SerializerTest.php b/test/Storage/Plugin/SerializerTest.php new file mode 100644 index 000000000..c089b07c0 --- /dev/null +++ b/test/Storage/Plugin/SerializerTest.php @@ -0,0 +1,123 @@ +_adapter = $this->getMockForAbstractClass('Zend\Cache\Storage\Adapter\AbstractAdapter'); + $this->_options = new Cache\Storage\Plugin\PluginOptions(); + $this->_plugin = new Cache\Storage\Plugin\Serializer(); + $this->_plugin->setOptions($this->_options); + } + + public function testAddPlugin() + { + $this->_adapter->addPlugin($this->_plugin, 100); + + // check attached callbacks + $expectedListeners = array( + 'getItem.post' => 'onReadItemPost', + 'getItems.post' => 'onReadItemsPost', + + 'setItem.pre' => 'onWriteItemPre', + 'setItems.pre' => 'onWriteItemsPre', + 'addItem.pre' => 'onWriteItemPre', + 'addItems.pre' => 'onWriteItemsPre', + 'replaceItem.pre' => 'onWriteItemPre', + 'replaceItems.pre' => 'onWriteItemsPre', + 'checkAndSetItem.pre' => 'onWriteItemPre', + + 'incrementItem.pre' => 'onIncrementItemPre', + 'incrementItems.pre' => 'onIncrementItemsPre', + 'decrementItem.pre' => 'onDecrementItemPre', + 'decrementItems.pre' => 'onDecrementItemsPre', + + 'getCapabilities.post' => 'onGetCapabilitiesPost', + ); + foreach ($expectedListeners as $eventName => $expectedCallbackMethod) { + $listeners = $this->_adapter->getEventManager()->getListeners($eventName); + + // event should attached only once + $this->assertSame(1, $listeners->count()); + + // check expected callback method + $cb = $listeners->top()->getCallback(); + $this->assertArrayHasKey(0, $cb); + $this->assertSame($this->_plugin, $cb[0]); + $this->assertArrayHasKey(1, $cb); + $this->assertSame($expectedCallbackMethod, $cb[1]); + + // check expected priority + $meta = $listeners->top()->getMetadata(); + $this->assertArrayHasKey('priority', $meta); + if (substr($eventName, -4) == '.pre') { + $this->assertSame(100, $meta['priority']); + } else { + $this->assertSame(-100, $meta['priority']); + } + } + } + + public function testRemovePlugin() + { + $this->_adapter->addPlugin($this->_plugin); + $this->_adapter->removePlugin($this->_plugin); + + // no events should be attached + $this->assertEquals(0, count($this->_adapter->getEventManager()->getEvents())); + } + + public function testUnserializeOnReadItem() + { + $value = serialize(123); + $event = new PostEvent('getItem.post', $this->_adapter, new ArrayObject(), $value); + $this->_plugin->onReadItemPost($event); + + $this->assertFalse($event->propagationIsStopped()); + $this->assertSame(123, $event->getResult()); + } + + public function testUnserializeOnReadItems() + { + $values = array('key1' => serialize(123), 'key2' => serialize(456)); + $event = new PostEvent('getItems.post', $this->_adapter, new ArrayObject(), $values); + + $this->_plugin->onReadItemsPost($event); + + $this->assertFalse($event->propagationIsStopped()); + + $values = $event->getResult(); + $this->assertSame(123, $values['key1']); + $this->assertSame(456, $values['key2']); + } +} diff --git a/test/Storage/TestAsset/ClearExpiredMockAdapter.php b/test/Storage/TestAsset/ClearExpiredMockAdapter.php new file mode 100644 index 000000000..5fc3ee4c5 --- /dev/null +++ b/test/Storage/TestAsset/ClearExpiredMockAdapter.php @@ -0,0 +1,20 @@ + 'onSetItemPre', + 'setItem.post' => 'onSetItemPost' + ); + + public function __construct($options = array()) + { + if (is_array($options)) { + $options = new Plugin\PluginOptions($options); + } + if ($options instanceof Plugin\PluginOptions) { + $this->setOptions($options); + } + } + + public function setOptions(Plugin\PluginOptions $options) + { + $this->options = $options; + return $this; + } + + public function getOptions() + { + return $this->options; + } + + public function attach(EventManagerInterface $eventCollection) + { + $handles = array(); + foreach ($this->eventCallbacks as $eventName => $method) { + $handles[] = $eventCollection->attach($eventName, array($this, $method)); + } + $this->handles[ \spl_object_hash($eventCollection) ] = $handles; + } + + public function detach(EventManagerInterface $eventCollection) + { + $index = \spl_object_hash($eventCollection); + foreach ($this->handles[$index] as $i => $handle) { + $eventCollection->detach($handle); + unset($this->handles[$index][$i]); + } + + // remove empty handles of event collection + if (!$this->handles[$index]) { + unset($this->handles[$index]); + } + } + + public function onSetItemPre(Event $event) + { + $this->calledEvents[] = $event; + } + + public function onSetItemPost(Event $event) + { + $this->calledEvents[] = $event; + } + + public function getHandles() + { + return $this->handles; + } + + public function getEventCallbacks() + { + return $this->eventCallbacks; + } + + public function getCalledEvents() + { + return $this->calledEvents; + } +} diff --git a/test/Storage/TestAsset/OptimizableMockAdapter.php b/test/Storage/TestAsset/OptimizableMockAdapter.php new file mode 100644 index 000000000..6af535dbe --- /dev/null +++ b/test/Storage/TestAsset/OptimizableMockAdapter.php @@ -0,0 +1,20 @@ +assertInstanceOf('Zend\Cache\Storage\AdapterPluginManager', $adapters); + } + + public function testChangeAdapterPluginManager() + { + $adapters = new Cache\Storage\AdapterPluginManager(); + Cache\StorageFactory::setAdapterPluginManager($adapters); + $this->assertSame($adapters, Cache\StorageFactory::getAdapterPluginManager()); + } + + public function testAdapterFactory() + { + $adapter1 = Cache\StorageFactory::adapterFactory('Memory'); + $this->assertInstanceOf('Zend\Cache\Storage\Adapter\Memory', $adapter1); + + $adapter2 = Cache\StorageFactory::adapterFactory('Memory'); + $this->assertInstanceOf('Zend\Cache\Storage\Adapter\Memory', $adapter2); + + $this->assertNotSame($adapter1, $adapter2); + } + + public function testDefaultPluginManager() + { + $manager = Cache\StorageFactory::getPluginManager(); + $this->assertInstanceOf('Zend\Cache\Storage\PluginManager', $manager); + } + + public function testChangePluginManager() + { + $manager = new Cache\Storage\PluginManager(); + Cache\StorageFactory::setPluginManager($manager); + $this->assertSame($manager, Cache\StorageFactory::getPluginManager()); + } + + public function testPluginFactory() + { + $plugin1 = Cache\StorageFactory::pluginFactory('Serializer'); + $this->assertInstanceOf('Zend\Cache\Storage\Plugin\Serializer', $plugin1); + + $plugin2 = Cache\StorageFactory::pluginFactory('Serializer'); + $this->assertInstanceOf('Zend\Cache\Storage\Plugin\Serializer', $plugin2); + + $this->assertNotSame($plugin1, $plugin2); + } + + public function testFactoryAdapterAsString() + { + $cache = Cache\StorageFactory::factory(array( + 'adapter' => 'Memory', + )); + $this->assertInstanceOf('Zend\Cache\Storage\Adapter\Memory', $cache); + } + + public function testFactoryAdapterAsArray() + { + $cache = Cache\StorageFactory::factory(array( + 'adapter' => array( + 'name' => 'Memory', + ) + )); + $this->assertInstanceOf('Zend\Cache\Storage\Adapter\Memory', $cache); + } + + public function testFactoryWithPlugins() + { + $adapter = 'Memory'; + $plugins = array('Serializer', 'ClearExpiredByFactor'); + + $cache = Cache\StorageFactory::factory(array( + 'adapter' => $adapter, + 'plugins' => $plugins, + )); + + // test adapter + $this->assertInstanceOf('Zend\Cache\Storage\Adapter\Memory', $cache); + + // test plugin structure + $i = 0; + foreach ($cache->getPluginRegistry() as $plugin) { + $this->assertInstanceOf('Zend\Cache\Storage\Plugin\\' . $plugins[$i++], $plugin); + } + } + + public function testFactoryWithPluginsAndOptionsArray() + { + $factory = array( + 'adapter' => array( + 'name' => 'Memory', + 'options' => array( + 'ttl' => 123, + 'namespace' => 'willBeOverwritten' + ), + ), + 'plugins' => array( + // plugin as a simple string entry + 'Serializer', + + // plugin as name-options pair + 'ClearExpiredByFactor' => array( + 'clearing_factor' => 1, + ), + + // plugin with full definition + array( + 'name' => 'IgnoreUserAbort', + 'priority' => 100, + 'options' => array( + 'exit_on_abort' => false, + ), + ), + ), + 'options' => array( + 'namespace' => 'test', + ) + ); + $storage = Cache\StorageFactory::factory($factory); + + // test adapter + $this->assertInstanceOf('Zend\Cache\Storage\Adapter\\' . $factory['adapter']['name'], $storage); + $this->assertEquals(123, $storage->getOptions()->getTtl()); + $this->assertEquals('test', $storage->getOptions()->getNamespace()); + + // test plugin structure + foreach ($storage->getPluginRegistry() as $plugin) { + + // test plugin options + $pluginClass = get_class($plugin); + switch ($pluginClass) { + case 'Zend\Cache\Storage\Plugin\ClearExpiredByFactor': + $this->assertSame( + $factory['plugins']['ClearExpiredByFactor']['clearing_factor'], + $plugin->getOptions()->getClearingFactor() + ); + break; + + case 'Zend\Cache\Storage\Plugin\Serializer': + break; + + case 'Zend\Cache\Storage\Plugin\IgnoreUserAbort': + $this->assertFalse($plugin->getOptions()->getExitOnAbort()); + break; + + default: + $this->fail("Unexpected plugin class '{$pluginClass}'"); + } + + } + } +} diff --git a/test/TestAsset/DummyPattern.php b/test/TestAsset/DummyPattern.php new file mode 100644 index 000000000..7cc461e73 --- /dev/null +++ b/test/TestAsset/DummyPattern.php @@ -0,0 +1,30 @@ +_dummyOption = $value; + return $this; + } + + public function getDummyOption() + { + return $this->_dummyOption; + } +} diff --git a/test/TestAsset/DummyStorageAdapter.php b/test/TestAsset/DummyStorageAdapter.php new file mode 100644 index 000000000..f8e333fcd --- /dev/null +++ b/test/TestAsset/DummyStorageAdapter.php @@ -0,0 +1,17 @@ +setOptions($options); + } +} diff --git a/test/bootstrap.php b/test/bootstrap.php new file mode 100644 index 000000000..f6a38110b --- /dev/null +++ b/test/bootstrap.php @@ -0,0 +1,34 @@ + Date: Sat, 1 Sep 2012 20:40:03 +0200 Subject: [PATCH 4/5] Resolve more mismatched phpDoc --- src/Pattern/PatternOptions.php | 6 +++--- src/Storage/Adapter/AbstractAdapter.php | 5 ++--- src/Storage/Adapter/AbstractZendServer.php | 3 +-- src/Storage/Adapter/Apc.php | 4 ++-- src/Storage/Adapter/Filesystem.php | 4 ++-- src/Storage/Adapter/FilesystemOptions.php | 2 +- src/Storage/Adapter/Memcached.php | 1 - src/Storage/Adapter/MemcachedOptions.php | 1 + src/Storage/Adapter/Memory.php | 7 ++----- src/Storage/Capabilities.php | 6 +++--- 10 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/Pattern/PatternOptions.php b/src/Pattern/PatternOptions.php index 3090e4b79..dce78c9d2 100644 --- a/src/Pattern/PatternOptions.php +++ b/src/Pattern/PatternOptions.php @@ -152,7 +152,7 @@ class PatternOptions extends AbstractOptions * Constructor * * @param array|Traversable|null $options - * @return AbstractOptions + * @return PatternOptions * @throws Exception\InvalidArgumentException */ public function __construct($options = null) @@ -492,7 +492,7 @@ public function getIndexFilename() /** * Set object to cache * - * @param mixed $value + * @param mixed $object * @return $this */ public function setObject($object) @@ -575,7 +575,7 @@ public function getObjectCacheMethods() * Used by: * - ObjectCache * - * @param mixed $value + * @param mixed $objectKey * @return $this */ public function setObjectKey($objectKey) diff --git a/src/Storage/Adapter/AbstractAdapter.php b/src/Storage/Adapter/AbstractAdapter.php index 9e0e7e53f..61a950042 100644 --- a/src/Storage/Adapter/AbstractAdapter.php +++ b/src/Storage/Adapter/AbstractAdapter.php @@ -538,7 +538,7 @@ public function hasItems(array $keys) /** * Internal method to test multiple items. * - * @param array $keys + * @param array $normalizedKeys * @return array Array of found keys * @throws Exception\ExceptionInterface */ @@ -750,7 +750,6 @@ public function setItems(array $keyValuePairs) * Internal method to store multiple items. * * @param array $normalizedKeyValuePairs - * @param array $normalizedOptions * @return array Array of not stored keys * @throws Exception\ExceptionInterface */ @@ -1234,7 +1233,7 @@ public function removeItems(array $keys) /** * Internal method to remove multiple items. * - * @param array $keys + * @param array $normalizedKeys * @return array Array of not removed keys * @throws Exception\ExceptionInterface */ diff --git a/src/Storage/Adapter/AbstractZendServer.php b/src/Storage/Adapter/AbstractZendServer.php index b18991add..d5d9ffc92 100644 --- a/src/Storage/Adapter/AbstractZendServer.php +++ b/src/Storage/Adapter/AbstractZendServer.php @@ -87,7 +87,6 @@ protected function internalGetItems(array & $normalizedKeys) * Internal method to test if an item exists. * * @param string $normalizedKey - * @param array $normalizedOptions * @return boolean * @throws Exception\ExceptionInterface */ @@ -101,7 +100,7 @@ protected function internalHasItem(& $normalizedKey) /** * Internal method to test multiple items. * - * @param array $keys + * @param array $normalizedKeys * @return array Array of found keys * @throws Exception\ExceptionInterface */ diff --git a/src/Storage/Adapter/Apc.php b/src/Storage/Adapter/Apc.php index c5cee10e0..b380672f4 100644 --- a/src/Storage/Adapter/Apc.php +++ b/src/Storage/Adapter/Apc.php @@ -265,7 +265,7 @@ protected function internalHasItem(& $normalizedKey) /** * Internal method to test multiple items. * - * @param array $keys + * @param array $normalizedKeys * @return array Array of found keys * @throws Exception\ExceptionInterface */ @@ -523,7 +523,7 @@ protected function internalRemoveItem(& $normalizedKey) /** * Internal method to remove multiple items. * - * @param array $keys + * @param array $normalizedKeys * @return array Array of not removed keys * @throws Exception\ExceptionInterface */ diff --git a/src/Storage/Adapter/Filesystem.php b/src/Storage/Adapter/Filesystem.php index a87257a9a..4454c7d05 100644 --- a/src/Storage/Adapter/Filesystem.php +++ b/src/Storage/Adapter/Filesystem.php @@ -615,7 +615,6 @@ public function hasItems(array $keys) * Internal method to test if an item exists. * * @param string $normalizedKey - * @param array $normalizedOptions * @return boolean * @throws Exception\ExceptionInterface */ @@ -665,6 +664,7 @@ public function getMetadata($key) * Get metadatas * * @param array $keys + * @param array $options * @return array Associative array of keys and metadata */ public function getMetadatas(array $keys, array $options = array()) @@ -1032,7 +1032,7 @@ public function touchItems(array $keys) /** * Internal method to reset lifetime of an item * - * @param string $key + * @param string $normalizedKey * @return boolean * @throws Exception\ExceptionInterface */ diff --git a/src/Storage/Adapter/FilesystemOptions.php b/src/Storage/Adapter/FilesystemOptions.php index e27de8578..bc472f92c 100644 --- a/src/Storage/Adapter/FilesystemOptions.php +++ b/src/Storage/Adapter/FilesystemOptions.php @@ -107,7 +107,7 @@ class FilesystemOptions extends AdapterOptions * Constructor * * @param array|Traversable|null $options - * @return AbstractOptions + * @return FilesystemOptions * @throws Exception\InvalidArgumentException */ public function __construct($options = null) diff --git a/src/Storage/Adapter/Memcached.php b/src/Storage/Adapter/Memcached.php index cbd77d8c8..d0f5a955b 100644 --- a/src/Storage/Adapter/Memcached.php +++ b/src/Storage/Adapter/Memcached.php @@ -17,7 +17,6 @@ use Traversable; use Zend\Cache\Exception; use Zend\Cache\Storage\AvailableSpaceCapableInterface; -use Zend\Cache\Storage\CallbackEvent; use Zend\Cache\Storage\Capabilities; use Zend\Cache\Storage\Event; use Zend\Cache\Storage\FlushableInterface; diff --git a/src/Storage/Adapter/MemcachedOptions.php b/src/Storage/Adapter/MemcachedOptions.php index 89c627786..8f0d3418e 100644 --- a/src/Storage/Adapter/MemcachedOptions.php +++ b/src/Storage/Adapter/MemcachedOptions.php @@ -176,6 +176,7 @@ public function getLibOptions() /** * Get libmemcached option * + * @param string|int $key * @return mixed * @link http://php.net/manual/memcached.constants.php */ diff --git a/src/Storage/Adapter/Memory.php b/src/Storage/Adapter/Memory.php index a779f735e..ba462e717 100644 --- a/src/Storage/Adapter/Memory.php +++ b/src/Storage/Adapter/Memory.php @@ -115,7 +115,7 @@ public function getAvailableSpace() /** * Get the storage iterator * - * @return MemoryIterator + * @return KeyListIterator */ public function getIterator() { @@ -346,9 +346,7 @@ protected function internalGetItems(array & $normalizedKeys) * Internal method to test if an item exists. * * @param string $normalizedKey - * @param array $normalizedOptions * @return boolean - * @throws Exception\ExceptionInterface */ protected function internalHasItem(& $normalizedKey) { @@ -370,9 +368,8 @@ protected function internalHasItem(& $normalizedKey) /** * Internal method to test multiple items. * - * @param array $keys + * @param array $normalizedKeys * @return array Array of found keys - * @throws Exception\ExceptionInterface */ protected function internalHasItems(array & $normalizedKeys) { diff --git a/src/Storage/Capabilities.php b/src/Storage/Capabilities.php index 97dce7604..d3dcf4197 100644 --- a/src/Storage/Capabilities.php +++ b/src/Storage/Capabilities.php @@ -156,7 +156,7 @@ class Capabilities /** * Constructor * - * @param Adapter $adapter + * @param StorageInterface $storage * @param stdClass $marker * @param array $capabilities * @param null|Capabilities $baseCapabilities @@ -179,7 +179,7 @@ public function __construct( /** * Get the storage adapter * - * @return Adapter + * @return StorageInterface */ public function getAdapter() { @@ -514,7 +514,7 @@ protected function getCapability($property, $default = null) * Change a capability * * @param stdClass $marker - * @param string $name + * @param string $property * @param mixed $value * @return Capabilities Fluent interface * @throws Exception\InvalidArgumentException From 6cb2de0226c6d7d9412c35b43d2fbb9d6930b0bb Mon Sep 17 00:00:00 2001 From: Michel Hunziker Date: Sat, 1 Sep 2012 22:09:27 +0200 Subject: [PATCH 5/5] Add more missing @throws annotations --- src/Pattern/PatternOptions.php | 6 ++++++ src/Storage/Capabilities.php | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/Pattern/PatternOptions.php b/src/Pattern/PatternOptions.php index dce78c9d2..02b03ff70 100644 --- a/src/Pattern/PatternOptions.php +++ b/src/Pattern/PatternOptions.php @@ -235,6 +235,7 @@ public function getCacheOutput() * - ClassCache * * @param string $class + * @throws Exception\InvalidArgumentException * @return PatternOptions */ public function setClass($class) @@ -319,6 +320,7 @@ public function getClassNonCacheMethods() * Set directory permission * * @param false|int $dirPermission + * @throws Exception\InvalidArgumentException * @return PatternOptions */ public function setDirPermission($dirPermission) @@ -359,6 +361,7 @@ public function getDirPermission() * - CaptureCache * * @param false|int $umask + * @throws Exception\InvalidArgumentException * @return PatternOptions */ public function setUmask($umask) @@ -430,6 +433,7 @@ public function getFileLocking() * Set file permission * * @param false|int $filePermission + * @throws Exception\InvalidArgumentException * @return PatternOptions */ public function setFilePermission($filePermission) @@ -493,6 +497,7 @@ public function getIndexFilename() * Set object to cache * * @param mixed $object + * @throws Exception\InvalidArgumentException * @return $this */ public function setObject($object) @@ -634,6 +639,7 @@ public function getObjectNonCacheMethods() * - CaptureCache * * @param string $publicDir + * @throws Exception\InvalidArgumentException * @return PatternOptions */ public function setPublicDir($publicDir) diff --git a/src/Storage/Capabilities.php b/src/Storage/Capabilities.php index d3dcf4197..de0d3f07d 100644 --- a/src/Storage/Capabilities.php +++ b/src/Storage/Capabilities.php @@ -210,6 +210,7 @@ public function getSupportedDatatypes() * * @param stdClass $marker * @param array $datatypes + * @throws Exception\InvalidArgumentException * @return Capabilities Fluent interface */ public function setSupportedDatatypes(stdClass $marker, array $datatypes) @@ -265,6 +266,7 @@ public function getSupportedMetadata() * * @param stdClass $marker * @param string[] $metadata + * @throws Exception\InvalidArgumentException * @return Capabilities Fluent interface */ public function setSupportedMetadata(stdClass $marker, array $metadata) @@ -292,6 +294,7 @@ public function getMinTtl() * * @param stdClass $marker * @param int $minTtl + * @throws Exception\InvalidArgumentException * @return Capabilities Fluent interface */ public function setMinTtl(stdClass $marker, $minTtl) @@ -318,6 +321,7 @@ public function getMaxTtl() * * @param stdClass $marker * @param int $maxTtl + * @throws Exception\InvalidArgumentException * @return Capabilities Fluent interface */ public function setMaxTtl(stdClass $marker, $maxTtl) @@ -367,6 +371,7 @@ public function getTtlPrecision() * * @param stdClass $marker * @param float $ttlPrecision + * @throws Exception\InvalidArgumentException * @return Capabilities Fluent interface */ public function setTtlPrecision(stdClass $marker, $ttlPrecision) @@ -437,6 +442,7 @@ public function getMaxKeyLength() * * @param stdClass $marker * @param int $maxKeyLength + * @throws Exception\InvalidArgumentException * @return Capabilities Fluent interface */ public function setMaxKeyLength(stdClass $marker, $maxKeyLength)