diff --git a/src/Storage/Adapter/AbstractAdapter.php b/src/Storage/Adapter/AbstractAdapter.php index 0d76dbda1..593bf85c6 100644 --- a/src/Storage/Adapter/AbstractAdapter.php +++ b/src/Storage/Adapter/AbstractAdapter.php @@ -331,6 +331,11 @@ 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); return $this; diff --git a/src/Storage/Adapter/Memcached.php b/src/Storage/Adapter/Memcached.php index ee6b6005c..7dab2740e 100644 --- a/src/Storage/Adapter/Memcached.php +++ b/src/Storage/Adapter/Memcached.php @@ -52,7 +52,7 @@ class Memcached extends AbstractAdapter * @throws Exception * @return void */ - public function __construct() + public function __construct($options = null) { if (!extension_loaded('memcached')) { throw new Exception\ExtensionNotLoadedException("Memcached extension is not loaded"); @@ -60,6 +60,13 @@ public function __construct() $this->memcached= new MemcachedResource(); + if (!empty($options)) { + $this->setOptions($options); + } + + $options= $this->getOptions(); + $this->memcached->addServer($options->getServer(), $options->getPort()); + } /* options */ @@ -478,7 +485,14 @@ public function replaceItem($key, $value, array $options = array()) return $eventRs->last(); } - $result= $this->memcached->replace($internalKey, $value, $options['ttl']); + $internalKey = $options['namespace'] . $baseOptions->getNamespaceSeparator() . $key; + if (!$this->memcached->get($internalKey)) { + throw new Exception\ItemNotFoundException( + "Key '{$internalKey}' doesn't exist" + ); + } + + $result = $this->memcached->replace($internalKey, $value, $options['ttl']); if ($result === false) { $type = is_object($value) ? get_class($value) : gettype($value); @@ -534,14 +548,15 @@ public function removeItem($key, array $options = array()) $internalKey = $options['namespace'] . $baseOptions->getNamespaceSeparator() . $key; - $result= $this->memcached->delete($internalKey); + $result = $this->memcached->delete($internalKey); if ($result === false) { if (!$options['ignore_missing_items']) { throw new Exception\ItemNotFoundException("Key '{$internalKey}' not found"); } } - + $result = true; + return $this->triggerPost(__FUNCTION__, $args, $result); } catch (\Exception $e) { return $this->triggerException(__FUNCTION__, $args, $e); @@ -672,43 +687,11 @@ public function decrementItem($key, $value, array $options = array()) /* non-blocking */ - /** - * Find items. - * - * Options: - * - ttl optional - * - The time-to-life (Default: ttl of object) - * - namespace optional - * - The namespace to use (Default: namespace of object) - * - tags optional - * - Tags to search for used with matching modes of - * Zend\Cache\Storage\Adapter::MATCH_TAGS_* - * - * @param int $mode Matching mode (Value of Zend\Cache\Storage\Adapter::MATCH_*) - * @param array $options - * @return boolean - * @throws Exception - * @see fetch() - * @see fetchAll() - * - * @triggers find.pre(PreEvent) - * @triggers find.post(PostEvent) - * @triggers find.exception(ExceptionEvent) - */ - public function find($mode = self::MATCH_ACTIVE, array $options=array()) - { - throw Exception\RuntimeException(sprintf( - '%s is not yet implemented', - __METHOD__ - )); - } - /** * Fetches the next item from result set * * @return array|boolean The next item or false - * @throws Exception - * @see fetchAll() + * @see fetchAll() * * @triggers fetch.pre(PreEvent) * @triggers fetch.post(PostEvent) @@ -716,12 +699,72 @@ public function find($mode = self::MATCH_ACTIVE, array $options=array()) */ public function fetch() { - throw Exception\RuntimeException(sprintf( - '%s is not yet implemented', - __METHOD__ - )); + if (!$this->stmtActive) { + return false; + } + + $args = new ArrayObject(); + + try { + $eventRs = $this->triggerPre(__FUNCTION__, $args); + if ($eventRs->stopped()) { + return $eventRs->last(); + } + + $prefixL = strlen($this->stmtOptions['namespace'] . $this->getOptions()->getNamespaceSeparator()); + + if (!$this->stmtIterator) { + // clear stmt + $this->stmtActive = false; + $this->stmtIterator = null; + $this->stmtOptions = null; + + $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); + } + } + } + + return $this->triggerPost(__FUNCTION__, $args, $result); + } catch (\Exception $e) { + return $this->triggerException(__FUNCTION__, $args, $e); + } } + /** + * FetchAll + * + * @throws Exception + * @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 { + unset($elem['key']); + } + } + + return $result; + } + /* cleaning */ /** @@ -807,8 +850,7 @@ public function getCapabilities() 'object' => 'object', 'resource' => false, ), - 'supportedMetadata' => array( - ), + 'supportedMetadata' => array(), 'maxTtl' => 0, 'staticTtl' => false, 'tagging' => false, @@ -830,6 +872,71 @@ 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. * diff --git a/src/Storage/Adapter/MemcachedOptions.php b/src/Storage/Adapter/MemcachedOptions.php index a3ec0da58..6dbebb322 100644 --- a/src/Storage/Adapter/MemcachedOptions.php +++ b/src/Storage/Adapter/MemcachedOptions.php @@ -62,6 +62,20 @@ class MemcachedOptions extends AdapterOptions 'tcp_nodelay' => MemcachedResource::OPT_TCP_NODELAY, ); + /** + * Memcached server address + * + * @var string + */ + protected $server = 'localhost'; + + /** + * Memcached port + * + * @var integer + */ + protected $port = 11211; + /** * Whether or not to enable binary protocol for communication with server * @@ -202,6 +216,37 @@ class MemcachedOptions extends AdapterOptions */ protected $tcpNodelay = false; + 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)) + || 0 > $port + ) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects a positive integer', + __METHOD__ + )); + } + + $this->port= $port; + return $this; + } + + public function getPort() + { + return $this->port; + } + /** * Set flag indicating whether or not to enable binary protocol for * communication with server @@ -708,6 +753,10 @@ public function getServerFailureLimit() */ public function setSocketSendSize($socketSendSize) { + if ($socketSendSize === null) { + return $this; + } + if ((!is_int($socketSendSize) && !is_numeric($socketSendSize)) || 0 > $socketSendSize ) { @@ -739,6 +788,10 @@ public function getSocketSendSize() */ public function setSocketRecvSize($socketRecvSize) { + if ($socketRecvSize === null) { + return $this; + } + if ((!is_int($socketRecvSize) && !is_numeric($socketRecvSize)) || 0 > $socketRecvSize ) { diff --git a/test/Storage/Adapter/CommonAdapterTest.php b/test/Storage/Adapter/CommonAdapterTest.php index daac6febe..88875528a 100644 --- a/test/Storage/Adapter/CommonAdapterTest.php +++ b/test/Storage/Adapter/CommonAdapterTest.php @@ -247,18 +247,33 @@ public function testGetItemReturnsFalseIfNonReadable() public function testGetMetadata() { + $capabilities = $this->_storage->getCapabilities(); + if (!$capabilities->getSupportedMetadata()) { + $this->markTestSkipped("Metadata are not supported by the adapter"); + } + $this->assertTrue($this->_storage->setItem('key', 'value')); $this->assertInternalType('array', $this->_storage->getMetadata('key')); } 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'); @@ -267,6 +282,11 @@ 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')); @@ -275,6 +295,11 @@ public function testGetMetadataReturnsFalseIfNonReadable() public function testGetMetadatas() { + $capabilities = $this->_storage->getCapabilities(); + if (!$capabilities->getSupportedMetadata()) { + $this->markTestSkipped("Metadata are not supported by the adapter"); + } + $items = array( 'key1' => 'value1', 'key2' => 'value2' @@ -292,6 +317,11 @@ public function testGetMetadatas() 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')); @@ -300,6 +330,11 @@ public function testGetMetadatasReturnsEmptyArrayIfNonReadable() 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')); @@ -726,7 +761,7 @@ public function testGetDelayedAndFetch() $this->assertTrue($this->_storage->getDelayed(array_keys($items))); $fetchedKeys = array(); - while ( ($item = $this->_storage->fetch()) ) { + while ( $item = $this->_storage->fetch() ) { $this->assertArrayHasKey('key', $item); $this->assertArrayHasKey('value', $item); @@ -798,11 +833,14 @@ public function testGetDelayedAndFetchAllWithSelectInfo() ))); $fetchedItems = $this->_storage->fetchAll(); + $this->assertEquals(count($items), count($fetchedItems)); foreach ($fetchedItems as $item) { - foreach ($capabilities->getSupportedMetadata() as $selectProperty) { - $this->assertArrayHasKey($selectProperty, $item); - } + if (is_array($capabilities->getSupportedMetadata())) { + foreach ($capabilities->getSupportedMetadata() as $selectProperty) { + $this->assertArrayHasKey($selectProperty, $item); + } + } } } @@ -964,7 +1002,7 @@ public function testTouchItem() usleep($capabilities->getTtlPrecision() * 2000000); $this->assertTrue($this->_storage->touchItem('key')); - usleep($capabilities->getTtlPrecision() * 2000000); + usleep($capabilities->getTtlPrecision() * 1000000); $this->assertTrue($this->_storage->hasItem('key')); if (!$capabilities->getUseRequestTime()) { diff --git a/test/Storage/Adapter/MemcachedTest.php b/test/Storage/Adapter/MemcachedTest.php index 5c7ac92df..d4f801203 100644 --- a/test/Storage/Adapter/MemcachedTest.php +++ b/test/Storage/Adapter/MemcachedTest.php @@ -21,8 +21,7 @@ namespace ZendTest\Cache\Storage\Adapter; -use Memcached, - Zend\Cache, +use Zend\Cache, Zend\Cache\Exception; /** @@ -51,9 +50,8 @@ public function setUp() public function tearDown() { - if (extension_loaded('memcached')) { - $m = new Memcached(); - $m->flush(); + if (!empty($this->_storage)) { + $this->_storage->clear(); } parent::tearDown();