From ebbc7068f95a6cb0c8c8d6dc1397fa6c333911d1 Mon Sep 17 00:00:00 2001 From: Ben Oman Date: Sat, 25 Jun 2016 17:51:43 -0600 Subject: [PATCH] Fix for #19183 to add support for new PHP MongoDB extension in sessions. --- .../Storage/Handler/MongoDbSessionHandler.php | 68 +++++++-- .../Handler/MongoDbSessionHandlerTest.php | 132 ++++++++++++++---- 2 files changed, 162 insertions(+), 38 deletions(-) diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MongoDbSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MongoDbSessionHandler.php index f1df25d0a6..23bca61e60 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MongoDbSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MongoDbSessionHandler.php @@ -61,15 +61,15 @@ class MongoDbSessionHandler implements \SessionHandlerInterface * If you use such an index, you can drop `gc_probability` to 0 since * no garbage-collection is required. * - * @param \Mongo|\MongoClient $mongo A MongoClient or Mongo instance - * @param array $options An associative array of field options + * @param \Mongo|\MongoClient|\MongoDB\Client $mongo A MongoDB\Client, MongoClient or Mongo instance + * @param array $options An associative array of field options * * @throws \InvalidArgumentException When MongoClient or Mongo instance not provided * @throws \InvalidArgumentException When "database" or "collection" not provided */ public function __construct($mongo, array $options) { - if (!($mongo instanceof \MongoClient || $mongo instanceof \Mongo)) { + if (!($mongo instanceof \MongoDB\Client || $mongo instanceof \MongoClient || $mongo instanceof \Mongo)) { throw new \InvalidArgumentException('MongoClient or Mongo instance required'); } @@ -108,7 +108,9 @@ class MongoDbSessionHandler implements \SessionHandlerInterface */ public function destroy($sessionId) { - $this->getCollection()->remove(array( + $methodName = ($this->mongo instanceof \MongoDB\Client) ? 'deleteOne' : 'remove'; + + $this->getCollection()->$methodName(array( $this->options['id_field'] => $sessionId, )); @@ -120,8 +122,10 @@ class MongoDbSessionHandler implements \SessionHandlerInterface */ public function gc($maxlifetime) { - $this->getCollection()->remove(array( - $this->options['expiry_field'] => array('$lt' => new \MongoDate()), + $methodName = ($this->mongo instanceof \MongoDB\Client) ? 'deleteOne' : 'remove'; + + $this->getCollection()->$methodName(array( + $this->options['expiry_field'] => array('$lt' => $this->createDateTime()), )); return true; @@ -132,18 +136,28 @@ class MongoDbSessionHandler implements \SessionHandlerInterface */ public function write($sessionId, $data) { - $expiry = new \MongoDate(time() + (int) ini_get('session.gc_maxlifetime')); + $expiry = $this->createDateTime(time() + (int) ini_get('session.gc_maxlifetime')); $fields = array( - $this->options['data_field'] => new \MongoBinData($data, \MongoBinData::BYTE_ARRAY), - $this->options['time_field'] => new \MongoDate(), + $this->options['time_field'] => $this->createDateTime(), $this->options['expiry_field'] => $expiry, ); - $this->getCollection()->update( + $options = array('upsert' => true); + + if ($this->mongo instanceof \MongoDB\Client) { + $fields[$this->options['data_field']] = new \MongoDB\BSON\Binary($data, \MongoDB\BSON\Binary::TYPE_OLD_BINARY); + } else { + $fields[$this->options['data_field']] = new \MongoBinData($data, \MongoBinData::BYTE_ARRAY); + $options['multiple'] = false; + } + + $methodName = ($this->mongo instanceof \MongoDB\Client) ? 'updateOne' : 'update'; + + $this->getCollection()->$methodName( array($this->options['id_field'] => $sessionId), array('$set' => $fields), - array('upsert' => true, 'multiple' => false) + $options ); return true; @@ -156,10 +170,18 @@ class MongoDbSessionHandler implements \SessionHandlerInterface { $dbData = $this->getCollection()->findOne(array( $this->options['id_field'] => $sessionId, - $this->options['expiry_field'] => array('$gte' => new \MongoDate()), + $this->options['expiry_field'] => array('$gte' => $this->createDateTime()), )); - return null === $dbData ? '' : $dbData[$this->options['data_field']]->bin; + if (null === $dbData) { + return ''; + } + + if ($dbData[$this->options['data_field']] instanceof \MongoDB\BSON\Binary) { + return $dbData[$this->options['data_field']]->getData(); + } + + return $dbData[$this->options['data_field']]->bin; } /** @@ -185,4 +207,24 @@ class MongoDbSessionHandler implements \SessionHandlerInterface { return $this->mongo; } + + /** + * Create a date object using the class appropriate for the current mongo connection. + * + * Return an instance of a MongoDate or \MongoDB\BSON\UTCDateTime + * + * @param int $seconds An integer representing UTC seconds since Jan 1 1970. Defaults to now. + */ + private function createDateTime($seconds = null) + { + if (is_null($seconds)) { + $seconds = time(); + } + + if ($this->mongo instanceof \MongoDB\Client) { + return new \MongoDB\BSON\UTCDateTime($seconds * 1000); + } + + return new \MongoDate($seconds); + } } diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php index 556f26a33c..0ae0d4b2f7 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php @@ -15,7 +15,6 @@ use Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandl /** * @author Markus Bachmann - * @requires extension mongo * @group time-sensitive */ class MongoDbSessionHandlerTest extends \PHPUnit_Framework_TestCase @@ -31,7 +30,15 @@ class MongoDbSessionHandlerTest extends \PHPUnit_Framework_TestCase { parent::setUp(); - $mongoClass = version_compare(phpversion('mongo'), '1.3.0', '<') ? 'Mongo' : 'MongoClient'; + if (!extension_loaded('mongo') && !extension_loaded('mongodb')) { + $this->markTestSkipped('The Mongo or MongoDB extension is required.'); + } + + if (phpversion('mongodb')) { + $mongoClass = 'MongoDB\Client'; + } else { + $mongoClass = version_compare(phpversion('mongo'), '1.3.0', '<') ? 'Mongo' : 'MongoClient'; + } $this->mongo = $this->getMockBuilder($mongoClass) ->disableOriginalConstructor() @@ -98,14 +105,28 @@ class MongoDbSessionHandlerTest extends \PHPUnit_Framework_TestCase $that->assertArrayHasKey($that->options['expiry_field'], $criteria); $that->assertArrayHasKey('$gte', $criteria[$that->options['expiry_field']]); - $that->assertInstanceOf('MongoDate', $criteria[$that->options['expiry_field']]['$gte']); - $that->assertGreaterThanOrEqual($criteria[$that->options['expiry_field']]['$gte']->sec, $testTimeout); - return array( + if (phpversion('mongodb')) { + $that->assertInstanceOf('MongoDB\BSON\UTCDateTime', $criteria[$that->options['expiry_field']]['$gte']); + $that->assertGreaterThanOrEqual(round(intval((string) $criteria[$that->options['expiry_field']]['$gte']) / 1000), $testTimeout); + } else { + $that->assertInstanceOf('MongoDate', $criteria[$that->options['expiry_field']]['$gte']); + $that->assertGreaterThanOrEqual($criteria[$that->options['expiry_field']]['$gte']->sec, $testTimeout); + } + + $fields = array( $that->options['id_field'] => 'foo', - $that->options['data_field'] => new \MongoBinData('bar', \MongoBinData::BYTE_ARRAY), - $that->options['id_field'] => new \MongoDate(), ); + + if (phpversion('mongodb')) { + $fields[$that->options['data_field']] = new \MongoDB\BSON\Binary('bar', \MongoDB\BSON\Binary::TYPE_OLD_BINARY); + $fields[$that->options['id_field']] = new \MongoDB\BSON\UTCDateTime(time() * 1000); + } else { + $fields[$that->options['data_field']] = new \MongoBinData('bar', \MongoBinData::BYTE_ARRAY); + $fields[$that->options['id_field']] = new \MongoDate(); + } + + return $fields; })); $this->assertEquals('bar', $this->storage->read('foo')); @@ -123,11 +144,18 @@ class MongoDbSessionHandlerTest extends \PHPUnit_Framework_TestCase $that = $this; $data = array(); + $methodName = phpversion('mongodb') ? 'updateOne' : 'update'; + $collection->expects($this->once()) - ->method('update') + ->method($methodName) ->will($this->returnCallback(function ($criteria, $updateData, $options) use ($that, &$data) { $that->assertEquals(array($that->options['id_field'] => 'foo'), $criteria); - $that->assertEquals(array('upsert' => true, 'multiple' => false), $options); + + if (phpversion('mongodb')) { + $that->assertEquals(array('upsert' => true), $options); + } else { + $that->assertEquals(array('upsert' => true, 'multiple' => false), $options); + } $data = $updateData['$set']; })); @@ -135,10 +163,17 @@ class MongoDbSessionHandlerTest extends \PHPUnit_Framework_TestCase $expectedExpiry = time() + (int) ini_get('session.gc_maxlifetime'); $this->assertTrue($this->storage->write('foo', 'bar')); - $this->assertEquals('bar', $data[$this->options['data_field']]->bin); - $that->assertInstanceOf('MongoDate', $data[$this->options['time_field']]); - $this->assertInstanceOf('MongoDate', $data[$this->options['expiry_field']]); - $this->assertGreaterThanOrEqual($expectedExpiry, $data[$this->options['expiry_field']]->sec); + if (phpversion('mongodb')) { + $that->assertEquals('bar', $data[$that->options['data_field']]->getData()); + $that->assertInstanceOf('MongoDB\BSON\UTCDateTime', $data[$that->options['time_field']]); + $that->assertInstanceOf('MongoDB\BSON\UTCDateTime', $data[$that->options['expiry_field']]); + $that->assertGreaterThanOrEqual($expectedExpiry, round(intval((string) $data[$that->options['expiry_field']]) / 1000)); + } else { + $that->assertEquals('bar', $data[$that->options['data_field']]->bin); + $that->assertInstanceOf('MongoDate', $data[$that->options['time_field']]); + $that->assertInstanceOf('MongoDate', $data[$that->options['expiry_field']]); + $that->assertGreaterThanOrEqual($expectedExpiry, $data[$that->options['expiry_field']]->sec); + } } public function testWriteWhenUsingExpiresField() @@ -164,20 +199,33 @@ class MongoDbSessionHandlerTest extends \PHPUnit_Framework_TestCase $that = $this; $data = array(); + $methodName = phpversion('mongodb') ? 'updateOne' : 'update'; + $collection->expects($this->once()) - ->method('update') + ->method($methodName) ->will($this->returnCallback(function ($criteria, $updateData, $options) use ($that, &$data) { $that->assertEquals(array($that->options['id_field'] => 'foo'), $criteria); - $that->assertEquals(array('upsert' => true, 'multiple' => false), $options); + + if (phpversion('mongodb')) { + $that->assertEquals(array('upsert' => true), $options); + } else { + $that->assertEquals(array('upsert' => true, 'multiple' => false), $options); + } $data = $updateData['$set']; })); $this->assertTrue($this->storage->write('foo', 'bar')); - $this->assertEquals('bar', $data[$this->options['data_field']]->bin); - $that->assertInstanceOf('MongoDate', $data[$this->options['time_field']]); - $that->assertInstanceOf('MongoDate', $data[$this->options['expiry_field']]); + if (phpversion('mongodb')) { + $that->assertEquals('bar', $data[$that->options['data_field']]->getData()); + $that->assertInstanceOf('MongoDB\BSON\UTCDateTime', $data[$that->options['time_field']]); + $that->assertInstanceOf('MongoDB\BSON\UTCDateTime', $data[$that->options['expiry_field']]); + } else { + $that->assertEquals('bar', $data[$that->options['data_field']]->bin); + $that->assertInstanceOf('MongoDate', $data[$that->options['time_field']]); + $that->assertInstanceOf('MongoDate', $data[$that->options['expiry_field']]); + } } public function testReplaceSessionData() @@ -191,8 +239,10 @@ class MongoDbSessionHandlerTest extends \PHPUnit_Framework_TestCase $data = array(); + $methodName = phpversion('mongodb') ? 'updateOne' : 'update'; + $collection->expects($this->exactly(2)) - ->method('update') + ->method($methodName) ->will($this->returnCallback(function ($criteria, $updateData, $options) use (&$data) { $data = $updateData; })); @@ -200,7 +250,11 @@ class MongoDbSessionHandlerTest extends \PHPUnit_Framework_TestCase $this->storage->write('foo', 'bar'); $this->storage->write('foo', 'foobar'); - $this->assertEquals('foobar', $data['$set'][$this->options['data_field']]->bin); + if (phpversion('mongodb')) { + $this->assertEquals('foobar', $data['$set'][$this->options['data_field']]->getData()); + } else { + $this->assertEquals('foobar', $data['$set'][$this->options['data_field']]->bin); + } } public function testDestroy() @@ -212,8 +266,10 @@ class MongoDbSessionHandlerTest extends \PHPUnit_Framework_TestCase ->with($this->options['database'], $this->options['collection']) ->will($this->returnValue($collection)); + $methodName = phpversion('mongodb') ? 'deleteOne' : 'remove'; + $collection->expects($this->once()) - ->method('remove') + ->method($methodName) ->with(array($this->options['id_field'] => 'foo')); $this->assertTrue($this->storage->destroy('foo')); @@ -230,19 +286,45 @@ class MongoDbSessionHandlerTest extends \PHPUnit_Framework_TestCase $that = $this; + $methodName = phpversion('mongodb') ? 'deleteOne' : 'remove'; + $collection->expects($this->once()) - ->method('remove') + ->method($methodName) ->will($this->returnCallback(function ($criteria) use ($that) { - $that->assertInstanceOf('MongoDate', $criteria[$that->options['expiry_field']]['$lt']); - $that->assertGreaterThanOrEqual(time() - 1, $criteria[$that->options['expiry_field']]['$lt']->sec); + if (phpversion('mongodb')) { + $that->assertInstanceOf('MongoDB\BSON\UTCDateTime', $criteria[$that->options['expiry_field']]['$lt']); + $that->assertGreaterThanOrEqual(time() - 1, round(intval((string) $criteria[$that->options['expiry_field']]['$lt']) / 1000)); + } else { + $that->assertInstanceOf('MongoDate', $criteria[$that->options['expiry_field']]['$lt']); + $that->assertGreaterThanOrEqual(time() - 1, $criteria[$that->options['expiry_field']]['$lt']->sec); + } })); $this->assertTrue($this->storage->gc(1)); } + public function testGetConnection() + { + $method = new \ReflectionMethod($this->storage, 'getMongo'); + $method->setAccessible(true); + + if (phpversion('mongodb')) { + $mongoClass = 'MongoDB\Client'; + } else { + $mongoClass = version_compare(phpversion('mongo'), '1.3.0', '<') ? 'Mongo' : 'MongoClient'; + } + + $this->assertInstanceOf($mongoClass, $method->invoke($this->storage)); + } + private function createMongoCollectionMock() { - $collection = $this->getMockBuilder('MongoCollection') + $collectionClass = 'MongoCollection'; + if (phpversion('mongodb')) { + $collectionClass = 'MongoDB\Collection'; + } + + $collection = $this->getMockBuilder($collectionClass) ->disableOriginalConstructor() ->getMock();