From 20bd5f887d8bc7cef780a5c67abb3f42068653c4 Mon Sep 17 00:00:00 2001 From: Remi Collet Date: Fri, 27 Nov 2015 18:27:08 +0100 Subject: [PATCH] see #47 implements Zend\Cache\Storage\Adapter\Apcu (for PHP 7 and APCu 5.1) --- src/Storage/Adapter/Apcu.php | 750 +++++++++++++++++++++++++++ src/Storage/Adapter/ApcuIterator.php | 157 ++++++ src/Storage/Adapter/ApcuOptions.php | 47 ++ src/Storage/AdapterPluginManager.php | 1 + test/Storage/Adapter/ApcuTest.php | 60 +++ 5 files changed, 1015 insertions(+) create mode 100644 src/Storage/Adapter/Apcu.php create mode 100644 src/Storage/Adapter/ApcuIterator.php create mode 100644 src/Storage/Adapter/ApcuOptions.php create mode 100644 test/Storage/Adapter/ApcuTest.php diff --git a/src/Storage/Adapter/Apcu.php b/src/Storage/Adapter/Apcu.php new file mode 100644 index 000000000..2ffd7874d --- /dev/null +++ b/src/Storage/Adapter/Apcu.php @@ -0,0 +1,750 @@ +options) { + $this->setOptions(new ApcuOptions()); + } + return $this->options; + } + + /* TotalSpaceCapableInterface */ + + /** + * Get total space in bytes + * + * @return int|float + */ + public function getTotalSpace() + { + if ($this->totalSpace === null) { + $smaInfo = apcu_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 = apcu_sma_info(true); + return $smaInfo['avail_mem']; + } + + /* IterableInterface */ + + /** + * Get the storage iterator + * + * @return ApcuIterator + */ + public function getIterator() + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ''; + $pattern = null; + if ($namespace !== '') { + $prefix = $namespace . $options->getNamespaceSeparator(); + $pattern = '/^' . preg_quote($prefix, '/') . '/'; + } + + $baseIt = new BaseApcuIterator($pattern, 0, 1, APC_LIST_ACTIVE); + return new ApcuIterator($this, $baseIt, $prefix); + } + + /* FlushableInterface */ + + /** + * Flush the whole storage + * + * @return bool + */ + public function flush() + { + return apcu_clear_cache(); + } + + /* ClearByNamespaceInterface */ + + /** + * Remove items by given namespace + * + * @param string $namespace + * @return bool + */ + public function clearByNamespace($namespace) + { + $namespace = (string) $namespace; + if ($namespace === '') { + throw new Exception\InvalidArgumentException('No namespace given'); + } + + $options = $this->getOptions(); + $prefix = $namespace . $options->getNamespaceSeparator(); + $pattern = '/^' . preg_quote($prefix, '/') . '/'; + return apcu_delete(new BaseApcuIterator($pattern, 0, 1, APC_LIST_ACTIVE)); + } + + /* ClearByPrefixInterface */ + + /** + * Remove items matching given prefix + * + * @param string $prefix + * @return bool + */ + public function clearByPrefix($prefix) + { + $prefix = (string) $prefix; + if ($prefix === '') { + throw new Exception\InvalidArgumentException('No prefix given'); + } + + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $nsPrefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $pattern = '/^' . preg_quote($nsPrefix . $prefix, '/') . '/'; + return apcu_delete(new BaseApcuIterator($pattern, 0, 1, APC_LIST_ACTIVE)); + } + + /* reading */ + + /** + * Internal method to get an item. + * + * @param string $normalizedKey + * @param bool $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(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $result = apcu_fetch($internalKey, $success); + + if (!$success) { + return; + } + + $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(); + $namespace = $options->getNamespace(); + if ($namespace === '') { + return apcu_fetch($normalizedKeys); + } + + $prefix = $namespace . $options->getNamespaceSeparator(); + $internalKeys = []; + foreach ($normalizedKeys as $normalizedKey) { + $internalKeys[] = $prefix . $normalizedKey; + } + + $fetch = apcu_fetch($internalKeys); + + // remove namespace prefix + $prefixL = strlen($prefix); + $result = []; + foreach ($fetch as $internalKey => & $value) { + $result[substr($internalKey, $prefixL)] = $value; + } + + return $result; + } + + /** + * Internal method to test if an item exists. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalHasItem(& $normalizedKey) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + return apcu_exists($prefix . $normalizedKey); + } + + /** + * Internal method to test multiple items. + * + * @param array $normalizedKeys + * @return array Array of found keys + * @throws Exception\ExceptionInterface + */ + protected function internalHasItems(array & $normalizedKeys) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + if ($namespace === '') { + // array_filter with no callback will remove entries equal to FALSE + return array_keys(array_filter(apcu_exists($normalizedKeys))); + } + + $prefix = $namespace . $options->getNamespaceSeparator(); + $internalKeys = []; + foreach ($normalizedKeys as $normalizedKey) { + $internalKeys[] = $prefix . $normalizedKey; + } + + $exists = apcu_exists($internalKeys); + $result = []; + $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|bool Metadata on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetMetadata(& $normalizedKey) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + + // @see http://pecl.php.net/bugs/bug.php?id=22564 + if (!apcu_exists($internalKey)) { + $metadata = false; + } else { + $format = APC_ITER_ALL ^ APC_ITER_VALUE ^ APC_ITER_TYPE ^ APC_ITER_REFCOUNT; + $regexp = '/^' . preg_quote($internalKey, '/') . '$/'; + $it = new BaseApcuIterator($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 = []; + foreach ($normalizedKeys as $normalizedKey) { + $keysRegExp[] = preg_quote($normalizedKey, '/'); + } + + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefixL = 0; + + if ($namespace === '') { + $pattern = '/^(' . implode('|', $keysRegExp) . ')' . '$/'; + } else { + $prefix = $namespace . $options->getNamespaceSeparator(); + $prefixL = strlen($prefix); + $pattern = '/^' . preg_quote($prefix, '/') . '(' . implode('|', $keysRegExp) . ')' . '$/'; + } + + $format = APC_ITER_ALL ^ APC_ITER_VALUE ^ APC_ITER_TYPE ^ APC_ITER_REFCOUNT; + $it = new BaseApcuIterator($pattern, $format, 100, APC_LIST_ACTIVE); + $result = []; + foreach ($it as $internalKey => $metadata) { + // @see http://pecl.php.net/bugs/bug.php?id=22564 + if (!apcu_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 bool + * @throws Exception\ExceptionInterface + */ + protected function internalSetItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $ttl = $options->getTtl(); + + if (!apcu_store($internalKey, $value, $ttl)) { + $type = is_object($value) ? get_class($value) : gettype($value); + throw new Exception\RuntimeException( + "apcu_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(); + $namespace = $options->getNamespace(); + if ($namespace === '') { + return array_keys(apcu_store($normalizedKeyValuePairs, null, $options->getTtl())); + } + + $prefix = $namespace . $options->getNamespaceSeparator(); + $internalKeyValuePairs = []; + foreach ($normalizedKeyValuePairs as $normalizedKey => &$value) { + $internalKey = $prefix . $normalizedKey; + $internalKeyValuePairs[$internalKey] = &$value; + } + + $failedKeys = apcu_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 bool + * @throws Exception\ExceptionInterface + */ + protected function internalAddItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $ttl = $options->getTtl(); + + if (!apcu_add($internalKey, $value, $ttl)) { + if (apcu_exists($internalKey)) { + return false; + } + + $type = is_object($value) ? get_class($value) : gettype($value); + throw new Exception\RuntimeException( + "apcu_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(); + $namespace = $options->getNamespace(); + if ($namespace === '') { + return array_keys(apcu_add($normalizedKeyValuePairs, null, $options->getTtl())); + } + + $prefix = $namespace . $options->getNamespaceSeparator(); + $internalKeyValuePairs = []; + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + $internalKey = $prefix . $normalizedKey; + $internalKeyValuePairs[$internalKey] = $value; + } + + $failedKeys = apcu_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 bool + * @throws Exception\ExceptionInterface + */ + protected function internalReplaceItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + + if (!apcu_exists($internalKey)) { + return false; + } + + $ttl = $options->getTtl(); + if (!apcu_store($internalKey, $value, $ttl)) { + $type = is_object($value) ? get_class($value) : gettype($value); + throw new Exception\RuntimeException( + "apcu_store('{$internalKey}', <{$type}>, {$ttl}) failed" + ); + } + + return true; + } + + /** + * Internal method to remove an item. + * + * @param string $normalizedKey + * @return bool + * @throws Exception\ExceptionInterface + */ + protected function internalRemoveItem(& $normalizedKey) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + return apcu_delete($prefix . $normalizedKey); + } + + /** + * 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(); + $namespace = $options->getNamespace(); + if ($namespace === '') { + return apcu_delete($normalizedKeys); + } + + $prefix = $namespace . $options->getNamespaceSeparator(); + $internalKeys = []; + foreach ($normalizedKeys as $normalizedKey) { + $internalKeys[] = $prefix . $normalizedKey; + } + + $failedKeys = apcu_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|bool The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalIncrementItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $value = (int) $value; + $newValue = apcu_inc($internalKey, $value); + + // initial value + if ($newValue === false) { + $ttl = $options->getTtl(); + $newValue = $value; + if (!apcu_add($internalKey, $newValue, $ttl)) { + throw new Exception\RuntimeException( + "apcu_add('{$internalKey}', {$newValue}, {$ttl}) failed" + ); + } + } + + return $newValue; + } + + /** + * Internal method to decrement an item. + * + * @param string $normalizedKey + * @param int $value + * @return int|bool The new value on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalDecrementItem(& $normalizedKey, & $value) + { + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); + $internalKey = $prefix . $normalizedKey; + $value = (int) $value; + $newValue = apcu_dec($internalKey, $value); + + // initial value + if ($newValue === false) { + $ttl = $options->getTtl(); + $newValue = -$value; + if (!apcu_add($internalKey, $newValue, $ttl)) { + throw new Exception\RuntimeException( + "apcu_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, + [ + 'supportedDatatypes' => [ + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => true, + 'object' => 'object', + 'resource' => false, + ], + 'supportedMetadata' => [ + '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 APCu + * + * @param array $metadata + * @return void + */ + protected function normalizeMetadata(array & $metadata) + { + $apcMetadata = $metadata; + $metadata = [ + 'internal_key' => isset($metadata['key']) ? $metadata['key'] : $metadata['info'], + 'atime' => isset($metadata['access_time']) ? $metadata['access_time'] : $metadata['atime'], + 'ctime' => isset($metadata['creation_time']) ? $metadata['creation_time'] : $metadata['ctime'], + 'mtime' => isset($metadata['modified_time']) ? $metadata['modified_time'] : $metadata['mtime'], + 'rtime' => isset($metadata['deletion_time']) ? $metadata['deletion_time'] : $metadata['dtime'], + 'size' => $metadata['mem_size'], + 'hits' => isset($metadata['nhits']) ? $metadata['nhits'] : $metadata['num_hits'], + 'ttl' => $metadata['ttl'], + ]; + } + + /** + * Internal method to set an item only if token matches + * + * @param mixed $token + * @param string $normalizedKey + * @param mixed $value + * @return bool + * @see getItem() + * @see setItem() + */ + protected function internalCheckAndSetItem(& $token, & $normalizedKey, & $value) + { + if (is_int($token) && is_int($value)) { + return apcu_cas($normalizedKey, $token, $value); + } + + return parent::internalCheckAndSetItem($token, $normalizedKey, $value); + } +} diff --git a/src/Storage/Adapter/ApcuIterator.php b/src/Storage/Adapter/ApcuIterator.php new file mode 100644 index 000000000..1f14618d7 --- /dev/null +++ b/src/Storage/Adapter/ApcuIterator.php @@ -0,0 +1,157 @@ +storage = $storage; + $this->baseIterator = $baseIterator; + $this->prefixLength = strlen($prefix); + } + + /** + * Get storage instance + * + * @return Apcu + */ + 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 ApcuIterator 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 bool + */ + 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/ApcuOptions.php b/src/Storage/Adapter/ApcuOptions.php new file mode 100644 index 000000000..8d5893c9a --- /dev/null +++ b/src/Storage/Adapter/ApcuOptions.php @@ -0,0 +1,47 @@ +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/AdapterPluginManager.php b/src/Storage/AdapterPluginManager.php index 84882cd33..299c78363 100644 --- a/src/Storage/AdapterPluginManager.php +++ b/src/Storage/AdapterPluginManager.php @@ -28,6 +28,7 @@ class AdapterPluginManager extends AbstractPluginManager */ protected $invokableClasses = [ 'apc' => 'Zend\Cache\Storage\Adapter\Apc', + 'apcu' => 'Zend\Cache\Storage\Adapter\Apcu', 'blackhole' => 'Zend\Cache\Storage\Adapter\BlackHole', 'dba' => 'Zend\Cache\Storage\Adapter\Dba', 'filesystem' => 'Zend\Cache\Storage\Adapter\Filesystem', diff --git a/test/Storage/Adapter/ApcuTest.php b/test/Storage/Adapter/ApcuTest.php new file mode 100644 index 000000000..9e7e412cc --- /dev/null +++ b/test/Storage/Adapter/ApcuTest.php @@ -0,0 +1,60 @@ +markTestSkipped('Enable TESTS_ZEND_CACHE_APCU_ENABLED to run this test'); + } + + try { + new Cache\Storage\Adapter\Apcu(); + } catch (Cache\Exception\ExtensionNotLoadedException $e) { + $this->markTestSkipped($e->getMessage()); + } + + // 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\ApcuOptions(); + $this->_storage = new Cache\Storage\Adapter\Apcu(); + $this->_storage->setOptions($this->_options); + + parent::setUp(); + } + + public function tearDown() + { + if (function_exists('apcu_clear_cache')) { + apcu_clear_cache(); + } + + // reset ini configurations + ini_set('apc.use_request_time', $this->iniUseRequestTime); + + parent::tearDown(); + } +}