diff --git a/src/Exception/OutOfCapacityException.php b/src/Exception/OutOfCapacityException.php new file mode 100644 index 000000000..8c81c8ff3 --- /dev/null +++ b/src/Exception/OutOfCapacityException.php @@ -0,0 +1,33 @@ +setOptions($options); + } + } + /** * Destructor * @@ -331,13 +345,8 @@ public function removePlugin(Plugin $plugin) $registry = $this->getPluginRegistry(); if ($registry->contains($plugin)) { $plugin->detach($this->events()); - } else { - throw new Exception\LogicException(sprintf( - 'Plugin of type "%s" already removed', - get_class($plugin) - )); + $registry->detach($plugin); } - $registry->detach($plugin); return $this; } @@ -557,10 +566,10 @@ public function replaceItems(array $keyValuePairs, array $options = array()) /** * Check and set item * - * @param string $token - * @param string|int $key - * @param mixed $value - * @param array $options + * @param mixed $token + * @param string $key + * @param mixed $value + * @param array $options * @return bool */ public function checkAndSetItem($token, $key, $value, array $options = array()) @@ -739,6 +748,19 @@ public function decrementItems(array $keyValuePairs, array $options = array()) /** * Get delayed * + * Options: + * - ttl optional + * - The time-to-live (Default: ttl of object) + * - namespace optional + * - The namespace to use (Default: namespace of object) + * - select optional + * - An array of the information the returned item contains + * (Default: array('key', 'value')) + * - callback optional + * - An result callback will be invoked for each item in the result set. + * - The first argument will be the item array. + * - The callback does not have to return anything. + * * @param array $keys * @param array $options * @return bool @@ -758,7 +780,6 @@ public function getDelayed(array $keys, array $options = array()) } $this->normalizeOptions($options); - if (!isset($options['select'])) { $options['select'] = array('key', 'value'); } diff --git a/src/Storage/Adapter/AdapterOptions.php b/src/Storage/Adapter/AdapterOptions.php index 20bb0101a..6db8295f2 100644 --- a/src/Storage/Adapter/AdapterOptions.php +++ b/src/Storage/Adapter/AdapterOptions.php @@ -86,7 +86,7 @@ class AdapterOptions extends Options /** * Cast to array - * + * * @return array */ public function toArray() @@ -178,7 +178,7 @@ public function getKeyPattern() */ public function setNamespace($namespace) { - $nameapace = (string)$namespace; + $namespace = (string)$namespace; if ($namespace === '') { throw new Exception\InvalidArgumentException('No namespace given'); } diff --git a/src/Storage/Adapter/Apc.php b/src/Storage/Adapter/Apc.php index 23d23ba52..32f2838fc 100644 --- a/src/Storage/Adapter/Apc.php +++ b/src/Storage/Adapter/Apc.php @@ -64,11 +64,11 @@ class Apc extends AbstractAdapter /** * Constructor * - * @param array $options Option + * @param null|array|Traversable|ApcOptions $options * @throws Exception * @return void */ - public function __construct() + public function __construct($options = null) { if (version_compare('3.1.6', phpversion('apc')) > 0) { throw new Exception\ExtensionNotLoadedException("Missing ext/apc >= 3.1.6"); @@ -103,6 +103,8 @@ public function __construct() 'internal_key' => \APC_ITER_KEY, ); } + + parent::__construct($options); } /* options */ @@ -116,8 +118,8 @@ public function __construct() */ public function setOptions($options) { - if (!is_array($options) - && !$options instanceof Traversable + if (!is_array($options) + && !$options instanceof Traversable && !$options instanceof ApcOptions ) { throw new Exception\InvalidArgumentException(sprintf( diff --git a/src/Storage/Adapter/Filesystem.php b/src/Storage/Adapter/Filesystem.php index 049a5ab72..5f6df9b94 100644 --- a/src/Storage/Adapter/Filesystem.php +++ b/src/Storage/Adapter/Filesystem.php @@ -75,13 +75,13 @@ class Filesystem extends AbstractAdapter * Set options. * * @param array|Traversable|FilesystemOptions $options - * @return FilesystemAdapter + * @return Filesystem * @see getOptions() */ public function setOptions($options) { - if (!is_array($options) - && !$options instanceof Traversable + if (!is_array($options) + && !$options instanceof Traversable && !$options instanceof FilesystemOptions ) { throw new Exception\InvalidArgumentException(sprintf( @@ -648,10 +648,10 @@ public function addItems(array $keyValuePairs, array $options = array()) /** * check and set item * - * @param $token - * @param $key - * @param $value - * @param array $options + * @param string $token + * @param string $key + * @param mixed $value + * @param array $options * @return bool|mixed * @throws ItemNotFoundException */ @@ -1320,7 +1320,7 @@ protected function internalHasItem($key, array &$options) * @return array|bool * @throws ItemNotFoundException */ - protected function internalGetMetadata($key, array &$options) + protected function internalGetMetadata($key, array &$options) { $keyInfo = $this->getKeyInfo($key, $options['namespace']); if (!$keyInfo) { @@ -1537,7 +1537,7 @@ protected function clearByPrefix($prefix, $mode, array &$opts) // if MATCH_TAGS mode -> check if all given tags available in current cache if (($mode & self::MATCH_TAGS) == self::MATCH_TAGS ) { - if (!isset($info['tags']) + if (!isset($info['tags']) || count(array_diff($opts['tags'], $info['tags'])) > 0 ) { continue; @@ -1545,7 +1545,7 @@ protected function clearByPrefix($prefix, $mode, array &$opts) // if MATCH_NO_TAGS mode -> check if no given tag available in current cache } elseif(($mode & self::MATCH_NO_TAGS) == self::MATCH_NO_TAGS) { - if (isset($info['tags']) + if (isset($info['tags']) && count(array_diff($opts['tags'], $info['tags'])) != count($opts['tags']) ) { continue; @@ -1553,7 +1553,7 @@ protected function clearByPrefix($prefix, $mode, array &$opts) // if MATCH_ANY_TAGS mode -> check if any given tag available in current cache } elseif ( ($mode & self::MATCH_ANY_TAGS) == self::MATCH_ANY_TAGS ) { - if (!isset($info['tags']) + if (!isset($info['tags']) || count(array_diff($opts['tags'], $info['tags'])) == count($opts['tags']) ) { continue; @@ -1686,7 +1686,7 @@ protected function getFileSpec($key, $ns) * @return array|boolean The info array or false if file wasn't found * @throws Exception\RuntimeException */ - protected function readInfoFile($file) + protected function readInfoFile($file) { if (!file_exists($file)) { return false; @@ -1718,10 +1718,10 @@ protected function getFileContent($file) if ($this->getOptions()->getFileLocking()) { set_error_handler(function($errno, $errstr = '', $errfile = '', $errline = 0) use ($file) { $message = sprintf( - 'Error getting contents from file "%s" (in %s@%d): %s', - $file, - $errfile, - $errline, + 'Error getting contents from file "%s" (in %s@%d): %s', + $file, + $errfile, + $errline, $errstr ); throw new Exception\RuntimeException($message, $errno); @@ -1730,17 +1730,17 @@ protected function getFileContent($file) restore_error_handler(); if ($fp === false) { throw new Exception\RuntimeException(sprintf( - 'Unknown error getting contents from file "%s"', + 'Unknown error getting contents from file "%s"', $file )); } set_error_handler(function($errno, $errstr = '', $errfile = '', $errline = 0) use ($file) { $message = sprintf( - 'Error locking file "%s" (in %s@%d): %s', - $file, - $errfile, - $errline, + 'Error locking file "%s" (in %s@%d): %s', + $file, + $errfile, + $errline, $errstr ); throw new Exception\RuntimeException($message, $errno); @@ -1859,7 +1859,7 @@ protected function putFileContent($file, $data) * @return void * @throw RuntimeException */ - protected function unlink($file) + protected function unlink($file) { // If file does not exist, nothing to do if (!file_exists($file)) { diff --git a/src/Storage/Adapter/Memcached.php b/src/Storage/Adapter/Memcached.php index 7dab2740e..98b6931f7 100644 --- a/src/Storage/Adapter/Memcached.php +++ b/src/Storage/Adapter/Memcached.php @@ -23,6 +23,7 @@ use ArrayObject, Memcached as MemcachedResource, + MemcachedException, stdClass, Traversable, Zend\Cache\Exception, @@ -38,6 +39,13 @@ */ class Memcached extends AbstractAdapter { + /** + * Major version of ext/memcached + * + * @var null|int + */ + protected static $extMemcachedMajorVersion; + /** * Memcached instance * @@ -48,25 +56,28 @@ class Memcached extends AbstractAdapter /** * Constructor * - * @param array $options Option + * @param null|array|Traversable|MemcachedOptions $options * @throws Exception * @return void */ public function __construct($options = null) { - if (!extension_loaded('memcached')) { - throw new Exception\ExtensionNotLoadedException("Memcached extension is not loaded"); + if (static::$extMemcachedMajorVersion === null) { + $v = (string) phpversion('memcached'); + static::$extMemcachedMajorVersion = ($v !== '') ? (int)$v[0] : 0; } - - $this->memcached= new MemcachedResource(); - - if (!empty($options)) { - $this->setOptions($options); + + if (static::$extMemcachedMajorVersion < 1) { + throw new Exception\ExtensionNotLoadedException('Need ext/memcached version >= 1.0.0'); } - $options= $this->getOptions(); + $this->memcached = new MemcachedResource(); + + parent::__construct($options); + + $options = $this->getOptions(); $this->memcached->addServer($options->getServer(), $options->getPort()); - + } /* options */ @@ -74,15 +85,15 @@ public function __construct($options = null) /** * Set options. * - * @param string|Traversable|MemcachedOptions $options + * @param array|Traversable|MemcachedOptions $options * @return Memcached * @see getOptions() */ public function setOptions($options) { - if (!is_array($options) - && !$options instanceof Traversable - && !$options instanceof MemcachedOptions + if (!is_array($options) + && !$options instanceof Traversable + && !$options instanceof MemcachedOptions ) { throw new Exception\InvalidArgumentException(sprintf( '%s expects an array, a Traversable object, or a MemcachedOptions object; ' @@ -125,15 +136,12 @@ public function getOptions() return $this->options; } - /* reading */ /** * Get an item. * * Options: - * - ttl optional - * - The time-to-life (Default: ttl of object) * - namespace optional * - The namespace to use (Default: namespace of object) * - ignore_missing_items optional @@ -168,15 +176,18 @@ public function getItem($key, array $options = array()) return $eventRs->last(); } - $internalKey = $options['namespace'] . $baseOptions->getNamespaceSeparator() . $key; - $result = $this->memcached->get($internalKey); - if ($result===false) { - if (!$options['ignore_missing_items']) { - throw new Exception\ItemNotFoundException("Key '{$internalKey}' not found"); - } + $this->memcached->setOption(MemcachedResource::OPT_PREFIX_KEY, $options['namespace']); + if (array_key_exists('token', $options)) { + $result = $this->memcached->get($key, null, $options['token']); } else { - if (array_key_exists('token', $options)) { - $options['token'] = $result; + $result = $this->memcached->get($key); + } + + if ($result === false) { + if (($rsCode = $this->memcached->getResultCode()) != 0 + && ($rsCode != MemcachedResource::RES_NOTFOUND || !$options['ignore_missing_items']) + ) { + throw $this->getExceptionByResultCode($rsCode); } } @@ -190,8 +201,6 @@ public function getItem($key, array $options = array()) * Get multiple items. * * Options: - * - ttl optional - * - The time-to-life (Default: ttl of object) * - namespace optional * - The namespace to use (Default: namespace of object) * @@ -223,23 +232,10 @@ public function getItems(array $keys, array $options = array()) return $eventRs->last(); } - $namespaceSep = $baseOptions->getNamespaceSeparator(); - $internalKeys = array(); - foreach ($keys as $key) { - $internalKeys[] = $options['namespace'] . $namespaceSep . $key; - } - - $fetch = $this->memcached->getMulti($internalKeys); - - if ($fetch===false) { - throw new Exception\ItemNotFoundException('Memcached::getMulti() failed'); - } - - // remove namespace prefix - $prefixL = strlen($options['namespace'] . $namespaceSep); - $result = array(); - foreach ($fetch as $internalKey => &$value) { - $result[ substr($internalKey, $prefixL) ] = $value; + $this->memcached->setOption(MemcachedResource::OPT_PREFIX_KEY, $options['namespace']); + $result = $this->memcached->getMulti($keys); + if ($result === false) { + throw $this->getExceptionByResultCode($this->memcached->getResultCode()); } return $this->triggerPost(__FUNCTION__, $args, $result); @@ -252,8 +248,6 @@ public function getItems(array $keys, array $options = array()) * Get metadata of an item. * * Options: - * - ttl optional - * - The time-to-life (Default: ttl of object) * - namespace optional * - The namespace to use (Default: namespace of object) * - ignore_missing_items optional @@ -270,7 +264,41 @@ public function getItems(array $keys, array $options = array()) */ public function getMetadata($key, array $options = array()) { - throw new Exception\UnsupportedMethodCallException(__FUNCTION__ . ' is not supported by the adapter'); + $baseOptions = $this->getOptions(); + if (!$baseOptions->getReadable()) { + return false; + } + + $this->normalizeOptions($options); + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'key' => & $key, + 'options' => & $options, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $this->memcached->setOption(MemcachedResource::OPT_PREFIX_KEY, $options['namespace']); + $result = $this->memcached->get($key); + + if ($result === false) { + if (($rsCode = $this->memcached->getResultCode()) != 0 + && ($rsCode != MemcachedResource::RES_NOTFOUND || !$options['ignore_missing_items']) + ) { + throw $this->getExceptionByResultCode($rsCode); + } + } else { + $result = array(); + } + + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + return $this->triggerException(__FUNCTION__, $args, $e); + } } /* writing */ @@ -279,10 +307,10 @@ public function getMetadata($key, array $options = array()) * Store an item. * * Options: + * - ttl optional + * - The time-to-live (Default: ttl of object) * - namespace optional * - The namespace to use (Default: namespace of object) - * - tags optional - * - An array of tags * * @param string $key * @param mixed $value @@ -315,12 +343,11 @@ public function setItem($key, $value, array $options = array()) return $eventRs->last(); } - $internalKey = $options['namespace'] . $baseOptions->getNamespaceSeparator() . $key; - if (!$this->memcached->set($internalKey, $value, $options['ttl'])) { - $type = is_object($value) ? get_class($value) : gettype($value); - throw new Exception\RuntimeException( - "Memcached::set('{$internalKey}', <{$type}>, {$options['ttl']}) failed" - ); + $this->memcached->setOption(MemcachedResource::OPT_PREFIX_KEY, $options['namespace']); + + $expiration = $this->expirationTime($options['ttl']); + if (!$this->memcached->set($key, $value, $expiration)) { + throw $this->getExceptionByResultCode($this->memcached->getResultCode()); } $result = true; @@ -334,10 +361,10 @@ public function setItem($key, $value, array $options = array()) * Store multiple items. * * Options: + * - ttl optional + * - The time-to-live (Default: ttl of object) * - namespace optional * - The namespace to use (Default: namespace of object) - * - tags optional - * - An array of tags * * @param array $keyValuePairs * @param array $options @@ -367,16 +394,11 @@ public function setItems(array $keyValuePairs, array $options = array()) return $eventRs->last(); } - $internalKeyValuePairs = array(); - $prefix = $options['namespace'] . $baseOptions->getNamespaceSeparator(); - foreach ($keyValuePairs as $key => &$value) { - $internalKey = $prefix . $key; - $internalKeyValuePairs[$internalKey] = &$value; - } + $this->memcached->setOption(MemcachedResource::OPT_PREFIX_KEY, $options['namespace']); - $errKeys = $this->memcached->setMulti($internalKeyValuePairs, $options['ttl']); - if ($errKeys==false) { - throw new Exception\RuntimeException("Memcached::setMulti(, {$options['ttl']}) failed"); + $expiration = $this->expirationTime($options['ttl']); + if (!$this->memcached->setMulti($keyValuePairs, $expiration)) { + throw $this->getExceptionByResultCode($this->memcached->getResultCode()); } $result = true; @@ -390,10 +412,10 @@ public function setItems(array $keyValuePairs, array $options = array()) * Add an item. * * Options: + * - ttl optional + * - The time-to-live (Default: ttl of object) * - namespace optional * - The namespace to use (Default: namespace of object) - * - tags optional - * - An array of tags * * @param string $key * @param mixed $value @@ -426,16 +448,11 @@ public function addItem($key, $value, array $options = array()) return $eventRs->last(); } - $internalKey = $options['namespace'] . $baseOptions->getNamespaceSeparator() . $key; - if (!$this->memcached->add($internalKey, $value, $options['ttl'])) { - if ($this->memcached->get($internalKey)!==false) { - throw new Exception\RuntimeException("Key '{$internalKey}' already exists"); - } + $this->memcached->setOption(MemcachedResource::OPT_PREFIX_KEY, $options['namespace']); - $type = is_object($value) ? get_class($value) : gettype($value); - throw new Exception\RuntimeException( - "Memcached::add('{$internalKey}', <{$type}>, {$options['ttl']}) failed" - ); + $expiration = $this->expirationTime($options['ttl']); + if (!$this->memcached->add($key, $value, $expiration)) { + throw $this->getExceptionByResultCode($this->memcached->getResultCode()); } $result = true; @@ -449,10 +466,10 @@ public function addItem($key, $value, array $options = array()) * Replace an item. * * Options: + * - ttl optional + * - The time-to-live (Default: ttl of object) * - namespace optional * - The namespace to use (Default: namespace of object) - * - tags optional - * - An array of tags * * @param string $key * @param mixed $value @@ -485,22 +502,64 @@ public function replaceItem($key, $value, array $options = array()) return $eventRs->last(); } - $internalKey = $options['namespace'] . $baseOptions->getNamespaceSeparator() . $key; - if (!$this->memcached->get($internalKey)) { - throw new Exception\ItemNotFoundException( - "Key '{$internalKey}' doesn't exist" - ); + $this->memcached->setOption(MemcachedResource::OPT_PREFIX_KEY, $options['namespace']); + + $expiration = $this->expirationTime($options['ttl']); + if (!$this->memcached->replace($key, $value, $expiration)) { + throw $this->getExceptionByResultCode($this->memcached->getResultCode()); + } + + $result = true; + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + return $this->triggerException(__FUNCTION__, $args, $e); + } + } + + /** + * Check and set item + * + * @param float $token + * @param string $key + * @param mixed $value + * @param array $options + * @return bool + */ + public function checkAndSetItem($token, $key, $value, array $options = array()) + { + $baseOptions = $this->getOptions(); + if (!$baseOptions->getWritable()) { + return false; + } + + $this->normalizeOptions($options); + $this->normalizeKey($key); + $args = new ArrayObject(array( + 'token' => & $token, + 'key' => & $key, + 'value' => & $value, + 'options' => & $options, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); } - - $result = $this->memcached->replace($internalKey, $value, $options['ttl']); - + + $this->memcached->setOption(MemcachedResource::OPT_PREFIX_KEY, $options['namespace']); + + $expiration = $this->expirationTime($options['ttl']); + $result = $this->memcached->cas($token, $key, $value, $expiration); + if ($result === false) { - $type = is_object($value) ? get_class($value) : gettype($value); - throw new Exception\RuntimeException( - "Memcached::replace('{$internalKey}', <{$type}>, {$options['ttl']}) failed" - ); + $rsCode = $this->memcached->getResultCode(); + if ($rsCode !== 0 && $rsCode != MemcachedResource::RES_DATA_EXISTS) { + throw $this->getExceptionByResultCode($rsCode); + } } + return $this->triggerPost(__FUNCTION__, $args, $result); } catch (\Exception $e) { return $this->triggerException(__FUNCTION__, $args, $e); @@ -536,7 +595,6 @@ public function removeItem($key, array $options = array()) $this->normalizeKey($key); $args = new ArrayObject(array( 'key' => & $key, - 'value' => & $value, 'options' => & $options, )); @@ -546,23 +604,90 @@ public function removeItem($key, array $options = array()) return $eventRs->last(); } - $internalKey = $options['namespace'] . $baseOptions->getNamespaceSeparator() . $key; - - $result = $this->memcached->delete($internalKey); - + $this->memcached->setOption(MemcachedResource::OPT_PREFIX_KEY, $options['namespace']); + $result = $this->memcached->delete($key); + if ($result === false) { - if (!$options['ignore_missing_items']) { - throw new Exception\ItemNotFoundException("Key '{$internalKey}' not found"); + if (($rsCode = $this->memcached->getResultCode()) != 0 + && ($rsCode != MemcachedResource::RES_NOTFOUND || !$options['ignore_missing_items']) + ) { + throw $this->getExceptionByResultCode($rsCode); } } + $result = true; - return $this->triggerPost(__FUNCTION__, $args, $result); } catch (\Exception $e) { return $this->triggerException(__FUNCTION__, $args, $e); } } + /** + * Remove items. + * + * Options: + * - namespace optional + * - The namespace to use (Default: namespace of object) + * + * @param array $keys + * @param array $options + * @return boolean + * @throws Exception + * + * @triggers removeItems.pre(PreEvent) + * @triggers removeItems.post(PostEvent) + * @triggers removeItems.exception(ExceptionEvent) + */ + public function removeItems(array $keys, array $options = array()) + { + $baseOptions = $this->getOptions(); + if (!$baseOptions->getWritable()) { + return false; + } + + $this->normalizeOptions($options); + $args = new ArrayObject(array( + 'keys' => & $keys, + 'options' => & $options, + )); + + try { + $this->memcached->setOption(MemcachedResource::OPT_PREFIX_KEY, $options['namespace']); + + $rsCodes = array(); + if (static::$extMemcachedMajorVersion >= 2) { + $rsCodes = $this->memcached->deleteMulti($keys); + } else { + foreach ($keys as $key) { + $rs = $this->memcached->delete($key); + if ($rs === false) { + $rsCodes[$key] = $this->memcached->getResultCode(); + } + } + } + + $missingKeys = null; + foreach ($rsCodes as $key => $rsCode) { + if ($rsCode !== true && $rsCode != 0) { + if ($rsCode != MemcachedResource::RES_NOTFOUND) { + throw $this->getExceptionByResultCode($rsCode); + } elseif (!$options['ignore_missing_items']) { + $missingKeys[] = $key; + } + } + } + if ($missingKeys) { + throw new Exception\ItemNotFoundException( + "Keys '" . implode("','", $missingKeys) . "' not found" + ); + } + + return true; + } catch (MemcachedException $e) { + throw new RuntimeException($e->getMessage(), 0, $e); + } + } + /** * Increment an item. * @@ -593,6 +718,7 @@ public function incrementItem($key, $value, array $options = array()) $this->normalizeKey($key); $args = new ArrayObject(array( 'key' => & $key, + 'value' => & $value, 'options' => & $options, )); @@ -602,19 +728,23 @@ public function incrementItem($key, $value, array $options = array()) return $eventRs->last(); } - $internalKey = $options['namespace'] . $baseOptions->getNamespaceSeparator() . $key; - $value = (int)$value; - $newValue = $this->memcached->increment($internalKey, $value); + $this->memcached->setOption(MemcachedResource::OPT_PREFIX_KEY, $options['namespace']); + + $value = (int)$value; + $newValue = $this->memcached->increment($key, $value); + if ($newValue === false) { - if ($this->memcached->get($internalKey)!==false) { - throw new Exception\RuntimeException("Memcached::increment('{$internalKey}', {$value}) failed"); - } elseif (!$options['ignore_missing_items']) { - throw new Exception\ItemNotFoundException( - "Key '{$internalKey}' not found" - ); + if (($rsCode = $this->memcached->getResultCode()) != 0 + && ($rsCode != MemcachedResource::RES_NOTFOUND || !$options['ignore_missing_items']) + ) { + throw $this->getExceptionByResultCode($rsCode); + } + + $expiration = $this->expirationTime($options['ttl']); + if (!$this->memcached->add($key, $value, $expiration)) { + throw $this->getExceptionByResultCode($this->memcached->getResultCode()); } - - $this->addItem($key, $value, $options); + $newValue = $value; } @@ -654,6 +784,7 @@ public function decrementItem($key, $value, array $options = array()) $this->normalizeKey($key); $args = new ArrayObject(array( 'key' => & $key, + 'value' => & $value, 'options' => & $options, )); @@ -663,19 +794,23 @@ public function decrementItem($key, $value, array $options = array()) return $eventRs->last(); } - $internalKey = $options['namespace'] . $baseOptions->getNamespaceSeparator() . $key; - $value = (int)$value; - $newValue = $this->memcached->decrement($internalKey, $value); + $this->memcached->setOption(MemcachedResource::OPT_PREFIX_KEY, $options['namespace']); + + $value = (int)$value; + $newValue = $this->memcached->decrement($key, $value); + if ($newValue === false) { - if ($this->memcached->get($internalKey)!==false) { - throw new Exception\RuntimeException("Memcached::decrement('{$internalKey}', {$value}) failed"); - } elseif (!$options['ignore_missing_items']) { - throw new Exception\ItemNotFoundException( - "Key '{$internalKey}' not found" - ); + if (($rsCode = $this->memcached->getResultCode()) != 0 + && ($rsCode != MemcachedResource::RES_NOTFOUND || !$options['ignore_missing_items']) + ) { + throw $this->getExceptionByResultCode($rsCode); + } + + $expiration = $this->expirationTime($options['ttl']); + if (!$this->memcached->add($key, -$value, $expiration)) { + throw $this->getExceptionByResultCode($this->memcached->getResultCode()); } - $this->addItem($key, -$value, $options); $newValue = -$value; } @@ -687,6 +822,98 @@ public function decrementItem($key, $value, array $options = array()) /* non-blocking */ + /** + * Get items that were marked to delay storage for purposes of removing blocking + * + * Options: + * - namespace optional + * - The namespace to use (Default: namespace of object) + * - select optional + * - An array of the information the returned item contains + * (Default: array('key', 'value')) + * - callback optional + * - An result callback will be invoked for each item in the result set. + * - The first argument will be the item array. + * - The callback does not have to return anything. + * + * @param array $keys + * @param array $options + * @return bool + * @throws Exception + * + * @triggers getDelayed.pre(PreEvent) + * @triggers getDelayed.post(PostEvent) + * @triggers getDelayed.exception(ExceptionEvent) + */ + public function getDelayed(array $keys, array $options = array()) + { + $baseOptions = $this->getOptions(); + if ($this->stmtActive) { + throw new Exception\RuntimeException('Statement already in use'); + } elseif (!$baseOptions->getReadable()) { + return false; + } elseif (!$keys) { + return true; + } + + $this->normalizeOptions($options); + if (isset($options['callback']) && !is_callable($options['callback'], false)) { + throw new Exception\InvalidArgumentException('Invalid callback'); + } + if (!isset($options['select'])) { + $options['select'] = array('key', 'value'); + } + + $args = new ArrayObject(array( + 'keys' => & $keys, + 'options' => & $options, + )); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $this->memcached->setOption(MemcachedResource::OPT_PREFIX_KEY, $options['namespace']); + + // redirect callback + if (isset($options['callback'])) { + $cb = function (MemcachedResource $memc, array &$item) use (&$options, $baseOptions) { + $select = & $options['select']; + + // handle selected key + if (!in_array('key', $select)) { + unset($item['key']); + } + + // handle selected value + if (!in_array('value', $select)) { + unset($item['value']); + } + + call_user_func($options['callback'], $item); + }; + + if (!$this->memcached->getDelayed($keys, false, $cb)) { + throw $this->getExceptionByResultCode($this->memcached->getResultCode()); + } + } else { + if (!$this->memcached->getDelayed($keys)) { + throw $this->getExceptionByResultCode($this->memcached->getResultCode()); + } + + $this->stmtActive = true; + $this->stmtOptions = &$options; + } + + $result = true; + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + return $this->triggerException(__FUNCTION__, $args, $e); + } + } + /** * Fetches the next item from result set * @@ -711,24 +938,25 @@ public function fetch() return $eventRs->last(); } - $prefixL = strlen($this->stmtOptions['namespace'] . $this->getOptions()->getNamespaceSeparator()); + $result = $this->memcached->fetch(); + if (!empty($result)) { + $select = & $this->stmtOptions['select']; - if (!$this->stmtIterator) { - // clear stmt - $this->stmtActive = false; - $this->stmtIterator = null; - $this->stmtOptions = null; + // handle selected key + if (!in_array('key', $select)) { + unset($result['key']); + } - $result = false; - } else { - $result = $this->memcached->fetch(); - if (!empty($result)) { - $select = $this->stmtOptions['select']; - if (in_array('key', $select)) { - $result['key'] = substr($result['key'], $prefixL); - } + // handle selected value + if (!in_array('value', $select)) { + unset($result['value']); } - } + + } else { + // clear stmt + $this->stmtActive = false; + $this->stmtOptions = null; + } return $this->triggerPost(__FUNCTION__, $args, $result); } catch (\Exception $e) { @@ -738,44 +966,35 @@ public function fetch() /** * FetchAll - * + * * @throws Exception - * @return array + * @return array */ public function fetchAll() { - $prefixL = strlen($this->stmtOptions['namespace'] . $this->getOptions()->getNamespaceSeparator()); - $result = $this->memcached->fetchAll(); - if ($result === false) { throw new Exception\RuntimeException("Memcached::fetchAll() failed"); } - + $select = $this->stmtOptions['select']; - + foreach ($result as &$elem) { - if (in_array('key', $select)) { - $elem['key'] = substr($elem['key'], $prefixL); - } else { + if (!in_array('key', $select)) { unset($elem['key']); } } - + return $result; } - + /* cleaning */ /** * Clear items off all namespaces. * * Options: - * - ttl optional - * - The time-to-life (Default: ttl of object) - * - tags optional - * - Tags to search for used with matching modes of - * Zend\Cache\Storage\Adapter::MATCH_TAGS_* + * - No options available for this adapter * * @param int $mode Matching mode (Value of Zend\Cache\Storage\Adapter::MATCH_*) * @param array $options @@ -806,8 +1025,11 @@ public function clear($mode = self::MATCH_EXPIRED, array $options = array()) return $eventRs->last(); } - $result = $this->memcached->flush(); - + if (!$this->memcached->flush()) { + throw $this->getExceptionByResultCode($this->memcached->getResultCode()); + } + + $result = true; return $this->triggerPost(__FUNCTION__, $args, $result); } catch (\Exception $e) { return $this->triggerException(__FUNCTION__, $args, $e); @@ -857,8 +1079,8 @@ public function getCapabilities() 'ttlPrecision' => 1, 'useRequestTime' => false, 'expiredRead' => false, + 'maxKeyLength' => 255, 'namespaceIsPrefix' => true, - 'namespaceSeparator' => $this->getOptions()->getNamespaceSeparator(), 'iterable' => false, 'clearAllNamespaces' => true, 'clearByNamespace' => false, @@ -872,71 +1094,6 @@ public function getCapabilities() } } - /** - * Get items that were marked to delay storage for purposes of removing blocking - * - * @param array $keys - * @param array $options - * @return bool - * @throws Exception - * - * @triggers getDelayed.pre(PreEvent) - * @triggers getDelayed.post(PostEvent) - * @triggers getDelayed.exception(ExceptionEvent) - */ - public function getDelayed(array $keys, array $options = array()) - { - $baseOptions = $this->getOptions(); - if ($this->stmtActive) { - throw new Exception\RuntimeException('Statement already in use'); - } elseif (!$baseOptions->getReadable()) { - return false; - } elseif (!$keys) { - return true; - } - - $this->normalizeOptions($options); - if (isset($options['callback']) && !is_callable($options['callback'], false)) { - throw new Exception\InvalidArgumentException('Invalid callback'); - } - - $args = new ArrayObject(array( - 'key' => & $key, - 'options' => & $options, - )); - - try { - $eventRs = $this->triggerPre(__FUNCTION__, $args); - if ($eventRs->stopped()) { - return $eventRs->last(); - } - - $prefix = $options['namespace'] . $baseOptions->getNamespaceSeparator(); - - $search = array(); - foreach ($keys as $key) { - $search[] = $prefix.$key; - } - - $this->stmtIterator = $this->memcached->getDelayed($search); - - $this->stmtActive = true; - $this->stmtOptions = &$options; - - if (isset($options['callback'])) { - $callback = $options['callback']; - while (($item = $this->fetch()) !== false) { - call_user_func($callback, $item); - } - } - - $result = true; - return $this->triggerPost(__FUNCTION__, $args, $result); - } catch (\Exception $e) { - return $this->triggerException(__FUNCTION__, $args, $e); - } - } - /** * Get storage capacity. * @@ -959,7 +1116,12 @@ public function getCapacity(array $options = array()) return $eventRs->last(); } - $mem = array_pop($this->memcached->getStats()); + $stats = $this->memcached->getStats(); + if ($stats === false) { + throw $this->getExceptionByResultCode($this->memcached->getResultCode()); + } + + $mem = array_pop($stats); $result = array( 'free' => $mem['limit_maxbytes'] - $mem['bytes'], 'total' => $mem['limit_maxbytes'], @@ -971,5 +1133,51 @@ public function getCapacity(array $options = array()) } /* 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. + * + * @param int $ttl + * @return int + */ + protected function expirationTime($ttl) + { + if ($ttl > 2592000) { + return time() + $ttl; + } + return $ttl; + } + + /** + * Generate exception based of memcached result code + * + * @param int $code + * @return Exception\RuntimeException|Exception\ItemNotFoundException + * @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" + ); + + case MemcachedResource::RES_NOTFOUND: + case MemcachedResource::RES_NOTSTORED: + return new Exception\ItemNotFoundException($this->memcached->getResultMessage()); + + default: + return new Exception\RuntimeException($this->memcached->getResultMessage()); + } + } } diff --git a/src/Storage/Adapter/MemcachedOptions.php b/src/Storage/Adapter/MemcachedOptions.php index 6dbebb322..0db7a4d6b 100644 --- a/src/Storage/Adapter/MemcachedOptions.php +++ b/src/Storage/Adapter/MemcachedOptions.php @@ -37,7 +37,7 @@ class MemcachedOptions extends AdapterOptions { /** * Map of option keys to \Memcached options - * + * * @var array */ private $optionsMap = array( @@ -51,7 +51,6 @@ class MemcachedOptions extends AdapterOptions 'libketama_compatible' => MemcachedResource::OPT_LIBKETAMA_COMPATIBLE, 'no_block' => MemcachedResource::OPT_NO_BLOCK, 'poll_timeout' => MemcachedResource::OPT_POLL_TIMEOUT, - 'prefix_key' => MemcachedResource::OPT_PREFIX_KEY, 'recv_timeout' => MemcachedResource::OPT_RECV_TIMEOUT, 'retry_timeout' => MemcachedResource::OPT_RETRY_TIMEOUT, 'send_timeout' => MemcachedResource::OPT_SEND_TIMEOUT, @@ -60,176 +59,188 @@ class MemcachedOptions extends AdapterOptions 'socket_recv_size' => MemcachedResource::OPT_SOCKET_RECV_SIZE, 'socket_send_size' => MemcachedResource::OPT_SOCKET_SEND_SIZE, 'tcp_nodelay' => MemcachedResource::OPT_TCP_NODELAY, + + // The prefix_key act as namespace an will be set directly + // 'prefix_key' => MemcachedResource::OPT_PREFIX_KEY, ); /** * Memcached server address - * - * @var string + * + * @var string */ protected $server = 'localhost'; - + /** * Memcached port - * + * * @var integer */ protected $port = 11211; - + /** * Whether or not to enable binary protocol for communication with server - * + * * @var bool */ protected $binaryProtocol = false; /** * Enable or disable buffered I/O - * + * * @var bool */ protected $bufferWrites = false; /** * Whether or not to cache DNS lookups - * + * * @var bool */ protected $cacheLookups = false; /** * Whether or not to use compression - * + * * @var bool */ protected $compression = true; /** * Time at which to issue connection timeout, in ms - * + * * @var int */ protected $connectTimeout = 1000; /** * Server distribution algorithm - * + * * @var int */ protected $distribution = MemcachedResource::DISTRIBUTION_MODULA; /** * Hashing algorithm to use - * + * * @var int */ protected $hash = MemcachedResource::HASH_DEFAULT; /** * Whether or not to enable compatibility with libketama-like behavior. - * + * * @var bool */ protected $libketamaCompatible = false; - /** - * Namespace separator - * - * @var string - */ - protected $namespaceSeparator = ':'; - /** * Whether or not to enable asynchronous I/O - * + * * @var bool */ protected $noBlock = false; /** * Timeout for connection polling, in ms - * + * * @var int */ protected $pollTimeout = 0; - /** - * Prefix to use with keys - * - * @var string - */ - protected $prefixKey = ''; - /** * Maximum allowed time for a recv operation, in ms - * + * * @var int */ protected $recvTimeout = 0; /** * Time to wait before retrying a connection, in seconds - * + * * @var int */ protected $retryTimeout = 0; /** * Maximum allowed time for a send operation, in ms - * + * * @var int */ protected $sendTimeout = 0; /** * Serializer to use - * + * * @var int */ protected $serializer = MemcachedResource::SERIALIZER_PHP; /** * Maximum number of server connection errors - * + * * @var int */ protected $serverFailureLimit = 0; /** * Maximum socket send buffer in bytes - * + * * @var int */ protected $socketSendSize; /** * Maximum socket recv buffer in bytes - * + * * @var int */ protected $socketRecvSize; /** * Whether or not to enable no-delay feature for connecting sockets - * + * * @var bool */ protected $tcpNodelay = false; + /** + * Set namespace. + * + * The option Memcached::OPT_PREFIX_KEY will be used as the namespace. + * It can't be longer than 128 characters. + * + * @see AdapterOptions::setNamespace() + * @see MemcachedOptions::setPrefixKey() + */ + public function setNamespace($namespace) + { + $namespace = (string)$namespace; + + if (128 < strlen($namespace)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects a prefix key of no longer than 128 characters', + __METHOD__ + )); + } + + return parent::setNamespace($namespace); + } + public function setServer($server) { $this->server= $server; return $this; } - + public function getServer() { return $this->server; } - + public function setPort($port) { - if ((!is_int($port) && !is_numeric($port)) + if ((!is_int($port) && !is_numeric($port)) || 0 > $port ) { throw new Exception\InvalidArgumentException(sprintf( @@ -237,18 +248,18 @@ public function setPort($port) __METHOD__ )); } - + $this->port= $port; return $this; } - + public function getPort() { return $this->port; } - + /** - * Set flag indicating whether or not to enable binary protocol for + * Set flag indicating whether or not to enable binary protocol for * communication with server * * @param bool $binaryProtocol @@ -259,7 +270,7 @@ public function setBinaryProtocol($binaryProtocol) $this->binaryProtocol = (bool) $binaryProtocol; return $this; } - + /** * Whether or not to enable binary protocol for communication with server * @@ -281,7 +292,7 @@ public function setBufferWrites($bufferWrites) $this->bufferWrites = (bool) $bufferWrites; return $this; } - + /** * Whether or not buffered I/O is enabled * @@ -303,7 +314,7 @@ public function setCacheLookups($cacheLookups) $this->cacheLookups = (bool) $cacheLookups; return $this; } - + /** * Whether or not to cache DNS lookups * @@ -325,7 +336,7 @@ public function setCompression($compression) $this->compression = (bool) $compression; return $this; } - + /** * Whether or not compression is enabled * @@ -344,7 +355,7 @@ public function getCompression() */ public function setConnectTimeout($connectTimeout) { - if ((!is_int($connectTimeout) && !is_numeric($connectTimeout)) + if ((!is_int($connectTimeout) && !is_numeric($connectTimeout)) || 0 > $connectTimeout ) { throw new Exception\InvalidArgumentException(sprintf( @@ -356,7 +367,7 @@ public function setConnectTimeout($connectTimeout) $this->connectTimeout = (int) $connectTimeout; return $this; } - + /** * Get connection timeout value * @@ -388,7 +399,7 @@ public function setDistribution($distribution) $this->distribution = $distribution; return $this; } - + /** * Get server distribution algorithm * @@ -427,7 +438,7 @@ public function setHash($hash) $this->hash = $hash; return $this; } - + /** * Get hash algorithm * @@ -449,7 +460,7 @@ public function setLibketamaCompatible($libketamaCompatible) $this->libketamaCompatible = (bool) $libketamaCompatible; return $this; } - + /** * Whether or not to enable libketama compatibility * @@ -460,28 +471,6 @@ public function getLibketamaCompatible() return $this->libketamaCompatible; } - /** - * Set namespace separator - * - * @param string $separator - * @return MemcachedOptions - */ - public function setNamespaceSeparator($separator) - { - $this->namespaceSeparator = (string) $separator; - return $this; - } - - /** - * Get namespace separator - * - * @return string - */ - public function getNamespaceSeparator() - { - return $this->namespaceSeparator; - } - /** * Set flag indicating whether or not to enable asynchronous I/O * @@ -493,7 +482,7 @@ public function setNoBlock($noBlock) $this->noBlock = (bool) $noBlock; return $this; } - + /** * Whether or not to enable asynchronous I/O * @@ -512,7 +501,7 @@ public function getNoBlock() */ public function setPollTimeout($pollTimeout) { - if ((!is_int($pollTimeout) && !is_numeric($pollTimeout)) + if ((!is_int($pollTimeout) && !is_numeric($pollTimeout)) || 0 > $pollTimeout ) { throw new Exception\InvalidArgumentException(sprintf( @@ -524,7 +513,7 @@ public function setPollTimeout($pollTimeout) $this->pollTimeout = (int) $pollTimeout; return $this; } - + /** * Get connection polling timeout value * @@ -538,36 +527,26 @@ public function getPollTimeout() /** * Set prefix for keys * + * The prefix key act as namespace. + * * @param string $prefixKey * @return MemcachedOptions */ public function setPrefixKey($prefixKey) { - if (!is_string($prefixKey)) { - throw new Exception\InvalidArgumentException(sprintf( - '%s expects a string', - __METHOD__ - )); - } - if (128 < strlen($prefixKey)) { - throw new Exception\InvalidArgumentException(sprintf( - '%s expects a prefix key of no longer than 128 characters', - __METHOD__ - )); - } - - $this->prefixKey = $prefixKey; - return $this; + return $this->setNamespace($prefixKey); } - + /** * Get prefix key * + * The prefix key act as namespace. + * * @return string */ public function getPrefixKey() { - return $this->prefixKey; + return $this->getNamespace(); } /** @@ -578,7 +557,7 @@ public function getPrefixKey() */ public function setRecvTimeout($recvTimeout) { - if ((!is_int($recvTimeout) && !is_numeric($recvTimeout)) + if ((!is_int($recvTimeout) && !is_numeric($recvTimeout)) || 0 > $recvTimeout ) { throw new Exception\InvalidArgumentException(sprintf( @@ -590,7 +569,7 @@ public function setRecvTimeout($recvTimeout) $this->recvTimeout = (int) $recvTimeout; return $this; } - + /** * Get recv timeout value * @@ -609,7 +588,7 @@ public function getRecvTimeout() */ public function setRetryTimeout($retryTimeout) { - if ((!is_int($retryTimeout) && !is_numeric($retryTimeout)) + if ((!is_int($retryTimeout) && !is_numeric($retryTimeout)) || 0 > $retryTimeout ) { throw new Exception\InvalidArgumentException(sprintf( @@ -621,7 +600,7 @@ public function setRetryTimeout($retryTimeout) $this->retryTimeout = (int) $retryTimeout; return $this; } - + /** * Get retry timeout value, in seconds * @@ -640,7 +619,7 @@ public function getRetryTimeout() */ public function setSendTimeout($sendTimeout) { - if ((!is_int($sendTimeout) && !is_numeric($sendTimeout)) + if ((!is_int($sendTimeout) && !is_numeric($sendTimeout)) || 0 > $sendTimeout ) { throw new Exception\InvalidArgumentException(sprintf( @@ -652,7 +631,7 @@ public function setSendTimeout($sendTimeout) $this->sendTimeout = (int) $sendTimeout; return $this; } - + /** * Get send timeout value * @@ -703,7 +682,7 @@ public function setSerializer($serializer) $this->serializer = $serializer; return $this; } - + /** * Get serializer * @@ -722,7 +701,7 @@ public function getSerializer() */ public function setServerFailureLimit($serverFailureLimit) { - if ((!is_int($serverFailureLimit) && !is_numeric($serverFailureLimit)) + if ((!is_int($serverFailureLimit) && !is_numeric($serverFailureLimit)) || 0 > $serverFailureLimit ) { throw new Exception\InvalidArgumentException(sprintf( @@ -734,7 +713,7 @@ public function setServerFailureLimit($serverFailureLimit) $this->serverFailureLimit = (int) $serverFailureLimit; return $this; } - + /** * Get maximum server failures allowed * @@ -756,8 +735,8 @@ public function setSocketSendSize($socketSendSize) if ($socketSendSize === null) { return $this; } - - if ((!is_int($socketSendSize) && !is_numeric($socketSendSize)) + + if ((!is_int($socketSendSize) && !is_numeric($socketSendSize)) || 0 > $socketSendSize ) { throw new Exception\InvalidArgumentException(sprintf( @@ -769,7 +748,7 @@ public function setSocketSendSize($socketSendSize) $this->socketSendSize = (int) $socketSendSize; return $this; } - + /** * Get maximum socket send buffer in bytes * @@ -791,8 +770,8 @@ public function setSocketRecvSize($socketRecvSize) if ($socketRecvSize === null) { return $this; } - - if ((!is_int($socketRecvSize) && !is_numeric($socketRecvSize)) + + if ((!is_int($socketRecvSize) && !is_numeric($socketRecvSize)) || 0 > $socketRecvSize ) { throw new Exception\InvalidArgumentException(sprintf( @@ -804,7 +783,7 @@ public function setSocketRecvSize($socketRecvSize) $this->socketRecvSize = (int) $socketRecvSize; return $this; } - + /** * Get maximum socket recv buffer in bytes * @@ -826,7 +805,7 @@ public function setTcpNodelay($tcpNodelay) $this->tcpNodelay = (bool) $tcpNodelay; return $this; } - + /** * Whether or not to enable no-delay feature when connecting sockets * @@ -839,7 +818,7 @@ public function getTcpNodelay() /** * Get map of option keys to \Memcached constants - * + * * @return array */ public function getOptionsMap() diff --git a/src/Storage/Adapter/Memory.php b/src/Storage/Adapter/Memory.php index 362cf89b8..8d470e0df 100644 --- a/src/Storage/Adapter/Memory.php +++ b/src/Storage/Adapter/Memory.php @@ -25,6 +25,7 @@ stdClass, Zend\Cache\Exception, Zend\Cache\Storage\Capabilities, + Zend\Cache\Storage\Adapter\MemoryOptions, Zend\Cache\Utils; /** @@ -54,6 +55,49 @@ class Memory extends AbstractAdapter */ protected $data = array(); + /** + * Set options. + * + * @param array|Traversable|MemoryOptions $options + * @return Memory + * @see getOptions() + */ + public function setOptions($options) + { + if (!is_array($options) + && !$options instanceof Traversable + && !$options instanceof MemoryOptions + ) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects an array, a Traversable object, or an MemoryOptions instance; ' + . 'received "%s"', + __METHOD__, + (is_object($options) ? get_class($options) : gettype($options)) + )); + } + + if (!$options instanceof MemoryOptions) { + $options = new MemoryOptions($options); + } + + $this->options = $options; + return $this; + } + + /** + * Get options. + * + * @return MemoryOptions + * @see setOptions() + */ + public function getOptions() + { + if (!$this->options) { + $this->setOptions(new MemoryOptions()); + } + return $this->options; + } + /* reading */ /** @@ -332,6 +376,13 @@ public function setItem($key, $value, array $options = array()) return $eventRs->last(); } + if (!$this->hasFreeCapacity()) { + $memoryLimit = $baseOptions->getMemoryLimit(); + throw new Exception\OutOfCapacityException( + 'Memory usage exceeds limit ({$memoryLimit}).' + ); + } + $ns = $options['namespace']; $this->data[$ns][$key] = array($value, microtime(true), $options['tags']); @@ -379,6 +430,13 @@ public function setItems(array $keyValuePairs, array $options = array()) return $eventRs->last(); } + if (!$this->hasFreeCapacity()) { + $memoryLimit = $baseOptions->getMemoryLimit(); + throw new Exception\OutOfCapacityException( + 'Memory usage exceeds limit ({$memoryLimit}).' + ); + } + $ns = $options['namespace']; if (!isset($this->data[$ns])) { $this->data[$ns] = array(); @@ -436,6 +494,13 @@ public function addItem($key, $value, array $options = array()) return $eventRs->last(); } + if (!$this->hasFreeCapacity()) { + $memoryLimit = $baseOptions->getMemoryLimit(); + throw new Exception\OutOfCapacityException( + 'Memory usage exceeds limit ({$memoryLimit}).' + ); + } + $ns = $options['namespace']; if (isset($this->data[$ns][$key])) { throw new Exception\RuntimeException("Key '{$key}' already exists within namespace '$ns'"); @@ -486,6 +551,13 @@ public function addItems(array $keyValuePairs, array $options = array()) return $eventRs->last(); } + if (!$this->hasFreeCapacity()) { + $memoryLimit = $baseOptions->getMemoryLimit(); + throw new Exception\OutOfCapacityException( + 'Memory usage exceeds limit ({$memoryLimit}).' + ); + } + $ns = $options['namespace']; if (!isset($this->data[$ns])) { $this->data[$ns] = array(); @@ -1276,7 +1348,7 @@ public function getCapabilities() 'resource' => true, ), 'supportedMetadata' => array( - 'mtime', + 'mtime', 'tags', ), 'maxTtl' => PHP_INT_MAX, @@ -1319,13 +1391,34 @@ public function getCapacity(array $options = array()) return $eventRs->last(); } - $result = Utils::getPhpMemoryCapacity(); + $total = $this->getOptions()->getMemoryLimit(); + $free = $total - (float) memory_get_usage(true); + $result = array( + 'total' => $total, + 'free' => ($free >= 0) ? $free : 0, + ); return $this->triggerPost(__FUNCTION__, $args, $result); } /* internal */ + /** + * Has the memory adapter storage free capacity + * to store items + * + * Similar logic as getCapacity() but without triggering + * events and returns boolean. + * + * @return boolean + */ + protected function hasFreeCapacity() + { + $total = $this->getOptions()->getMemoryLimit(); + $free = $total - (float) memory_get_usage(true); + return ($free > 0); + } + /** * Internal method to check if an key exists * and if it isn't expired. diff --git a/src/Storage/Adapter/MemoryOptions.php b/src/Storage/Adapter/MemoryOptions.php new file mode 100644 index 000000000..ed78409eb --- /dev/null +++ b/src/Storage/Adapter/MemoryOptions.php @@ -0,0 +1,82 @@ +memoryLimit = (int) $bytes; + return $this; + } + + /** + * Get memory limit + * + * If the used memory of PHP exceeds this limit an OutOfCapacityException + * will be thrown. + * + * @return int + */ + public function getMemoryLimit() + { + if ($this->memoryLimit === null) { + $memoryLimit = Utils::bytesFromString(ini_get('memory_limit')); + if ($memoryLimit >= 0) { + $this->memoryLimit = floor($memoryLimit / 2); + } else { + // use a hard memory limit of 32M if php memory limit is disabled + $this->memoryLimit = 33554432; + } + } + + return $this->memoryLimit; + } +} diff --git a/src/Storage/Adapter/WinCache.php b/src/Storage/Adapter/WinCache.php index 8bc97f04e..a4c0086fc 100644 --- a/src/Storage/Adapter/WinCache.php +++ b/src/Storage/Adapter/WinCache.php @@ -46,16 +46,16 @@ class WinCache extends AbstractAdapter /** * Constructor * - * @param array $options Option + * @param array|Traversable|WinCacheOptions $options * @throws Exception * @return void */ - public function __construct() + public function __construct($options = null) { if (!extension_loaded('wincache')) { throw new Exception\ExtensionNotLoadedException("WinCache extension is not loaded"); } - + $enabled = ini_get('wincache.ucenabled'); if (PHP_SAPI == 'cli') { $enabled = $enabled && (bool) ini_get('wincache.enablecli'); @@ -66,6 +66,8 @@ public function __construct() "WinCache is disabled - see 'wincache.ucenabled' and 'wincache.enablecli'" ); } + + parent::__construct($options); } /* options */ @@ -73,15 +75,15 @@ public function __construct() /** * Set options. * - * @param stringTraversable|WinCacheOptions $options + * @param array|Traversable|WinCacheOptions $options * @return WinCache * @see getOptions() */ public function setOptions($options) { - if (!is_array($options) - && !$options instanceof Traversable - && !$options instanceof WinCacheOptions + if (!is_array($options) + && !$options instanceof Traversable + && !$options instanceof WinCacheOptions ) { throw new Exception\InvalidArgumentException(sprintf( '%s expects an array, a Traversable object; ' @@ -108,7 +110,6 @@ public function getOptions() return $this->options; } - /* reading */ /** @@ -322,15 +323,15 @@ public function hasItems(array $keys, array $options = array()) foreach ($keys as $key) { $internalKeys[] = $options['namespace'] . $namespaceSep . $key; } - + $prefixL = strlen($options['namespace'] . $namespaceSep); $result = array(); foreach ($internalKeys as $key) { if (wincache_ucache_exists($key)) { $result[] = substr($key, $prefixL); - } + } } - + return $this->triggerPost(__FUNCTION__, $args, $result); } catch (\Exception $e) { return $this->triggerException(__FUNCTION__, $args, $e); @@ -382,7 +383,7 @@ public function getMetadata($key, array $options = array()) $info = wincache_ucache_info(true, $internalKey); if (isset($info['ucache_entries'][1])) { $metadata = $info['ucache_entries'][1]; - } + } if (empty($metadata)) { if (!$options['ignore_missing_items']) { @@ -429,9 +430,9 @@ public function getMetadatas(array $keys, array $options = array()) if ($eventRs->stopped()) { return $eventRs->last(); } - + $result= array(); - + foreach ($keys as $key) { $internalKey = $options['namespace'] . $baseOptions->getNamespaceSeparator() . $key; @@ -448,7 +449,7 @@ public function getMetadatas(array $keys, array $options = array()) $result[ substr($internalKey, $prefixL) ] = & $metadata; } } - + if (!$options['ignore_missing_items']) { if (count($keys) != count($result)) { $missing = implode("', '", array_diff($keys, array_keys($result))); @@ -860,7 +861,7 @@ public function incrementItem($key, $value, array $options = array()) "Key '{$internalKey}' not found" ); } - + $this->addItem($key, $value, $options); $newValue = $value; } @@ -977,7 +978,7 @@ public function clear($mode = self::MATCH_EXPIRED, array $options = array()) } $result= wincache_ucache_clear(); - + return $this->triggerPost(__FUNCTION__, $args, $result); } catch (\Exception $e) { return $this->triggerException(__FUNCTION__, $args, $e); @@ -1093,17 +1094,17 @@ protected function normalizeMetadata(array &$metadata) $metadata['num_hits'] = $metadata['hitcount']; unset($metadata['hitcount']); } - + if (isset($metadata['ttl_seconds'])) { $metadata['ttl'] = $metadata['ttl_seconds']; unset($metadata['ttl_seconds']); } - + if (isset($metadata['value_size'])) { $metadata['mem_size'] = $metadata['value_size']; unset($metadata['value_size']); } - + // remove namespace prefix if (isset($metadata['key_name'])) { $pos = strpos($metadata['key_name'], $this->getOptions()->getNamespaceSeparator()); diff --git a/src/StorageFactory.php b/src/StorageFactory.php index 05a86ce9b..f558e9651 100644 --- a/src/StorageFactory.php +++ b/src/StorageFactory.php @@ -128,7 +128,7 @@ public static function factory($cfg) // set adapter or plugin options if (isset($cfg['options'])) { - if (!is_array($cfg['options']) + if (!is_array($cfg['options']) && !$cfg['options'] instanceof Traversable ) { throw new Exception\InvalidArgumentException( @@ -150,20 +150,23 @@ public static function factory($cfg) * Instantiate a storage adapter * * @param string|Storage\Adapter $adapterName - * @param array|Traversable|Storage\Adapter\AdapterOptions $options + * @param null|array|Traversable|Storage\Adapter\AdapterOptions $options * @return Storage\Adapter * @throws Exception\RuntimeException */ - public static function adapterFactory($adapterName, $options = array()) + public static function adapterFactory($adapterName, $options = null) { if ($adapterName instanceof Storage\Adapter) { // $adapterName is already an adapter object - $adapterName->setOptions($options); - return $adapterName; + $adapter = $adapterName; + } else { + $adapter = static::getAdapterBroker()->load($adapterName); + } + + if ($options !== null) { + $adapter->setOptions($options); } - $adapter = static::getAdapterBroker()->load($adapterName); - $adapter->setOptions($options); return $adapter; } diff --git a/src/Utils.php b/src/Utils.php index 632b66e01..696740aa7 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -26,7 +26,7 @@ * @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ -class Utils +abstract class Utils { /** * Get disk capacity @@ -224,7 +224,7 @@ static public function getHashAlgos() * @return float * @throws Exception\RuntimeException */ - static protected function bytesFromString($memStr) + static public function bytesFromString($memStr) { if (!preg_match('/\s*([\-\+]?\d+)\s*(\w*)\s*/', $memStr, $matches)) { throw new Exception\RuntimeException("Can't detect bytes of string '{$memStr}'"); diff --git a/test/Storage/Adapter/CommonAdapterTest.php b/test/Storage/Adapter/CommonAdapterTest.php index 88875528a..551cd1bd3 100644 --- a/test/Storage/Adapter/CommonAdapterTest.php +++ b/test/Storage/Adapter/CommonAdapterTest.php @@ -245,35 +245,36 @@ public function testGetItemReturnsFalseIfNonReadable() $this->assertFalse($this->_storage->getItem('key')); } + 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(); - if (!$capabilities->getSupportedMetadata()) { - $this->markTestSkipped("Metadata are not supported by the adapter"); - } - + $supportedMetadatas = $capabilities->getSupportedMetadata(); + $this->assertTrue($this->_storage->setItem('key', 'value')); - $this->assertInternalType('array', $this->_storage->getMetadata('key')); + $metadata = $this->_storage->getMetadata('key'); + + $this->assertInternalType('array', $metadata); + foreach ($supportedMetadatas as $supportedMetadata) { + $this->assertArrayHasKey($supportedMetadata, $metadata); + } } public function testGetMetadataReturnsFalseIfIgnoreMissingItemsEnabled() { - $capabilities = $this->_storage->getCapabilities(); - if (!$capabilities->getSupportedMetadata()) { - $this->markTestSkipped("Metadata are not supported by the adapter"); - } - $this->_options->setIgnoreMissingItems(true); $this->assertFalse($this->_storage->getMetadata('unknown')); } public function testGetMetadataThrowsItemNotFoundExceptionIfIgnoreMissingItemsDisabled() { - $capabilities = $this->_storage->getCapabilities(); - if (!$capabilities->getSupportedMetadata()) { - $this->markTestSkipped("Metadata are not supported by the adapter"); - } - $this->_options->setIgnoreMissingItems(false); $this->setExpectedException('Zend\Cache\Exception\ItemNotFoundException'); @@ -282,11 +283,6 @@ public function testGetMetadataThrowsItemNotFoundExceptionIfIgnoreMissingItemsDi public function testGetMetadataReturnsFalseIfNonReadable() { - $capabilities = $this->_storage->getCapabilities(); - if (!$capabilities->getSupportedMetadata()) { - $this->markTestSkipped("Metadata are not supported by the adapter"); - } - $this->_options->setReadable(false); $this->assertTrue($this->_storage->setItem('key', 'value')); @@ -296,10 +292,8 @@ public function testGetMetadataReturnsFalseIfNonReadable() public function testGetMetadatas() { $capabilities = $this->_storage->getCapabilities(); - if (!$capabilities->getSupportedMetadata()) { - $this->markTestSkipped("Metadata are not supported by the adapter"); - } - + $supportedMetadatas = $capabilities->getSupportedMetadata(); + $items = array( 'key1' => 'value1', 'key2' => 'value2' @@ -309,42 +303,20 @@ public function testGetMetadatas() $metadatas = $this->_storage->getMetadatas(array_keys($items)); $this->assertInternalType('array', $metadatas); $this->assertSame(count($items), count($metadatas)); - foreach ($metadatas as $k => $info) { - $this->assertTrue(isset($items[$k])); - $this->assertInternalType('array', $info); + foreach ($metadatas as $k => $metadata) { + $this->assertInternalType('array', $metadata); + foreach ($supportedMetadatas as $supportedMetadata) { + $this->assertArrayHasKey($supportedMetadata, $metadata); + } } } public function testGetMetadatasReturnsEmptyArrayIfNonReadable() { - $capabilities = $this->_storage->getCapabilities(); - if (!$capabilities->getSupportedMetadata()) { - $this->markTestSkipped("Metadata are not supported by the adapter"); - } - $this->_options->setReadable(false); $this->assertTrue($this->_storage->setItem('key', 'value')); - $this->assertEquals(array(), $this->_storage->getItems(array('key'))); - } - - public function testGetMetadataAgainstMetadataCapabilities() - { - $capabilities = $this->_storage->getCapabilities(); - if (!$capabilities->getSupportedMetadata()) { - $this->markTestSkipped("Metadata are not supported by the adapter"); - } - - $capabilities = $this->_storage->getCapabilities(); - - $this->assertTrue($this->_storage->setItem('key', 'value')); - - $metadata = $this->_storage->getMetadata('key'); - $this->assertInternalType('array', $metadata); - - foreach ($capabilities->getSupportedMetadata() as $property) { - $this->assertArrayHasKey($property, $metadata); - } + $this->assertEquals(array(), $this->_storage->getMetadatas(array('key'))); } public function testSetGetHasAndRemoveItem() @@ -833,14 +805,14 @@ public function testGetDelayedAndFetchAllWithSelectInfo() ))); $fetchedItems = $this->_storage->fetchAll(); - + $this->assertEquals(count($items), count($fetchedItems)); foreach ($fetchedItems as $item) { if (is_array($capabilities->getSupportedMetadata())) { foreach ($capabilities->getSupportedMetadata() as $selectProperty) { $this->assertArrayHasKey($selectProperty, $item); } - } + } } } diff --git a/test/Storage/Adapter/MemoryTest.php b/test/Storage/Adapter/MemoryTest.php index d5b3f3a33..d999f40d6 100644 --- a/test/Storage/Adapter/MemoryTest.php +++ b/test/Storage/Adapter/MemoryTest.php @@ -37,10 +37,19 @@ class MemoryTest extends CommonAdapterTest public function setUp() { // instantiate memory adapter - $this->_options = new Cache\Storage\Adapter\AdapterOptions(); + $this->_options = new Cache\Storage\Adapter\MemoryOptions(); $this->_storage = new Cache\Storage\Adapter\Memory(); $this->_storage->setOptions($this->_options); parent::setUp(); } + + public function testThrowOutOfCapacityException() + { + $this->_options->setMemoryLimit(memory_get_usage(true) - 8); + + $this->setExpectedException('Zend\Cache\Exception\OutOfCapacityException'); + $this->_storage->addItem('test', 'test'); + } + }