Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; Chris@0: Chris@0: /** Chris@14: * Session handler using the mongodb/mongodb package and MongoDB driver extension. Chris@0: * Chris@0: * @author Markus Bachmann Chris@14: * Chris@14: * @see https://packagist.org/packages/mongodb/mongodb Chris@14: * @see http://php.net/manual/en/set.mongodb.php Chris@0: */ Chris@14: class MongoDbSessionHandler extends AbstractSessionHandler Chris@0: { Chris@0: private $mongo; Chris@0: Chris@0: /** Chris@0: * @var \MongoCollection Chris@0: */ Chris@0: private $collection; Chris@0: Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@0: private $options; Chris@0: Chris@0: /** Chris@0: * Constructor. Chris@0: * Chris@0: * List of available options: Chris@0: * * database: The name of the database [required] Chris@0: * * collection: The name of the collection [required] Chris@0: * * id_field: The field name for storing the session id [default: _id] Chris@0: * * data_field: The field name for storing the session data [default: data] Chris@0: * * time_field: The field name for storing the timestamp [default: time] Chris@14: * * expiry_field: The field name for storing the expiry-timestamp [default: expires_at]. Chris@0: * Chris@0: * It is strongly recommended to put an index on the `expiry_field` for Chris@0: * garbage-collection. Alternatively it's possible to automatically expire Chris@0: * the sessions in the database as described below: Chris@0: * Chris@0: * A TTL collections can be used on MongoDB 2.2+ to cleanup expired sessions Chris@0: * automatically. Such an index can for example look like this: Chris@0: * Chris@0: * db..ensureIndex( Chris@0: * { "": 1 }, Chris@0: * { "expireAfterSeconds": 0 } Chris@0: * ) Chris@0: * Chris@0: * More details on: http://docs.mongodb.org/manual/tutorial/expire-data/ Chris@0: * Chris@0: * If you use such an index, you can drop `gc_probability` to 0 since Chris@0: * no garbage-collection is required. Chris@0: * Chris@14: * @param \MongoDB\Client $mongo A MongoDB\Client instance Chris@14: * @param array $options An associative array of field options Chris@0: * Chris@0: * @throws \InvalidArgumentException When MongoClient or Mongo instance not provided Chris@0: * @throws \InvalidArgumentException When "database" or "collection" not provided Chris@0: */ Chris@0: public function __construct($mongo, array $options) Chris@0: { Chris@14: if ($mongo instanceof \MongoClient || $mongo instanceof \Mongo) { Chris@14: @trigger_error(sprintf('Using %s with the legacy mongo extension is deprecated as of 3.4 and will be removed in 4.0. Use it with the mongodb/mongodb package and ext-mongodb instead.', __CLASS__), E_USER_DEPRECATED); Chris@14: } Chris@14: Chris@0: if (!($mongo instanceof \MongoDB\Client || $mongo instanceof \MongoClient || $mongo instanceof \Mongo)) { Chris@0: throw new \InvalidArgumentException('MongoClient or Mongo instance required'); Chris@0: } Chris@0: Chris@0: if (!isset($options['database']) || !isset($options['collection'])) { Chris@0: throw new \InvalidArgumentException('You must provide the "database" and "collection" option for MongoDBSessionHandler'); Chris@0: } Chris@0: Chris@0: $this->mongo = $mongo; Chris@0: Chris@17: $this->options = array_merge([ Chris@0: 'id_field' => '_id', Chris@0: 'data_field' => 'data', Chris@0: 'time_field' => 'time', Chris@0: 'expiry_field' => 'expires_at', Chris@17: ], $options); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function close() Chris@0: { Chris@0: return true; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@14: protected function doDestroy($sessionId) Chris@0: { Chris@0: $methodName = $this->mongo instanceof \MongoDB\Client ? 'deleteOne' : 'remove'; Chris@0: Chris@17: $this->getCollection()->$methodName([ Chris@0: $this->options['id_field'] => $sessionId, Chris@17: ]); Chris@0: Chris@0: return true; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function gc($maxlifetime) Chris@0: { Chris@14: $methodName = $this->mongo instanceof \MongoDB\Client ? 'deleteMany' : 'remove'; Chris@0: Chris@17: $this->getCollection()->$methodName([ Chris@17: $this->options['expiry_field'] => ['$lt' => $this->createDateTime()], Chris@17: ]); Chris@0: Chris@0: return true; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@14: protected function doWrite($sessionId, $data) Chris@0: { Chris@0: $expiry = $this->createDateTime(time() + (int) ini_get('session.gc_maxlifetime')); Chris@0: Chris@17: $fields = [ Chris@0: $this->options['time_field'] => $this->createDateTime(), Chris@0: $this->options['expiry_field'] => $expiry, Chris@17: ]; Chris@0: Chris@17: $options = ['upsert' => true]; Chris@0: Chris@0: if ($this->mongo instanceof \MongoDB\Client) { Chris@0: $fields[$this->options['data_field']] = new \MongoDB\BSON\Binary($data, \MongoDB\BSON\Binary::TYPE_OLD_BINARY); Chris@0: } else { Chris@0: $fields[$this->options['data_field']] = new \MongoBinData($data, \MongoBinData::BYTE_ARRAY); Chris@0: $options['multiple'] = false; Chris@0: } Chris@0: Chris@0: $methodName = $this->mongo instanceof \MongoDB\Client ? 'updateOne' : 'update'; Chris@0: Chris@0: $this->getCollection()->$methodName( Chris@17: [$this->options['id_field'] => $sessionId], Chris@17: ['$set' => $fields], Chris@0: $options Chris@0: ); Chris@0: Chris@0: return true; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@14: public function updateTimestamp($sessionId, $data) Chris@14: { Chris@14: $expiry = $this->createDateTime(time() + (int) ini_get('session.gc_maxlifetime')); Chris@14: Chris@14: if ($this->mongo instanceof \MongoDB\Client) { Chris@14: $methodName = 'updateOne'; Chris@17: $options = []; Chris@14: } else { Chris@14: $methodName = 'update'; Chris@17: $options = ['multiple' => false]; Chris@14: } Chris@14: Chris@14: $this->getCollection()->$methodName( Chris@17: [$this->options['id_field'] => $sessionId], Chris@17: ['$set' => [ Chris@14: $this->options['time_field'] => $this->createDateTime(), Chris@14: $this->options['expiry_field'] => $expiry, Chris@17: ]], Chris@14: $options Chris@14: ); Chris@14: Chris@14: return true; Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: protected function doRead($sessionId) Chris@0: { Chris@17: $dbData = $this->getCollection()->findOne([ Chris@0: $this->options['id_field'] => $sessionId, Chris@17: $this->options['expiry_field'] => ['$gte' => $this->createDateTime()], Chris@17: ]); Chris@0: Chris@0: if (null === $dbData) { Chris@0: return ''; Chris@0: } Chris@0: Chris@0: if ($dbData[$this->options['data_field']] instanceof \MongoDB\BSON\Binary) { Chris@0: return $dbData[$this->options['data_field']]->getData(); Chris@0: } Chris@0: Chris@0: return $dbData[$this->options['data_field']]->bin; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Return a "MongoCollection" instance. Chris@0: * Chris@0: * @return \MongoCollection Chris@0: */ Chris@0: private function getCollection() Chris@0: { Chris@0: if (null === $this->collection) { Chris@0: $this->collection = $this->mongo->selectCollection($this->options['database'], $this->options['collection']); Chris@0: } Chris@0: Chris@0: return $this->collection; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Return a Mongo instance. Chris@0: * Chris@0: * @return \Mongo|\MongoClient|\MongoDB\Client Chris@0: */ Chris@0: protected function getMongo() Chris@0: { Chris@0: return $this->mongo; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Create a date object using the class appropriate for the current mongo connection. Chris@0: * Chris@0: * Return an instance of a MongoDate or \MongoDB\BSON\UTCDateTime Chris@0: * Chris@0: * @param int $seconds An integer representing UTC seconds since Jan 1 1970. Defaults to now. Chris@0: * Chris@0: * @return \MongoDate|\MongoDB\BSON\UTCDateTime Chris@0: */ Chris@0: private function createDateTime($seconds = null) Chris@0: { Chris@0: if (null === $seconds) { Chris@0: $seconds = time(); Chris@0: } Chris@0: Chris@0: if ($this->mongo instanceof \MongoDB\Client) { Chris@0: return new \MongoDB\BSON\UTCDateTime($seconds * 1000); Chris@0: } Chris@0: Chris@0: return new \MongoDate($seconds); Chris@0: } Chris@0: }