diff --git a/.travis.yml b/.travis.yml index fa90aaa9..119c655a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,9 @@ cache: directories: - $HOME/.composer/cache +services: + - mongodb + matrix: fast_finish: true include: @@ -33,8 +36,8 @@ matrix: env: - EVENT_MANAGER_VERSION="^2.6.2" - SERVICE_MANAGER_VERSION="^2.7.5" - - php: hhvm - - php: hhvm + - php: hhvm + - php: hhvm env: - EVENT_MANAGER_VERSION="^2.6.2" - SERVICE_MANAGER_VERSION="^2.7.5" @@ -46,6 +49,7 @@ notifications: email: false before_install: + - pecl -q install mongodb - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi - composer self-update - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then composer require --dev --no-update satooshi/php-coveralls ; fi diff --git a/composer.json b/composer.json index b67f2cce..fdce1af5 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", "zendframework/zend-validator": "^2.6", "container-interop/container-interop": "^1.1", + "mongodb/mongodb": "^1.0.1", "fabpot/php-cs-fixer": "1.7.*", "phpunit/PHPUnit": "~4.0" }, @@ -32,7 +33,8 @@ "zendframework/zend-db": "Zend\\Db component", "zendframework/zend-http": "Zend\\Http component", "zendframework/zend-servicemanager": "Zend\\ServiceManager component", - "zendframework/zend-validator": "Zend\\Validator component" + "zendframework/zend-validator": "Zend\\Validator component", + "mongodb/mongodb": "If you want to use the MongoDB session save handler" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/doc/book/zend.session.save-handler.md b/doc/book/zend.session.save-handler.md index 0a123965..ef63fc92 100644 --- a/doc/book/zend.session.save-handler.md +++ b/doc/book/zend.session.save-handler.md @@ -79,24 +79,25 @@ orphan `Zend\Session\SaveHandler\MongoDB` allows you to provide a MongoDB instance to be utilized as a session save handler. You provide the options in the `Zend\Session\SaveHandler\MongoDBOptions` -class. +class. You must install the [mongodb PHP extensions](http://php.net/manual/en/set.mongodb.php) and the +[MongoDB PHP library](https://github.com/mongodb/mongo-php-library). ### Basic Usage A basic example is one like the following: ```php -use Mongo; +use MongoDB\Client; use Zend\Session\SaveHandler\MongoDB; use Zend\Session\SaveHandler\MongoDBOptions; use Zend\Session\SessionManager; -$mongo = new Mongo(); +$mongoClient = new Client(); $options = new MongoDBOptions(array( 'database' => 'myapp', 'collection' => 'sessions', )); -$saveHandler = new MongoDB($mongo, $options); +$saveHandler = new MongoDB($mongoClient, $options); $manager = new SessionManager(); $manager->setSaveHandler($saveHandler); ``` diff --git a/src/SaveHandler/MongoDB.php b/src/SaveHandler/MongoDB.php index d43a813c..8c6d63f5 100644 --- a/src/SaveHandler/MongoDB.php +++ b/src/SaveHandler/MongoDB.php @@ -10,10 +10,10 @@ namespace Zend\Session\SaveHandler; -use Mongo; -use MongoClient; -use MongoCollection; -use MongoDate; +use MongoDB\BSON\Binary; +use MongoDB\BSON\UTCDatetime; +use MongoDB\Client as MongoClient; +use MongoDB\Collection as MongoCollection; use Zend\Session\Exception\InvalidArgumentException; /** @@ -21,6 +21,13 @@ */ class MongoDB implements SaveHandlerInterface { + /** + * MongoClient instance + * + * @var MongoClient + */ + protected $mongoClient; + /** * MongoCollection instance * @@ -51,21 +58,12 @@ class MongoDB implements SaveHandlerInterface /** * Constructor * - * @param Mongo|MongoClient $mongo + * @param MongoClient $mongoClient * @param MongoDBOptions $options * @throws InvalidArgumentException */ - public function __construct($mongo, MongoDBOptions $options) + public function __construct($mongoClient, MongoDBOptions $options) { - if (!($mongo instanceof MongoClient || $mongo instanceof Mongo)) { - throw new InvalidArgumentException( - sprintf( - 'Parameter of type %s is invalid; must be MongoClient or Mongo', - (is_object($mongo) ? get_class($mongo) : gettype($mongo)) - ) - ); - } - if (null === ($database = $options->getDatabase())) { throw new InvalidArgumentException('The database option cannot be empty'); } @@ -74,7 +72,7 @@ public function __construct($mongo, MongoDBOptions $options) throw new InvalidArgumentException('The collection option cannot be empty'); } - $this->mongoCollection = $mongo->selectCollection($database, $collection); + $this->mongoClient = $mongoClient; $this->options = $options; } @@ -91,6 +89,16 @@ public function open($savePath, $name) $this->sessionName = $name; $this->lifetime = ini_get('session.gc_maxlifetime'); + $this->mongoCollection = $this->mongoClient->selectCollection( + $this->options->getDatabase(), + $this->options->getCollection() + ); + + $this->mongoCollection->createIndex( + [$this->options->getModifiedField() => 1], + $this->options->useExpireAfterSecondsIndex() ? ['expireAfterSeconds' => $this->lifetime] : [] + ); + return true; } @@ -118,12 +126,18 @@ public function read($id) ]); if (null !== $session) { - if ($session[$this->options->getModifiedField()] instanceof MongoDate && - $session[$this->options->getModifiedField()]->sec + - $session[$this->options->getLifetimeField()] > time()) { - return $session[$this->options->getDataField()]; + // check if session has expired if index is not used + if (!$this->options->useExpireAfterSecondsIndex()) { + $timestamp = $session[$this->options->getLifetimeField()]; + $timestamp += floor(((string)$session[$this->options->getModifiedField()]) / 1000); + + // session expired + if ($timestamp <= time()) { + $this->destroy($id); + return ''; + } } - $this->destroy($id); + return $session[$this->options->getDataField()]->getData(); } return ''; @@ -148,11 +162,13 @@ public function write($id, $data) $this->options->getNameField() => $this->sessionName, ]; - $newObj = ['$set' => [ - $this->options->getDataField() => (string) $data, - $this->options->getLifetimeField() => $this->lifetime, - $this->options->getModifiedField() => new MongoDate(), - ]]; + $newObj = [ + '$set' => [ + $this->options->getDataField() => new Binary((string)$data, Binary::TYPE_GENERIC), + $this->options->getLifetimeField() => $this->lifetime, + $this->options->getModifiedField() => new UTCDatetime(floor(microtime(true) * 1000)), + ], + ]; /* Note: a MongoCursorException will be thrown if a record with this ID * already exists with a different session name, since the upsert query @@ -160,9 +176,9 @@ public function write($id, $data) * This should only happen if ID's are not unique or if the session name * is altered mid-process. */ - $result = $this->mongoCollection->update($criteria, $newObj, $saveOptions); + $result = $this->mongoCollection->updateOne($criteria, $newObj, $saveOptions); - return (bool) (isset($result['ok']) ? $result['ok'] : $result); + return $result->isAcknowledged(); } /** @@ -173,12 +189,15 @@ public function write($id, $data) */ public function destroy($id) { - $result = $this->mongoCollection->remove([ - '_id' => $id, - $this->options->getNameField() => $this->sessionName, - ], $this->options->getSaveOptions()); + $result = $this->mongoCollection->deleteOne( + [ + '_id' => $id, + $this->options->getNameField() => $this->sessionName, + ], + $this->options->getSaveOptions() + ); - return (bool) (isset($result['ok']) ? $result['ok'] : $result); + return $result->isAcknowledged(); } /** @@ -200,10 +219,15 @@ public function gc($maxlifetime) * each document. Doing so would require a $where query to work with the * computed value (modified + lifetime) and be very inefficient. */ - $result = $this->mongoCollection->remove([ - $this->options->getModifiedField() => ['$lt' => new MongoDate(time() - $maxlifetime)], - ], $this->options->getSaveOptions()); + $microseconds = floor(microtime(true) * 1000) - $maxlifetime; + + $result = $this->mongoCollection->deleteMany( + [ + $this->options->getModifiedField() => ['$lt' => new UTCDateTime($microseconds)], + ], + $this->options->getSaveOptions() + ); - return (bool) (isset($result['ok']) ? $result['ok'] : $result); + return $result->isAcknowledged(); } } diff --git a/src/SaveHandler/MongoDBOptions.php b/src/SaveHandler/MongoDBOptions.php index c315716e..d8f94f62 100644 --- a/src/SaveHandler/MongoDBOptions.php +++ b/src/SaveHandler/MongoDBOptions.php @@ -68,6 +68,12 @@ class MongoDBOptions extends AbstractOptions */ protected $modifiedField = 'modified'; + /** + * Use expireAfterSeconds index + * + * @var bool + */ + protected $useExpireAfterSecondsIndex = false; /** * {@inheritdoc} @@ -286,4 +292,23 @@ public function getModifiedField() { return $this->modifiedField; } + + /** + * @return boolean + */ + public function useExpireAfterSecondsIndex() + { + return $this->useExpireAfterSecondsIndex; + } + + /** + * Enable expireAfterSeconds index. + * + * @see http://docs.mongodb.org/manual/tutorial/expire-data/ + * @param boolean $useExpireAfterSecondsIndex + */ + public function setUseExpireAfterSecondsIndex($useExpireAfterSecondsIndex) + { + $this->useExpireAfterSecondsIndex = (bool) $useExpireAfterSecondsIndex; + } } diff --git a/test/SaveHandler/MongoDBTest.php b/test/SaveHandler/MongoDBTest.php index aabb86cf..e37cd921 100644 --- a/test/SaveHandler/MongoDBTest.php +++ b/test/SaveHandler/MongoDBTest.php @@ -9,20 +9,22 @@ namespace ZendTest\Session\SaveHandler; -use Mongo; +use MongoDB\Client as MongoClient; +use MongoDB\Collection as MongoCollection; use Zend\Session\SaveHandler\MongoDB; use Zend\Session\SaveHandler\MongoDBOptions; /** * @group Zend_Session * @covers Zend\Session\SaveHandler\MongoDb + * @requires extension mongodb */ class MongoDBTest extends \PHPUnit_Framework_TestCase { /** - * @var Mongo|MongoClient + * @var MongoClient */ - protected $mongo; + protected $mongoClient; /** * MongoCollection instance @@ -43,19 +45,16 @@ class MongoDBTest extends \PHPUnit_Framework_TestCase */ public function setUp() { - if (!extension_loaded('mongo')) { - $this->markTestSkipped('Zend\Session\SaveHandler\MongoDB tests are not enabled due to missing Mongo extension'); - } - $this->options = new MongoDBOptions([ 'database' => 'zf2_tests', 'collection' => 'sessions', ]); - $mongoClass = (version_compare(phpversion('mongo'), '1.3.0', '<')) ? '\Mongo' : '\MongoClient'; - - $this->mongo = new $mongoClass(); - $this->mongoCollection = $this->mongo->selectCollection($this->options->getDatabase(), $this->options->getCollection()); + $this->mongoClient = new MongoClient(); + $this->mongoCollection = $this->mongoClient->selectCollection( + $this->options->getDatabase(), + $this->options->getCollection() + ); } /** @@ -70,21 +69,9 @@ public function tearDown() } } - public function testConstructorThrowsException() - { - $notMongo = new \stdClass(); - - $this->setExpectedException( - 'InvalidArgumentException', - 'Parameter of type stdClass is invalid; must be MongoClient or Mongo' - ); - - $saveHandler = new MongoDB($notMongo, $this->options); - } - public function testReadWrite() { - $saveHandler = new MongoDB($this->mongo, $this->options); + $saveHandler = new MongoDB($this->mongoClient, $this->options); $this->assertTrue($saveHandler->open('savepath', 'sessionname')); $id = '242'; @@ -108,7 +95,7 @@ public function testReadDestroysExpiredSession() $oldMaxlifetime = ini_get('session.gc_maxlifetime'); ini_set('session.gc_maxlifetime', 0); - $saveHandler = new MongoDB($this->mongo, $this->options); + $saveHandler = new MongoDB($this->mongoClient, $this->options); $this->assertTrue($saveHandler->open('savepath', 'sessionname')); $id = '242'; @@ -126,7 +113,7 @@ public function testReadDestroysExpiredSession() public function testGarbageCollection() { - $saveHandler = new MongoDB($this->mongo, $this->options); + $saveHandler = new MongoDB($this->mongoClient, $this->options); $this->assertTrue($saveHandler->open('savepath', 'sessionname')); $data = ['foo' => 'bar']; @@ -146,11 +133,11 @@ public function testGarbageCollection() } /** - * @expectedException MongoCursorException + * @expectedException \MongoDB\Driver\Exception\RuntimeException */ public function testWriteExceptionEdgeCaseForChangedSessionName() { - $saveHandler = new MongoDB($this->mongo, $this->options); + $saveHandler = new MongoDB($this->mongoClient, $this->options); $this->assertTrue($saveHandler->open('savepath', 'sessionname')); $id = '242';