Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Added MongoDB session save handler

  • Loading branch information...
commit 5329b36a79a7a8e110b7ca5e80569b669f8f372c 1 parent 2a926c8
@jmikola jmikola authored
View
201 library/Zend/Session/SaveHandler/MongoDB.php
@@ -0,0 +1,201 @@
+<?php
+
+/**
+ * Zend Framework (http://framework.zend.com/)
+ *
+ * @link http://github.com/zendframework/zf2 for the canonical source repository
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ * @package Zend_Session
+ */
+
+namespace Zend\Session\SaveHandler;
+
+use Mongo;
+use MongoDate;
+use Zend\Session\Exception\InvalidArgumentException;
+
+/**
+ * MongoDB session save handler
+ *
+ * @category Zend
+ * @package Zend_Session
+ * @subpackage SaveHandler
+ */
+class MongoDB implements SaveHandlerInterface
+{
+ /**
+ * MongoCollection instance
+ *
+ * @var MongoCollection
+ */
+ protected $mongoCollection;
+
+ /**
+ * Session name
+ *
+ * @var string
+ */
+ protected $sessionName;
+
+ /**
+ * Session lifetime
+ *
+ * @var int
+ */
+ protected $lifetime;
+
+ /**
+ * MongoDB session save handler options
+ * @var MongoDBOptions
+ */
+ protected $options;
+
+ /**
+ * Constructor
+ *
+ * @param Mongo $mongo
+ * @param MongoDBOptions $options
+ * @throws Zend\Session\Exception\InvalidArgumentException
+ */
+ public function __construct(Mongo $mongo, MongoDBOptions $options)
+ {
+ if (null === ($database = $options->getDatabase())) {
+ throw new InvalidArgumentException('The database option cannot be emtpy');
+ }
+
+ if (null === ($collection = $options->getCollection())) {
+ throw new InvalidArgumentException('The collection option cannot be emtpy');
+ }
+
+ $this->mongoCollection = $mongo->selectCollection($database, $collection);
+ $this->options = $options;
+ }
+
+ /**
+ * Open session
+ *
+ * @param string $savePath
+ * @param string $name
+ * @return boolean
+ */
+ public function open($savePath, $name)
+ {
+ // Note: session save path is not used
+ $this->sessionName = $name;
+ $this->lifetime = ini_get('session.gc_maxlifetime');
+
+ return true;
+ }
+
+ /**
+ * Close session
+ *
+ * @return boolean
+ */
+ public function close()
+ {
+ return true;
+ }
+
+ /**
+ * Read session data
+ *
+ * @param string $id
+ * @return string
+ */
+ public function read($id)
+ {
+ $session = $this->mongoCollection->findOne(array(
+ '_id' => $id,
+ $this->options->getNameField() => $this->sessionName,
+ ));
+
+ 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()];
+ }
+ $this->destroy($id);
+ }
+
+ return '';
+ }
+
+ /**
+ * Write session data
+ *
+ * @param string $id
+ * @param string $data
+ * @return boolean
+ */
+ public function write($id, $data)
+ {
+ $saveOptions = array_replace(
+ $this->options->getSaveOptions(),
+ array('upsert' => true, 'multiple' => false)
+ );
+
+ $criteria = array(
+ '_id' => $id,
+ $this->options->getNameField() => $this->sessionName,
+ );
+
+ $newObj = array('$set' => array(
+ $this->options->getDataField() => (string) $data,
+ $this->options->getLifetimeField() => $this->lifetime,
+ $this->options->getModifiedField() => new MongoDate(),
+ ));
+
+ /* Note: a MongoCursorException will be thrown if a record with this ID
+ * already exists with a different session name, since the upsert query
+ * cannot insert a new document with the same ID and new session name.
+ * 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);
+
+ return (bool) (isset($result['ok']) ? $result['ok'] : $result);
+ }
+
+ /**
+ * Destroy session
+ *
+ * @param string $id
+ * @return boolean
+ */
+ public function destroy($id)
+ {
+ $result = $this->mongoCollection->remove(array(
+ '_id' => $id,
+ $this->options->getNameField() => $this->sessionName,
+ ), $this->options->getSaveOptions());
+
+ return (bool) (isset($result['ok']) ? $result['ok'] : $result);
+ }
+
+ /**
+ * Garbage collection
+ *
+ * Note: MongoDB 2.2+ supports TTL collections, which may be used in place
+ * of this method by indexing with "modified" field with an
+ * "expireAfterSeconds" option.
+ *
+ * @see http://docs.mongodb.org/manual/tutorial/expire-data/
+ * @param int $maxlifetime
+ * @return boolean
+ */
+ public function gc($maxlifetime)
+ {
+ /* Note: unlike DbTableGateway, we do not use the lifetime field in
+ * 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(array(
+ $this->options->getModifiedField() => array('$lt' => new MongoDate(time() - $maxlifetime)),
+ ), $this->options->getSaveOptions());
+
+ return (bool) (isset($result['ok']) ? $result['ok'] : $result);
+ }
+}
View
260 library/Zend/Session/SaveHandler/MongoDBOptions.php
@@ -0,0 +1,260 @@
+<?php
+
+/**
+ * Zend Framework (http://framework.zend.com/)
+ *
+ * @link http://github.com/zendframework/zf2 for the canonical source repository
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ * @package Zend_Session
+ */
+
+namespace Zend\Session\SaveHandler;
+
+use Zend\Session\Exception\InvalidArgumentException;
+use Zend\Stdlib\AbstractOptions;
+
+/**
+ * MongoDB session save handler Options
+ *
+ * @category Zend
+ * @package Zend_Session
+ * @subpackage SaveHandler
+ */
+class MongoDBOptions extends AbstractOptions
+{
+ /**
+ * Database name
+ *
+ * @var string
+ */
+ protected $database;
+
+ /**
+ * Collection name
+ *
+ * @var string
+ */
+ protected $collection;
+
+ /**
+ * Save options
+ *
+ * @see http://php.net/manual/en/mongocollection.save.php
+ * @var string
+ */
+ protected $saveOptions = array('safe' => true);
+
+ /**
+ * Name field
+ *
+ * @var string
+ */
+ protected $nameField = 'name';
+
+ /**
+ * Data field
+ *
+ * @var string
+ */
+ protected $dataField = 'data';
+
+ /**
+ * Lifetime field
+ *
+ * @var string
+ */
+ protected $lifetimeField = 'lifetime';
+
+ /**
+ * Modified field
+ *
+ * @var string
+ */
+ protected $modifiedField = 'modified';
+
+ /**
+ * Set database name
+ *
+ * @param string $database
+ * @return MongoDBOptions
+ * @throws Zend\Session\Exception\InvalidArgumentException
+ */
+ public function setDatabase($database)
+ {
+ $database = (string) $database;
+ if (strlen($database) === 0) {
+ throw new InvalidArgumentException('$database must be a non-empty string');
+ }
+ $this->database = $database;
+ return $this;
+ }
+
+ /**
+ * Get database name
+ *
+ * @return string
+ */
+ public function getDatabase()
+ {
+ return $this->database;
+ }
+
+ /**
+ * Set collection name
+ *
+ * @param string $collection
+ * @return MongoDBOptions
+ * @throws Zend\Session\Exception\InvalidArgumentException
+ */
+ public function setCollection($collection)
+ {
+ $collection = (string) $collection;
+ if (strlen($collection) === 0) {
+ throw new InvalidArgumentException('$collection must be a non-empty string');
+ }
+ $this->collection = $collection;
+ return $this;
+ }
+
+ /**
+ * Get collection name
+ *
+ * @return string
+ */
+ public function getCollection()
+ {
+ return $this->collection;
+ }
+
+ /**
+ * Set save options
+ *
+ * @see http://php.net/manual/en/mongocollection.save.php
+ * @param array $saveOptions
+ * @return MongoDBOptions
+ */
+ public function setSaveOptions(array $saveOptions)
+ {
+ $this->saveOptions = $saveOptions;
+ return $this;
+ }
+
+ /**
+ * Get save options
+ *
+ * @return string
+ */
+ public function getSaveOptions()
+ {
+ return $this->saveOptions;
+ }
+
+ /**
+ * Set name field
+ *
+ * @param string $nameField
+ * @return MongoDBOptions
+ * @throws Zend\Session\Exception\InvalidArgumentException
+ */
+ public function setNameField($nameField)
+ {
+ $nameField = (string) $nameField;
+ if (strlen($nameField) === 0) {
+ throw new InvalidArgumentException('$nameField must be a non-empty string');
+ }
+ $this->nameField = $nameField;
+ return $this;
+ }
+
+ /**
+ * Get name field
+ *
+ * @return string
+ */
+ public function getNameField()
+ {
+ return $this->nameField;
+ }
+
+ /**
+ * Set data field
+ *
+ * @param string $dataField
+ * @return MongoDBOptions
+ * @throws Zend\Session\Exception\InvalidArgumentException
+ */
+ public function setDataField($dataField)
+ {
+ $dataField = (string) $dataField;
+ if (strlen($dataField) === 0) {
+ throw new InvalidArgumentException('$dataField must be a non-empty string');
+ }
+ $this->dataField = $dataField;
+ return $this;
+ }
+
+ /**
+ * Get data field
+ *
+ * @return string
+ */
+ public function getDataField()
+ {
+ return $this->dataField;
+ }
+
+ /**
+ * Set lifetime field
+ *
+ * @param string $lifetimeField
+ * @return MongoDBOptions
+ * @throws Zend\Session\Exception\InvalidArgumentException
+ */
+ public function setLifetimeField($lifetimeField)
+ {
+ $lifetimeField = (string) $lifetimeField;
+ if (strlen($lifetimeField) === 0) {
+ throw new InvalidArgumentException('$lifetimeField must be a non-empty string');
+ }
+ $this->lifetimeField = $lifetimeField;
+ return $this;
+ }
+
+ /**
+ * Get lifetime Field
+ *
+ * @return string
+ */
+ public function getLifetimeField()
+ {
+ return $this->lifetimeField;
+ }
+
+ /**
+ * Set Modified Field
+ *
+ * @param string $modifiedField
+ * @return MongoDBOptions
+ * @throws Zend\Session\Exception\InvalidArgumentException
+ */
+ public function setModifiedField($modifiedField)
+ {
+ $modifiedField = (string) $modifiedField;
+ if (strlen($modifiedField) === 0) {
+ throw new InvalidArgumentException('$modifiedField must be a non-empty string');
+ }
+ $this->modifiedField = $modifiedField;
+ return $this;
+ }
+
+ /**
+ * Get modified Field
+ *
+ * @return string
+ */
+ public function getModifiedField()
+ {
+ return $this->modifiedField;
+ }
+}
View
146 tests/ZendTest/Session/SaveHandler/MongoDBOptionsTest.php
@@ -0,0 +1,146 @@
+<?php
+
+/**
+ * Zend Framework (http://framework.zend.com/)
+ *
+ * @link http://github.com/zendframework/zf2 for the canonical source repository
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ * @package Zend_Session
+ */
+
+namespace ZendTest\Session\SaveHandler;
+
+use Zend\Session\SaveHandler\MongoDBOptions;
+
+/**
+ * @category Zend
+ * @package Zend_Session
+ * @subpackage UnitTests
+ * @group Zend_Session
+ */
+class MongoDBOptionsTest extends \PHPUnit_Framework_TestCase
+{
+ public function testDefaults()
+ {
+ $options = new MongoDBOptions();
+ $this->assertNull($options->getDatabase());
+ $this->assertNull($options->getCollection());
+ $this->assertEquals(array('safe' => true), $options->getSaveOptions());
+ $this->assertEquals('name', $options->getNameField());
+ $this->assertEquals('data', $options->getDataField());
+ $this->assertEquals('lifetime', $options->getLifetimeField());
+ $this->assertEquals('modified', $options->getModifiedField());
+ }
+
+ public function testSetConstructor()
+ {
+ $options = new MongoDBOptions(array(
+ 'database' => 'testDatabase',
+ 'collection' => 'testCollection',
+ 'saveOptions' => array('safe' => 2),
+ 'nameField' => 'testName',
+ 'dataField' => 'testData',
+ 'lifetimeField' => 'testLifetime',
+ 'modifiedField' => 'testModified',
+ ));
+
+ $this->assertEquals('testDatabase', $options->getDatabase());
+ $this->assertEquals('testCollection', $options->getCollection());
+ $this->assertEquals(array('safe' => 2), $options->getSaveOptions());
+ $this->assertEquals('testName', $options->getNameField());
+ $this->assertEquals('testData', $options->getDataField());
+ $this->assertEquals('testLifetime', $options->getLifetimeField());
+ $this->assertEquals('testModified', $options->getModifiedField());
+ }
+
+ public function testSetters()
+ {
+ $options = new MongoDBOptions();
+ $options->setDatabase('testDatabase')
+ ->setCollection('testCollection')
+ ->setSaveOptions(array('safe' => 2))
+ ->setNameField('testName')
+ ->setDataField('testData')
+ ->setLifetimeField('testLifetime')
+ ->setModifiedField('testModified');
+
+ $this->assertEquals('testDatabase', $options->getDatabase());
+ $this->assertEquals('testCollection', $options->getCollection());
+ $this->assertEquals(array('safe' => 2), $options->getSaveOptions());
+ $this->assertEquals('testName', $options->getNameField());
+ $this->assertEquals('testData', $options->getDataField());
+ $this->assertEquals('testLifetime', $options->getLifetimeField());
+ $this->assertEquals('testModified', $options->getModifiedField());
+ }
+
+ /**
+ * @expectedException Zend\Session\Exception\InvalidArgumentException
+ */
+ public function testInvalidDatabase()
+ {
+ $options = new MongoDBOptions(array(
+ 'database' => null,
+ ));
+ }
+
+ /**
+ * @expectedException Zend\Session\Exception\InvalidArgumentException
+ */
+ public function testInvalidCollection()
+ {
+ $options = new MongoDBOptions(array(
+ 'collection' => null,
+ ));
+ }
+
+ /**
+ * @expectedException PHPUnit_Framework_Error
+ */
+ public function testInvalidSaveOptions()
+ {
+ $options = new MongoDBOptions(array(
+ 'saveOptions' => null,
+ ));
+ }
+
+ /**
+ * @expectedException Zend\Session\Exception\InvalidArgumentException
+ */
+ public function testInvalidNameField()
+ {
+ $options = new MongoDBOptions(array(
+ 'nameField' => null,
+ ));
+ }
+
+ /**
+ * @expectedException Zend\Session\Exception\InvalidArgumentException
+ */
+ public function testInvalidModifiedField()
+ {
+ $options = new MongoDBOptions(array(
+ 'modifiedField' => null,
+ ));
+ }
+
+ /**
+ * @expectedException Zend\Session\Exception\InvalidArgumentException
+ */
+ public function testInvalidLifetimeField()
+ {
+ $options = new MongoDBOptions(array(
+ 'lifetimeField' => null,
+ ));
+ }
+
+ /**
+ * @expectedException Zend\Session\Exception\InvalidArgumentException
+ */
+ public function testInvalidDataField()
+ {
+ $options = new MongoDBOptions(array(
+ 'dataField' => null,
+ ));
+ }
+}
View
158 tests/ZendTest/Session/SaveHandler/MongoDBTest.php
@@ -0,0 +1,158 @@
+<?php
+/**
+ * Zend Framework (http://framework.zend.com/)
+ *
+ * @link http://github.com/zendframework/zf2 for the canonical source repository
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ * @package Zend_Session
+ */
+
+namespace ZendTest\Session\SaveHandler;
+
+use Mongo;
+use Zend\Session\SaveHandler\MongoDB;
+use Zend\Session\SaveHandler\MongoDBOptions;
+
+/**
+ * @category Zend
+ * @package Zend_Session
+ * @subpackage UnitTests
+ * @group Zend_Session
+ */
+class MongoDBTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var Mongo
+ */
+ protected $mongo;
+
+ /**
+ * MongoCollection instance
+ *
+ * @var MongoCollection
+ */
+ protected $mongoCollection;
+
+ /**
+ * @var Zend\Session\SaveHandler\MongoDBOptions
+ */
+ protected $options;
+
+ /**
+ * Setup performed prior to each test method
+ *
+ * @return void
+ */
+ 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(array(
+ 'database' => 'zf2_tests',
+ 'collection' => 'sessions',
+ ));
+
+ $this->mongo = new Mongo();
+ $this->mongoCollection = $this->mongo->selectCollection($this->options->getDatabase(), $this->options->getCollection());
+ }
+
+ /**
+ * Tear-down operations performed after each test method
+ *
+ * @return void
+ */
+ public function tearDown()
+ {
+ if ($this->mongoCollection) {
+ $this->mongoCollection->drop();
+ }
+ }
+
+ public function testReadWrite()
+ {
+ $saveHandler = new MongoDB($this->mongo, $this->options);
+ $this->assertTrue($saveHandler->open('savepath', 'sessionname'));
+
+ $id = '242';
+ $data = array('foo' => 'bar', 'bar' => array('foo' => 'bar'));
+
+ $this->assertTrue($saveHandler->write($id, serialize($data)));
+ $this->assertEquals($data, unserialize($saveHandler->read($id)));
+
+ $data = array('foo' => array(1, 2, 3));
+
+ $this->assertTrue($saveHandler->write($id, serialize($data)));
+ $this->assertEquals($data, unserialize($saveHandler->read($id)));
+ }
+
+ public function testReadDestroysExpiredSession()
+ {
+ /* Note: due to the session save handler's open() method reading the
+ * "session.gc_maxlifetime" INI value directly, it's necessary to set
+ * that to simulate natural session expiration.
+ */
+ $oldMaxlifetime = ini_get('session.gc_maxlifetime');
+ ini_set('session.gc_maxlifetime', 0);
+
+ $saveHandler = new MongoDB($this->mongo, $this->options);
+ $this->assertTrue($saveHandler->open('savepath', 'sessionname'));
+
+ $id = '242';
+ $data = array('foo' => 'bar');
+
+ $this->assertNull($this->mongoCollection->findOne(array('_id' => $id)));
+
+ $this->assertTrue($saveHandler->write($id, serialize($data)));
+ $this->assertNotNull($this->mongoCollection->findOne(array('_id' => $id)));
+ $this->assertEquals('', $saveHandler->read($id));
+ $this->assertNull($this->mongoCollection->findOne(array('_id' => $id)));
+
+ ini_set('session.gc_maxlifetime', $oldMaxlifetime);
+ }
+
+ public function testGarbageCollection()
+ {
+ $saveHandler = new MongoDB($this->mongo, $this->options);
+ $this->assertTrue($saveHandler->open('savepath', 'sessionname'));
+
+ $data = array('foo' => 'bar');
+
+ $this->assertTrue($saveHandler->write(123, serialize($data)));
+ $this->assertTrue($saveHandler->write(456, serialize($data)));
+ $this->assertEquals(2, $this->mongoCollection->count());
+ $saveHandler->gc(5);
+ $this->assertEquals(2, $this->mongoCollection->count());
+
+ /* Note: MongoDate uses micro-second precision, so even a maximum
+ * lifetime of zero would not match records that were just inserted.
+ * Use a negative number instead.
+ */
+ $saveHandler->gc(-1);
+ $this->assertEquals(0, $this->mongoCollection->count());
+ }
+
+ /**
+ * @expectedException MongoCursorException
+ */
+ public function testWriteExceptionEdgeCaseForChangedSessionName()
+ {
+ $saveHandler = new MongoDB($this->mongo, $this->options);
+ $this->assertTrue($saveHandler->open('savepath', 'sessionname'));
+
+ $id = '242';
+ $data = array('foo' => 'bar');
+
+ /* Note: a MongoCursorException will be thrown if a record with this ID
+ * already exists with a different session name, since the upsert query
+ * cannot insert a new document with the same ID and new session name.
+ * This should only happen if ID's are not unique or if the session name
+ * is altered mid-process.
+ */
+ $saveHandler->write($id, serialize($data));
+ $saveHandler->open('savepath', 'sessionname_changed');
+ $saveHandler->write($id, serialize($data));
+ }
+}
Please sign in to comment.
Something went wrong with that request. Please try again.