Skip to content
This repository has been archived by the owner on Jan 31, 2020. It is now read-only.

Commit

Permalink
Merge pull request #34 from sandrokeil/feature/mongodb
Browse files Browse the repository at this point in the history
use mongodb extension instead of mongo extension
  • Loading branch information
weierophinney committed Apr 11, 2016
2 parents dd01fc5 + 70be000 commit f56d11c
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 72 deletions.
8 changes: 6 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ cache:
directories:
- $HOME/.composer/cache

services:
- mongodb

matrix:
fast_finish: true
include:
Expand All @@ -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"
Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -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,
Expand Down
9 changes: 5 additions & 4 deletions doc/book/zend.session.save-handler.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
```
Expand Down
98 changes: 61 additions & 37 deletions src/SaveHandler/MongoDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,24 @@

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;

/**
* MongoDB session save handler
*/
class MongoDB implements SaveHandlerInterface
{
/**
* MongoClient instance
*
* @var MongoClient
*/
protected $mongoClient;

/**
* MongoCollection instance
*
Expand Down Expand Up @@ -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');
}
Expand All @@ -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;
}

Expand All @@ -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;
}

Expand Down Expand Up @@ -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 '';
Expand All @@ -148,21 +162,23 @@ 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
* 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);
$result = $this->mongoCollection->updateOne($criteria, $newObj, $saveOptions);

return (bool) (isset($result['ok']) ? $result['ok'] : $result);
return $result->isAcknowledged();
}

/**
Expand All @@ -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();
}

/**
Expand All @@ -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();
}
}
25 changes: 25 additions & 0 deletions src/SaveHandler/MongoDBOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ class MongoDBOptions extends AbstractOptions
*/
protected $modifiedField = 'modified';

/**
* Use expireAfterSeconds index
*
* @var bool
*/
protected $useExpireAfterSecondsIndex = false;

/**
* {@inheritdoc}
Expand Down Expand Up @@ -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;
}
}
43 changes: 15 additions & 28 deletions test/SaveHandler/MongoDBTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
);
}

/**
Expand All @@ -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';
Expand All @@ -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';
Expand All @@ -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'];
Expand All @@ -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';
Expand Down

0 comments on commit f56d11c

Please sign in to comment.