From cc7cec224309ca6559c32c7c74fe6916128a7a1e Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney <matthew@zend.com> Date: Wed, 18 Apr 2018 12:35:59 -0500 Subject: [PATCH 1/2] Implements an adapter for ext-mongodb This patch is an alternative to #132, proposing a parallel adapter for ext-mongodb, instead of repurposing the existing ext-mongo adapter to only work with ext-mongodb. It copies the various `MongoDb*` classes to `ExtMongoDb*` variants, and applies the changes as suggested in #132. Additionally, it sets up the test harness to test ext-mongodb support in PHP 7.1 and 7.2, while testing ext-mongo support in PHP 5.6 and 7.0. When it tests ext-mongodb, it also adds a dependency on mongodb/mongodb so that the client library is available. --- .ci/mongodb.ini | 1 + .travis.yml | 17 +- composer.json | 2 + phpunit.xml.dist | 4 + src/Storage/Adapter/ExtMongoDb.php | 284 ++++++++++++++++++ src/Storage/Adapter/ExtMongoDbOptions.php | 187 ++++++++++++ .../Adapter/ExtMongoDbResourceManager.php | 253 ++++++++++++++++ src/Storage/AdapterPluginManager.php | 10 +- .../Storage/Adapter/ExtMongoDbOptionsTest.php | 117 ++++++++ .../Adapter/ExtMongoDbResourceManagerTest.php | 197 ++++++++++++ test/Storage/Adapter/ExtMongoDbTest.php | 73 +++++ 11 files changed, 1142 insertions(+), 3 deletions(-) create mode 100644 .ci/mongodb.ini create mode 100644 src/Storage/Adapter/ExtMongoDb.php create mode 100644 src/Storage/Adapter/ExtMongoDbOptions.php create mode 100644 src/Storage/Adapter/ExtMongoDbResourceManager.php create mode 100644 test/Storage/Adapter/ExtMongoDbOptionsTest.php create mode 100644 test/Storage/Adapter/ExtMongoDbResourceManagerTest.php create mode 100644 test/Storage/Adapter/ExtMongoDbTest.php diff --git a/.ci/mongodb.ini b/.ci/mongodb.ini new file mode 100644 index 000000000..45969d065 --- /dev/null +++ b/.ci/mongodb.ini @@ -0,0 +1 @@ +extension=mongodb.so diff --git a/.travis.yml b/.travis.yml index 846f6cedb..47cebbe5d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ env: global: - COMPOSER_ARGS="--no-interaction" - COVERAGE_DEPS="php-coveralls/php-coveralls" + - EXTMONGODB_DEPS="mongodb/mongodb" - TESTS_ZEND_CACHE_APC_ENABLED=true - TESTS_ZEND_CACHE_APCU_ENABLED=true - TESTS_ZEND_CACHE_FILESYSTEM_DIR=/dev/shm @@ -25,6 +26,7 @@ env: - TESTS_ZEND_CACHE_MEMCACHE_HOST=$TESTS_ZEND_CACHE_MEMCACHED_HOST - TESTS_ZEND_CACHE_MEMCACHE_PORT=$TESTS_ZEND_CACHE_MEMCACHED_PORT - TESTS_ZEND_CACHE_MONGODB_ENABLED=true + - TESTS_ZEND_CACHE_EXTMONGODB_ENABLED=true - TESTS_ZEND_CACHE_REDIS_ENABLED=true - TESTS_ZEND_CACHE_REDIS_HOST="127.0.0.1" - TESTS_ZEND_CACHE_REDIS_PORT=6379 @@ -42,6 +44,7 @@ matrix: env: - DEPS=lowest - APCU_PECL_VERSION="apcu-4.0.8" + - TESTS_ZEND_CACHE_EXTMONGODB_ENABLED=false - TESTS_ZEND_CACHE_MEMCACHE_ENABLED=true - TESTS_ZEND_CACHE_XCACHE_ENABLED=true - php: 5.6 @@ -49,12 +52,14 @@ matrix: - DEPS=locked - LEGACY_DEPS="phpbench/phpbench phpunit/phpunit" - APCU_PECL_VERSION="apcu-4.0.10" + - TESTS_ZEND_CACHE_EXTMONGODB_ENABLED=false - TESTS_ZEND_CACHE_MEMCACHE_ENABLED=true - TESTS_ZEND_CACHE_XCACHE_ENABLED=true - php: 5.6 env: - DEPS=latest - APCU_PECL_VERSION="apcu-4.0.11" + - TESTS_ZEND_CACHE_EXTMONGODB_ENABLED=false - TESTS_ZEND_CACHE_MEMCACHE_ENABLED=true - TESTS_ZEND_CACHE_XCACHE_ENABLED=true - php: 7 @@ -63,6 +68,7 @@ matrix: - APCU_PECL_VERSION="apcu-5.1.2" - APCU_BC_PECL_VERSION="apcu_bc-1.0.0" - TESTS_ZEND_CACHE_MONGODB_USE_POLYFILL=true + - TESTS_ZEND_CACHE_EXTMONGODB_ENABLED=false - php: 7 env: - DEPS=locked @@ -70,12 +76,14 @@ matrix: - APCU_PECL_VERSION="apcu-5.1.8" - APCU_BC_PECL_VERSION="apcu_bc-1.0.2" - TESTS_ZEND_CACHE_MONGODB_USE_POLYFILL=true + - TESTS_ZEND_CACHE_EXTMONGODB_ENABLED=false - php: 7 env: - DEPS=latest - APCU_PECL_VERSION="apcu" - APCU_BC_PECL_VERSION="apcu_bc-1.0.3" - TESTS_ZEND_CACHE_MONGODB_USE_POLYFILL=true + - TESTS_ZEND_CACHE_EXTMONGODB_ENABLED=false - php: 7.1 env: - DEPS=lowest @@ -158,10 +166,14 @@ install: phpenv config-add .ci/memcached.ini ; fi ; - if [[ $TESTS_ZEND_CACHE_MONGODB_ENABLED == 'true' && $TESTS_ZEND_CACHE_MONGODB_USE_POLYFILL != 'true' ]]; then + - if [[ $TESTS_ZEND_CACHE_MONGODB_ENABLED == 'true' && $TESTS_ZEND_CACHE_MONGODB_USE_POLYFILL != 'true' ]]; then phpenv config-add .ci/mongo.ini ; fi ; + - if [[ $TESTS_ZEND_CACHE_EXTMONGODB_ENABLED == 'true' ]]; then + phpenv config-add .ci/mongodb.ini ; + fi ; + - if [[ $TESTS_ZEND_CACHE_XCACHE_ENABLED == 'true' ]]; then phpenv config-add .ci/xcache.ini ; fi ; @@ -171,7 +183,8 @@ install: - if [[ $DEPS == 'latest' ]]; then travis_retry composer update $COMPOSER_ARGS ; fi - if [[ $DEPS == 'lowest' ]]; then travis_retry composer update --prefer-lowest --prefer-stable $COMPOSER_ARGS ; fi - if [[ $TEST_COVERAGE == 'true' ]]; then travis_retry composer require --dev $COMPOSER_ARGS $COVERAGE_DEPS ; fi - - if [[ $TESTS_ZEND_CACHE_MONGODB_USE_POLYFILL == 'true' ]]; then travis_retry composer require --dev --no-update mongofill/mongofill:dev-master ; fi + - if [[ $TESTS_ZEND_CACHE_MONGODB_USE_POLYFILL == 'true' ]]; then travis_retry composer require --dev --no-update $COMPOSER_ARGS mongofill/mongofill:dev-master ; fi + - if [[ $TESTS_ZEND_CACHE_EXTMONGODB_ENABLED == 'true' ]]; then travis_retry composer require --dev --no-update $COMPOSER_ARGS $EXTMONGODB_DEPS ; fi - stty cols 120 && composer show - pecl list - php -m diff --git a/composer.json b/composer.json index 5e1bda47e..cce7cde48 100644 --- a/composer.json +++ b/composer.json @@ -39,9 +39,11 @@ "ext-memcache": "Memcache >= 2.0.0 to use the Memcache storage adapter", "ext-memcached": "Memcached >= 1.0.0 to use the Memcached storage adapter", "ext-mongo": "Mongo, to use MongoDb storage adapter", + "ext-mongodb": "MongoDB, to use the ExtMongoDb storage adapter", "ext-redis": "Redis, to use Redis storage adapter", "ext-wincache": "WinCache, to use the WinCache storage adapter", "ext-xcache": "XCache, to use the XCache storage adapter", + "mongodb/mongodb": "Required for use with the ext-mongodb adapter", "mongofill/mongofill": "Alternative to ext-mongo - a pure PHP implementation designed as a drop in replacement" }, "autoload": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 84c843a7d..98938de96 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -46,6 +46,10 @@ <env name="TESTS_ZEND_CACHE_MONGODB_COLLECTION" value="cache" /> <env name="TESTS_ZEND_CACHE_MONGODB_CONNECTSTRING" value="mongodb://localhost/" /> <env name="TESTS_ZEND_CACHE_MONGODB_DATABASE" value="zend_test" /> + <env name="TESTS_ZEND_CACHE_EXTMONGODB_ENABLED" value="false" /> + <env name="TESTS_ZEND_CACHE_EXTMONGODB_COLLECTION" value="cache" /> + <env name="TESTS_ZEND_CACHE_EXTMONGODB_CONNECTSTRING" value="mongodb://localhost/" /> + <env name="TESTS_ZEND_CACHE_EXTMONGODB_DATABASE" value="zend_test" /> <env name="TESTS_ZEND_CACHE_REDIS_ENABLED" value="false" /> <env name="TESTS_ZEND_CACHE_REDIS_HOST" value="127.0.0.1" /> <env name="TESTS_ZEND_CACHE_REDIS_PORT" value="6379" /> diff --git a/src/Storage/Adapter/ExtMongoDb.php b/src/Storage/Adapter/ExtMongoDb.php new file mode 100644 index 000000000..c96bc01d8 --- /dev/null +++ b/src/Storage/Adapter/ExtMongoDb.php @@ -0,0 +1,284 @@ +<?php +/** + * @see https://github.com/zendframework/zend-cache for the canonical source repository + * @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com) + * @license https://github.com/zendframework/zend-cache/blob/master/LICENSE.md New BSD License + */ + +namespace Zend\Cache\Storage\Adapter; + +use MongoDB\Client; +use MongoDB\Collection; +use MongoDB\BSON\UTCDateTime as MongoDate; +use MongoDB\Driver\Exception\Exception as MongoDriverException; +use stdClass; +use Zend\Cache\Exception; +use Zend\Cache\Storage\Capabilities; +use Zend\Cache\Storage\FlushableInterface; + +/** + * Cache storage adapter for ext-mongodb + * + * If you are using ext-mongo, use the MongoDb adapter instead. + */ +class ExtMongoDb extends AbstractAdapter implements FlushableInterface +{ + /** + * Has this instance be initialized + * + * @var bool + */ + private $initialized = false; + + /** + * the mongodb resource manager + * + * @var null|ExtMongoDbResourceManager + */ + private $resourceManager; + + /** + * The mongodb resource id + * + * @var null|string + */ + private $resourceId; + + /** + * The namespace prefix + * + * @var string + */ + private $namespacePrefix = ''; + + /** + * {@inheritDoc} + * + * @throws Exception\ExtensionNotLoadedException + */ + public function __construct($options = null) + { + if (! extension_loaded('mongodb') || ! class_exists(Client::class)) { + throw new Exception\ExtensionNotLoadedException( + 'mongodb extension not loaded or Mongo PHP client library not installed' + ); + } + + parent::__construct($options); + + $initialized = & $this->initialized; + + $this->getEventManager()->attach( + 'option', + function () use (& $initialized) { + $initialized = false; + } + ); + } + + /** + * get mongodb resource + * + * @return Collection + */ + private function getMongoCollection() + { + if (! $this->initialized) { + $options = $this->getOptions(); + + $this->resourceManager = $options->getResourceManager(); + $this->resourceId = $options->getResourceId(); + $namespace = $options->getNamespace(); + $this->namespacePrefix = ($namespace === '' ? '' : $namespace . $options->getNamespaceSeparator()); + $this->initialized = true; + } + + return $this->resourceManager->getResource($this->resourceId); + } + + /** + * {@inheritDoc} + */ + public function setOptions($options) + { + return parent::setOptions( + $options instanceof ExtMongoDbOptions + ? $options + : new ExtMongoDbOptions($options) + ); + } + + /** + * Get options. + * + * @see setOptions() + * @return ExtMongoDbOptions + */ + public function getOptions() + { + return $this->options; + } + + /** + * {@inheritDoc} + * + * @throws Exception\RuntimeException + */ + protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null) + { + $result = $this->fetchFromCollection($normalizedKey); + $success = false; + + if (null === $result) { + return; + } + + if (isset($result['expires'])) { + if (! $result['expires'] instanceof MongoDate) { + throw new Exception\RuntimeException(sprintf( + "The found item _id '%s' for key '%s' is not a valid cache item" + . ": the field 'expired' isn't an instance of MongoDate, '%s' found instead", + (string) $result['_id'], + $this->namespacePrefix . $normalizedKey, + is_object($result['expires']) ? get_class($result['expires']) : gettype($result['expires']) + )); + } + + if ($result['expires']->sec < (new MongoDate())) { + $this->internalRemoveItem($normalizedKey); + return; + } + } + + if (! array_key_exists('value', $result)) { + throw new Exception\RuntimeException(sprintf( + "The found item _id '%s' for key '%s' is not a valid cache item: missing the field 'value'", + (string) $result['_id'], + $this->namespacePrefix . $normalizedKey + )); + } + + $success = true; + + return $casToken = $result['value']; + } + + /** + * {@inheritDoc} + * + * @throws Exception\RuntimeException + */ + protected function internalSetItem(& $normalizedKey, & $value) + { + $mongo = $this->getMongoCollection(); + $key = $this->namespacePrefix . $normalizedKey; + $ttl = $this->getOptions()->getTTl(); + $expires = null; + $cacheItem = [ + 'key' => $key, + 'value' => $value, + ]; + + if ($ttl > 0) { + $ttlSeconds = round((microtime(true) + $ttl) * 1000); + $cacheItem['expires'] = new MongoDate($ttlSeconds); + } + + try { + $mongo->deleteOne(['key' => $key]); + $result = $mongo->insertOne($cacheItem); + } catch (MongoDriverException $e) { + throw new Exception\RuntimeException($e->getMessage(), $e->getCode(), $e); + } + + return null !== $result && $result->isAcknowledged(); + } + + /** + * {@inheritDoc} + * + * @throws Exception\RuntimeException + */ + protected function internalRemoveItem(& $normalizedKey) + { + try { + $result = $this->getMongoCollection()->deleteOne(['key' => $this->namespacePrefix . $normalizedKey]); + } catch (MongoDriverException $e) { + throw new Exception\RuntimeException($e->getMessage(), $e->getCode(), $e); + } + + return null !== $result && $result->getDeletedCount() > 0; + } + + /** + * {@inheritDoc} + */ + public function flush() + { + $result = $this->getMongoCollection()->drop(); + return ((float) 1) === $result['ok']; + } + + /** + * {@inheritDoc} + */ + protected function internalGetCapabilities() + { + if ($this->capabilities) { + return $this->capabilities; + } + + return $this->capabilities = new Capabilities( + $this, + $this->capabilityMarker = new stdClass(), + [ + 'supportedDatatypes' => [ + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => true, + 'object' => false, + 'resource' => false, + ], + 'supportedMetadata' => [ + '_id', + ], + 'minTtl' => 1, + 'staticTtl' => true, + 'maxKeyLength' => 255, + 'namespaceIsPrefix' => true, + ] + ); + } + + /** + * {@inheritDoc} + * + * @throws Exception\ExceptionInterface + */ + protected function internalGetMetadata(& $normalizedKey) + { + $result = $this->fetchFromCollection($normalizedKey); + return null !== $result ? ['_id' => $result['_id']] : false; + } + + /** + * Return raw records from MongoCollection + * + * @param string $normalizedKey + * + * @return array|null + * + * @throws Exception\RuntimeException + */ + private function fetchFromCollection(& $normalizedKey) + { + try { + return $this->getMongoCollection()->findOne(['key' => $this->namespacePrefix . $normalizedKey]); + } catch (MongoDriverException $e) { + throw new Exception\RuntimeException($e->getMessage(), $e->getCode(), $e); + } + } +} diff --git a/src/Storage/Adapter/ExtMongoDbOptions.php b/src/Storage/Adapter/ExtMongoDbOptions.php new file mode 100644 index 000000000..182949dda --- /dev/null +++ b/src/Storage/Adapter/ExtMongoDbOptions.php @@ -0,0 +1,187 @@ +<?php +/** + * @see https://github.com/zendframework/zend-cache for the canonical source repository + * @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com) + * @license https://github.com/zendframework/zend-cache/blob/master/LICENSE.md New BSD License + */ + +namespace Zend\Cache\Storage\Adapter; + +/** + * Options for the ext-mongodb adapter implementation. + * + * If you are using ext-mongo, use the MongoDbOptions class instead. + */ +class ExtMongoDbOptions extends AdapterOptions +{ + // @codingStandardsIgnoreStart + /** + * Prioritized properties ordered by prio to be set first + * in case a bulk of options sets set at once + * + * @var string[] + */ + protected $__prioritizedProperties__ = [ + 'resource_manager', + 'resource_id' + ]; + // @codingStandardsIgnoreEnd + + /** + * The namespace separator + * + * @var string + */ + private $namespaceSeparator = ':'; + + /** + * The ext-mongodb resource manager + * + * @var null|ExtMongoDbResourceManager + */ + private $resourceManager; + + /** + * The resource id of the resource manager + * + * @var string + */ + private $resourceId = 'default'; + + /** + * Set namespace separator + * + * @param string $namespaceSeparator + * @return self Provides a fluent interface + */ + public function setNamespaceSeparator($namespaceSeparator) + { + $namespaceSeparator = (string) $namespaceSeparator; + + if ($this->namespaceSeparator !== $namespaceSeparator) { + $this->triggerOptionEvent('namespace_separator', $namespaceSeparator); + + $this->namespaceSeparator = $namespaceSeparator; + } + + return $this; + } + + /** + * Get namespace separator + * + * @return string + */ + public function getNamespaceSeparator() + { + return $this->namespaceSeparator; + } + + /** + * Set the ext-mongodb resource manager to use + * + * @param null|ExtMongoDbResourceManager $resourceManager + * @return self Provides a fluent interface + */ + public function setResourceManager(ExtMongoDbResourceManager $resourceManager = null) + { + if ($this->resourceManager !== $resourceManager) { + $this->triggerOptionEvent('resource_manager', $resourceManager); + + $this->resourceManager = $resourceManager; + } + + return $this; + } + + /** + * Get the ext-mongodb resource manager + * + * @return ExtMongoDbResourceManager + */ + public function getResourceManager() + { + return $this->resourceManager ?: $this->resourceManager = new ExtMongoDbResourceManager(); + } + + /** + * Get the ext-mongodb resource id + * + * @return string + */ + public function getResourceId() + { + return $this->resourceId; + } + + /** + * Set the ext-mongodb resource id + * + * @param string $resourceId + * @return self Provides a fluent interface + */ + public function setResourceId($resourceId) + { + $resourceId = (string) $resourceId; + + if ($this->resourceId !== $resourceId) { + $this->triggerOptionEvent('resource_id', $resourceId); + + $this->resourceId = $resourceId; + } + + return $this; + } + + /** + * Set the ext-mongodb server + * + * @param string $server + * @return self Provides a fluent interface + */ + public function setServer($server) + { + $this->getResourceManager()->setServer($this->getResourceId(), $server); + return $this; + } + + /** + * @param array $connectionOptions + * @return self Provides a fluent interface + */ + public function setConnectionOptions(array $connectionOptions) + { + $this->getResourceManager()->setConnectionOptions($this->getResourceId(), $connectionOptions); + return $this; + } + + /** + * @param array $driverOptions ext-mongodb driver options + * @return self Provides a fluent interface + */ + public function setDriverOptions(array $driverOptions) + { + $this->getResourceManager()->setDriverOptions($this->getResourceId(), $driverOptions); + return $this; + } + + /** + * @param string $database + * @return string Provides a fluent interface + */ + public function setDatabase($database) + { + $this->getResourceManager()->setDatabase($this->getResourceId(), $database); + return $this; + } + + /** + * @param string $collection + * @return self Provides a fluent interface + */ + public function setCollection($collection) + { + $this->getResourceManager()->setCollection($this->getResourceId(), $collection); + return $this; + } +} diff --git a/src/Storage/Adapter/ExtMongoDbResourceManager.php b/src/Storage/Adapter/ExtMongoDbResourceManager.php new file mode 100644 index 000000000..f9f9b518b --- /dev/null +++ b/src/Storage/Adapter/ExtMongoDbResourceManager.php @@ -0,0 +1,253 @@ +<?php +/** + * @see https://github.com/zendframework/zend-cache for the canonical source repository + * @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com) + * @license https://github.com/zendframework/zend-cache/blob/master/LICENSE.md New BSD License + */ + +namespace Zend\Cache\Storage\Adapter; + +use MongoDB\Client; +use MongoDB\Collection; +use MongoDB\Driver\Exception\Exception as MongoDriverException; +use Zend\Cache\Exception; + +/** + * Resource manager for the ext-mongodb adapter. + * + * If you are using ext-mongo, use the MongoDbResourceManager instead. + */ +class ExtMongoDbResourceManager +{ + /** + * Registered resources + * + * @var array[] + */ + private $resources = []; + + /** + * Check if a resource exists + * + * @param string $id + * + * @return bool + */ + public function hasResource($id) + { + return isset($this->resources[$id]); + } + + /** + * Set a resource + * + * @param string $id + * @param array|Collection $resource + * @return self Provides a fluent interface + * @throws Exception\RuntimeException + */ + public function setResource($id, $resource) + { + if ($resource instanceof Collection) { + $this->resources[$id] = [ + 'db' => (string) $resource->db, + 'db_instance' => $resource->db, + 'collection' => (string) $resource, + 'collection_instance' => $resource, + ]; + return $this; + } + + if (! is_array($resource)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects an array or %s; received %s', + __METHOD__, + Collection::class, + is_object($resource) ? get_class($resource) : gettype($resource) + )); + } + + $this->resources[$id] = $resource; + return $this; + } + + /** + * Instantiate and return the Collection resource + * + * @param string $id + * @return Collection + * @throws Exception\RuntimeException + */ + public function getResource($id) + { + if (! $this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + $resource = $this->resources[$id]; + if (! isset($resource['collection_instance'])) { + try { + if (! isset($resource['db_instance'])) { + if (! isset($resource['client_instance'])) { + $resource['client_instance'] = new Client( + isset($resource['server']) ? $resource['server'] : null, + isset($resource['connection_options']) ? $resource['connection_options'] : [], + isset($resource['driver_options']) ? $resource['driver_options'] : [] + ); + } + } + + $collection = $resource['client_instance']->selectCollection( + isset($resouce['db']) ? $resource['db'] : 'zend', + isset($resource['collection']) ? $resource['collection'] : 'cache' + ); + $collection->createIndex(['key' => 1]); + + $this->resources[$id]['collection_instance'] = $collection; + } catch (MongoDriverException $e) { + throw new Exception\RuntimeException($e->getMessage(), $e->getCode(), $e); + } + } + + return $this->resources[$id]['collection_instance']; + } + + /** + * @param string $id + * @param string $server + * @return void + */ + public function setServer($id, $server) + { + $this->resources[$id]['server'] = (string) $server; + + unset($this->resources[$id]['client_instance']); + unset($this->resources[$id]['db_instance']); + unset($this->resources[$id]['collection_instance']); + } + + /** + * @param string $id + * @return null|string + * @throws Exception\RuntimeException if no matching resource discovered + */ + public function getServer($id) + { + if (! $this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + return isset($this->resources[$id]['server']) ? $this->resources[$id]['server'] : null; + } + + /** + * @param string $id + * @param array $connectionOptions + * @return void + */ + public function setConnectionOptions($id, array $connectionOptions) + { + $this->resources[$id]['connection_options'] = $connectionOptions; + + unset($this->resources[$id]['client_instance']); + unset($this->resources[$id]['db_instance']); + unset($this->resources[$id]['collection_instance']); + } + + /** + * @param string $id + * @return array + * @throws Exception\RuntimeException if no matching resource discovered + */ + public function getConnectionOptions($id) + { + if (! $this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + return isset($this->resources[$id]['connection_options']) + ? $this->resources[$id]['connection_options'] + : []; + } + + /** + * @param string $id + * @param array $driverOptions + * @return void + */ + public function setDriverOptions($id, array $driverOptions) + { + $this->resources[$id]['driver_options'] = $driverOptions; + + unset($this->resources[$id]['client_instance']); + unset($this->resources[$id]['db_instance']); + unset($this->resources[$id]['collection_instance']); + } + + /** + * @param string $id + * @return array + * @throws Exception\RuntimeException if no matching resource discovered + */ + public function getDriverOptions($id) + { + if (! $this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + return isset($this->resources[$id]['driver_options']) ? $this->resources[$id]['driver_options'] : []; + } + + /** + * @param string $id + * @param string $database + * @return void + */ + public function setDatabase($id, $database) + { + $this->resources[$id]['db'] = (string) $database; + + unset($this->resources[$id]['db_instance']); + unset($this->resources[$id]['collection_instance']); + } + + /** + * @param string $id + * @return string + * @throws Exception\RuntimeException if no matching resource discovered + */ + public function getDatabase($id) + { + if (! $this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + return isset($this->resources[$id]['db']) ? $this->resources[$id]['db'] : ''; + } + + /** + * @param string $id + * @param string $collection + * @return void + */ + public function setCollection($id, $collection) + { + $this->resources[$id]['collection'] = (string) $collection; + + unset($this->resources[$id]['collection_instance']); + } + + /** + * @param string $id + * @return string + * @throws Exception\RuntimeException if no matching resource discovered + */ + public function getCollection($id) + { + if (! $this->hasResource($id)) { + throw new Exception\RuntimeException("No resource with id '{$id}'"); + } + + return isset($this->resources[$id]['collection']) ? $this->resources[$id]['collection'] : ''; + } +} diff --git a/src/Storage/AdapterPluginManager.php b/src/Storage/AdapterPluginManager.php index bb42efe2e..98b73f35d 100644 --- a/src/Storage/AdapterPluginManager.php +++ b/src/Storage/AdapterPluginManager.php @@ -3,7 +3,7 @@ * Zend Framework (http://framework.zend.com/) * * @link http://github.com/zendframework/zf2 for the canonical source repository - * @copyright Copyright (c) 2005-2016 Zend Technologies USA Inc. (http://www.zend.com) + * @copyright Copyright (c) 2005-2018 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ @@ -38,6 +38,12 @@ class AdapterPluginManager extends AbstractPluginManager 'dba' => Adapter\Dba::class, 'Dba' => Adapter\Dba::class, 'DBA' => Adapter\Dba::class, + 'ext_mongo_db' => Adapter\ExtMongoDb::class, + 'extmongodb' => Adapter\ExtMongoDb::class, + 'ExtMongoDb' => Adapter\ExtMongoDb::class, + 'ExtMongoDB' => Adapter\ExtMongoDb::class, + 'extMongoDb' => Adapter\ExtMongoDb::class, + 'extMongoDB' => Adapter\ExtMongoDb::class, 'filesystem' => Adapter\Filesystem::class, 'Filesystem' => Adapter\Filesystem::class, 'memcache' => Adapter\Memcache::class, @@ -81,6 +87,7 @@ class AdapterPluginManager extends AbstractPluginManager Adapter\Apcu::class => InvokableFactory::class, Adapter\BlackHole::class => InvokableFactory::class, Adapter\Dba::class => InvokableFactory::class, + Adapter\ExtMongoDb::class => InvokableFactory::class, Adapter\Filesystem::class => InvokableFactory::class, Adapter\Memcache::class => InvokableFactory::class, Adapter\Memcached::class => InvokableFactory::class, @@ -98,6 +105,7 @@ class AdapterPluginManager extends AbstractPluginManager 'zendcachestorageadapterapcu' => InvokableFactory::class, 'zendcachestorageadapterblackhole' => InvokableFactory::class, 'zendcachestorageadapterdba' => InvokableFactory::class, + 'zendcachestorageadapterextmongodb' => InvokableFactory::class, 'zendcachestorageadapterfilesystem' => InvokableFactory::class, 'zendcachestorageadaptermemcache' => InvokableFactory::class, 'zendcachestorageadaptermemcached' => InvokableFactory::class, diff --git a/test/Storage/Adapter/ExtMongoDbOptionsTest.php b/test/Storage/Adapter/ExtMongoDbOptionsTest.php new file mode 100644 index 000000000..89cde8dba --- /dev/null +++ b/test/Storage/Adapter/ExtMongoDbOptionsTest.php @@ -0,0 +1,117 @@ +<?php +/** + * @see https://github.com/zendframework/zend-cache for the canonical source repository + * @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com) + * @license https://github.com/zendframework/zend-cache/blob/master/LICENSE.md New BSD License + */ + +namespace ZendTest\Cache\Storage\Adapter; + +use MongoDB\Client; +use PHPUnit\Framework\TestCase; +use Zend\Cache\Storage\Adapter\ExtMongoDbOptions; +use Zend\Cache\Storage\Adapter\ExtMongoDbResourceManager; + +/** + * @covers Zend\Cache\Storage\Adapter\ExtMongoDbOptions<extended> + */ +class ExtMongoDbOptionsTest extends TestCase +{ + protected $object; + + public function setUp() + { + if (getenv('TESTS_ZEND_CACHE_EXTMONGODB_ENABLED') != 'true') { + $this->markTestSkipped('Enable TESTS_ZEND_CACHE_EXTMONGODB_ENABLED to run this test'); + } + + if (! extension_loaded('mongodb') || ! class_exists(Client::class)) { + $this->markTestSkipped("mongodb extension is not loaded"); + } + + $this->object = new ExtMongoDbOptions(); + } + + public function testSetNamespaceSeparator() + { + $this->assertAttributeEquals(':', 'namespaceSeparator', $this->object); + + $namespaceSeparator = '_'; + + $this->object->setNamespaceSeparator($namespaceSeparator); + + $this->assertAttributeEquals($namespaceSeparator, 'namespaceSeparator', $this->object); + } + + public function testGetNamespaceSeparator() + { + $this->assertEquals(':', $this->object->getNamespaceSeparator()); + + $namespaceSeparator = '_'; + + $this->object->setNamespaceSeparator($namespaceSeparator); + + $this->assertEquals($namespaceSeparator, $this->object->getNamespaceSeparator()); + } + + public function testSetResourceManager() + { + $this->assertAttributeEquals(null, 'resourceManager', $this->object); + + $resourceManager = new ExtMongoDbResourceManager(); + + $this->object->setResourceManager($resourceManager); + + $this->assertAttributeSame($resourceManager, 'resourceManager', $this->object); + } + + public function testGetResourceManager() + { + $this->assertInstanceOf( + ExtMongoDbResourceManager::class, + $this->object->getResourceManager() + ); + + $resourceManager = new ExtMongoDbResourceManager(); + + $this->object->setResourceManager($resourceManager); + + $this->assertSame($resourceManager, $this->object->getResourceManager()); + } + + public function testSetResourceId() + { + $this->assertAttributeEquals('default', 'resourceId', $this->object); + + $resourceId = 'foo'; + + $this->object->setResourceId($resourceId); + + $this->assertAttributeEquals($resourceId, 'resourceId', $this->object); + } + + public function testGetResourceId() + { + $this->assertEquals('default', $this->object->getResourceId()); + + $resourceId = 'foo'; + + $this->object->setResourceId($resourceId); + + $this->assertEquals($resourceId, $this->object->getResourceId()); + } + + public function testSetServer() + { + $resourceManager = new ExtMongoDbResourceManager(); + $this->object->setResourceManager($resourceManager); + + $resourceId = $this->object->getResourceId(); + $server = 'mongodb://test:1234'; + + $this->assertFalse($this->object->getResourceManager()->hasResource($resourceId)); + + $this->object->setServer($server); + $this->assertSame($server, $this->object->getResourceManager()->getServer($resourceId)); + } +} diff --git a/test/Storage/Adapter/ExtMongoDbResourceManagerTest.php b/test/Storage/Adapter/ExtMongoDbResourceManagerTest.php new file mode 100644 index 000000000..ce02e50a4 --- /dev/null +++ b/test/Storage/Adapter/ExtMongoDbResourceManagerTest.php @@ -0,0 +1,197 @@ +<?php +/** + * @see https://github.com/zendframework/zend-cache for the canonical source repository + * @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com) + * @license https://github.com/zendframework/zend-cache/blob/master/LICENSE.md New BSD License + */ + +namespace ZendTest\Cache\Storage\Adapter; + +use MongoDB\Client; +use MongoDB\Collection; +use PHPUnit\Framework\TestCase; +use stdClass; +use Zend\Cache\Exception; +use Zend\Cache\Storage\Adapter\ExtMongoDbOptions; +use Zend\Cache\Storage\Adapter\ExtMongoDbResourceManager; + +/** + * @covers Zend\Cache\Storage\Adapter\ExtMongoDbResourceManager + */ +class ExtMongoDbResourceManagerTest extends TestCase +{ + /** + * @var ExtMongoDbResourceManager + */ + protected $object; + + public function setUp() + { + if (getenv('TESTS_ZEND_CACHE_EXTMONGODB_ENABLED') != 'true') { + $this->markTestSkipped('Enable TESTS_ZEND_CACHE_EXTMONGODB_ENABLED to run this test'); + } + + if (! extension_loaded('mongodb') || ! class_exists(Client::class)) { + $this->markTestSkipped("mongodb extension is not loaded"); + } + + $this->object = new ExtMongoDbResourceManager(); + } + + public function testSetResourceAlreadyCreated() + { + $this->assertAttributeEmpty('resources', $this->object); + + $id = 'foo'; + + $client = new Client(getenv('TESTS_ZEND_CACHE_EXTMONGODB_CONNECTSTRING')); + $resource = $client->selectCollection( + getenv('TESTS_ZEND_CACHE_EXTMONGODB_DATABASE'), + getenv('TESTS_ZEND_CACHE_EXTMONGODB_COLLECTION') + ); + + $this->object->setResource($id, $resource); + + $this->assertSame($resource, $this->object->getResource($id)); + } + + public function testSetResourceArray() + { + $this->assertAttributeEmpty('resources', $this->object); + + $id = 'foo'; + $server = 'mongodb://test:1234'; + + $this->object->setResource($id, ['server' => $server]); + + $this->assertSame($server, $this->object->getServer($id)); + } + + public function testSetResourceThrowsException() + { + $id = 'foo'; + $resource = new stdClass(); + + $this->expectException(Exception\InvalidArgumentException::class); + $this->object->setResource($id, $resource); + } + + public function testHasResourceEmpty() + { + $id = 'foo'; + + $this->assertFalse($this->object->hasResource($id)); + } + + public function testHasResourceSet() + { + $id = 'foo'; + + $this->object->setResource($id, ['foo' => 'bar']); + + $this->assertTrue($this->object->hasResource($id)); + } + + public function testGetResourceNotSet() + { + $id = 'foo'; + + $this->assertFalse($this->object->hasResource($id)); + + $this->expectException(Exception\RuntimeException::class); + $this->object->getResource($id); + } + + public function testGetResourceInitialized() + { + $id = 'foo'; + + $client = new Client(getenv('TESTS_ZEND_CACHE_EXTMONGODB_CONNECTSTRING')); + $resource = $client->selectCollection( + getenv('TESTS_ZEND_CACHE_EXTMONGODB_DATABASE'), + getenv('TESTS_ZEND_CACHE_EXTMONGODB_COLLECTION') + ); + + $this->object->setResource($id, $resource); + + $this->assertSame($resource, $this->object->getResource($id)); + } + + public function testGetResourceNewResource() + { + $id = 'foo'; + $server = getenv('TESTS_ZEND_CACHE_EXTMONGODB_CONNECTSTRING'); + $connectionOptions = ['connectTimeoutMS' => 5]; + $database = getenv('TESTS_ZEND_CACHE_EXTMONGODB_DATABASE'); + $collection = getenv('TESTS_ZEND_CACHE_EXTMONGODB_COLLECTION'); + + $this->object->setServer($id, $server); + $this->object->setConnectionOptions($id, $connectionOptions); + $this->object->setDatabase($id, $database); + $this->object->setCollection($id, $collection); + + $this->assertInstanceOf(Collection::class, $this->object->getResource($id)); + } + + public function testGetResourceUnknownServerThrowsException() + { + $id = 'foo'; + $server = 'mongodb://unknown.unknown'; + $connectionOptions = ['connectTimeoutMS' => 5]; + $database = getenv('TESTS_ZEND_CACHE_EXTMONGODB_DATABASE'); + $collection = getenv('TESTS_ZEND_CACHE_EXTMONGODB_COLLECTION'); + + $this->object->setServer($id, $server); + $this->object->setConnectionOptions($id, $connectionOptions); + $this->object->setDatabase($id, $database); + $this->object->setCollection($id, $collection); + + $this->expectException(Exception\RuntimeException::class); + $this->object->getResource($id); + } + + public function testGetSetCollection() + { + $resourceId = 'testResource'; + $collectionName = 'testCollection'; + + $this->object->setCollection($resourceId, $collectionName); + $this->assertSame($collectionName, $this->object->getCollection($resourceId)); + } + + public function testGetSetConnectionOptions() + { + $resourceId = 'testResource'; + $connectionOptions = ['test1' => 'option1', 'test2' => 'option2']; + + $this->object->setConnectionOptions($resourceId, $connectionOptions); + $this->assertSame($connectionOptions, $this->object->getConnectionOptions($resourceId)); + } + + public function testGetSetServer() + { + $resourceId = 'testResource'; + $server = 'testServer'; + + $this->object->setServer($resourceId, $server); + $this->assertSame($server, $this->object->getServer($resourceId)); + } + + public function testGetSetDriverOptions() + { + $resourceId = 'testResource'; + $driverOptions = ['test1' => 'option1', 'test2' => 'option2']; + + $this->object->setDriverOptions($resourceId, $driverOptions); + $this->assertSame($driverOptions, $this->object->getDriverOptions($resourceId)); + } + + public function testGetSetDatabase() + { + $resourceId = 'testResource'; + $database = 'testDatabase'; + + $this->object->setDatabase($resourceId, $database); + $this->assertSame($database, $this->object->getDatabase($resourceId)); + } +} diff --git a/test/Storage/Adapter/ExtMongoDbTest.php b/test/Storage/Adapter/ExtMongoDbTest.php new file mode 100644 index 000000000..39dd111ff --- /dev/null +++ b/test/Storage/Adapter/ExtMongoDbTest.php @@ -0,0 +1,73 @@ +<?php +/** + * @see https://github.com/zendframework/zend-cache for the canonical source repository + * @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com) + * @license https://github.com/zendframework/zend-cache/blob/master/LICENSE.md New BSD License + */ + +namespace ZendTest\Cache\Storage\Adapter; + +use MongoDB\Client; +use Zend\Cache\Storage\Adapter\ExtMongoDb; +use Zend\Cache\Storage\Adapter\ExtMongoDbOptions; + +/** + * @covers Zend\Cache\Storage\Adapter\ExtMongoDb<extended> + */ +class ExtMongoDbTest extends CommonAdapterTest +{ + public function setUp() + { + if (getenv('TESTS_ZEND_CACHE_EXTMONGODB_ENABLED') != 'true') { + $this->markTestSkipped('Enable TESTS_ZEND_CACHE_MONGODB_ENABLED to run this test'); + } + + if (! extension_loaded('mongodb') || ! class_exists(Client::class)) { + $this->markTestSkipped("mongodb extension is not loaded"); + } + + $this->_options = new ExtMongoDbOptions([ + 'server' => getenv('TESTS_ZEND_CACHE_EXTMONGODB_CONNECTSTRING'), + 'database' => getenv('TESTS_ZEND_CACHE_EXTMONGODB_DATABASE'), + 'collection' => getenv('TESTS_ZEND_CACHE_EXTMONGODB_COLLECTION'), + ]); + + $this->_storage = new ExtMongoDb(); + $this->_storage->setOptions($this->_options); + $this->_storage->flush(); + + parent::setUp(); + } + + public function tearDown() + { + if ($this->_storage) { + $this->_storage->flush(); + } + + parent::tearDown(); + } + + public function getCommonAdapterNamesProvider() + { + return [ + ['ext_mongo_db'], + ['extmongodb'], + ['extMongoDb'], + ['extMongoDB'], + ['ExtMongoDb'], + ['ExtMongoDB'], + ]; + } + + public function testSetOptionsNotMongoDbOptions() + { + $this->_storage->setOptions([ + 'server' => getenv('TESTS_ZEND_CACHE_EXTMONGODB_CONNECTSTRING'), + 'database' => getenv('TESTS_ZEND_CACHE_EXTMONGODB_DATABASE'), + 'collection' => getenv('TESTS_ZEND_CACHE_EXTMONGODB_COLLECTION'), + ]); + + $this->assertInstanceOf(ExtMongoDbOptions::class, $this->_storage->getOptions()); + } +} From 1c29cf213d79fdfe49eafc683eb04e41f7cb2cc1 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney <matthew@zend.com> Date: Wed, 18 Apr 2018 15:56:17 -0500 Subject: [PATCH 2/2] Adds documentation for ext-mongodb adapter --- docs/book/storage/adapter.md | 59 ++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/docs/book/storage/adapter.md b/docs/book/storage/adapter.md index 48ab64fe0..c2834eb89 100644 --- a/docs/book/storage/adapter.md +++ b/docs/book/storage/adapter.md @@ -795,6 +795,11 @@ Name | Data Type | Default Value | Description PHP extension [mongo](http://php.net/mongo), or a MongoDB polyfill library, such as [Mongofill](https://github.com/mongofill/mongofill). +> #### ext-mongodb +> +> If you are using the mongodb extension (vs the mongo extension), you will need +> to use the [ExtMongoDb adapter](#the-extmongodb-adapter) instead. + This adapter implements the following interfaces: - `Zend\Cache\Storage\FlushableInterface` @@ -832,6 +837,60 @@ Key | Default | Description `connectionOptions` | `['fsync' => false, 'journal' => true]` | Associative array of options to pass to `MongoClient` (see the [MongoClient docs](http://php.net/MongoClient)). `driverOptions` | `[]` | Associative array of driver options to pass to `MongoClient` (see the [MongoClient docs](http://php.net/MongoClient)). +## The ExtMongoDB Adapter + +- Since 2.8.0 + +`Zend\Cache\Storage\Adapter\ExtMongoDB` stores cache items using the mongodb extension, and +requires that the MongoDB PHP Client library is also installed. You can install the client +library using the following: + +```bash +$ composer require mongodb/mongodb +``` + +> #### ext-mongo +> +> If you are using the mongo extension (vs the mongodb extension), you will need +> to use the [MongoDb adapter](#the-mongodb-adapter) instead. + +This adapter implements the following interfaces: + +- `Zend\Cache\Storage\FlushableInterface` + +### Capabilities + +Capability | Value +---------- | ----- +`supportedDatatypes` | `string`, `null`, `boolean`, `integer`, `double`, `array` +`supportedMetadata` | _id +`minTtl` | 0 +`maxTtl` | 0 +`staticTtl` | `true` +`ttlPrecision` | 1 +`useRequestTime` | `false` +`lockOnExpire` | 0 +`maxKeyLength` | 255 +`namespaceIsPrefix` | `true` +`namespaceSeparator` | <Option value of namespace_separator> + +### Adapter specific options + +Name | Data Type | Default Value | Description +---- | --------- | ------------- | ----------- +`lib_option` | `array` | | Associative array of options where the array key is the option name. +`namespace_separator` | `string` | ":" | A separator for the namespace and prefix. + +Available keys for `lib_option` include: + +Key | Default | Description +--- | ------- | ----------- +`server` | `mongodb://localhost:27017` | The MongoDB server connection string (see the [MongoDB\\Client docs](https://docs.mongodb.com/php-library/current/reference/method/MongoDBClient__construct/)). +`database` | `zend` | Name of the database to use; MongoDB will create this database if it does not exist. +`collection` | `cache` | Name of the collection to use; MongoDB will create this collection if it does not exist. +`connectionOptions` | `['fsync' => false, 'journal' => true]` | Associative array of URI options (such as authentication credentials or query string parameters) to pass to `MongoDB\\Client` (see the [MongoDB\\Client docs](https://docs.mongodb.com/php-library/current/reference/method/MongoDBClient__construct/)). +`driverOptions` | `[]` | Associative array of driver options to pass to `MongoDB\\Client` (see the [MongoDB\\Client docs](https://docs.mongodb.com/php-library/current/reference/method/MongoDBClient__construct/)). + ## The WinCache Adapter `Zend\Cache\Storage\Adapter\WinCache` stores cache items into shared memory