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