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

use mongodb extension instead of mongo extension #34

Merged
merged 1 commit into from
Apr 11, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only available in mongo php library. We don't need this. We can do everything with the raw extension here, imho.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can, but it requires far more code and code complexity. The Mongo PHP Library exists to provide the convenience layer that was previously in the extension, but in a way that can be more quickly and easily updated as new features are added and/or bugfixes released. Considering most developers will now be using the extension / PHP library combination if using MongoDB at all on PHP, this is not an unreasonable requirement.

use MongoDB\Collection as MongoCollection;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only available in mongo php library. We don't need this. We can do everything with the raw extension here, imho.

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems you are adding a feature. Please do it in a different PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, as mentioned above. I will do that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd argue differently. This PR:

  • is swapping out the underlying extension used
  • gaining the ability to expose features previously unavailable (due to extension limitations)

As such, it's already targeted at a new minor version. Pushing the feature into a later PR creates more work for @sandrokeil, when we can easily evaluate it in this same pull request.


/**
* {@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