getDatabase())) { throw new InvalidArgumentException('The database option cannot be empty'); } if (null === ($collection = $options->getCollection())) { throw new InvalidArgumentException('The collection option cannot be empty'); } $this->options = $options; } /** * Open session * * @param string $path * @param string $name * @return bool */ #[ReturnTypeWillChange] public function open($path, $name) { // Note: session save path is not used $this->sessionName = $name; $this->lifetime = (int) 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; } /** * Close session * * @return bool */ #[ReturnTypeWillChange] public function close() { return true; } /** * Read session data * * @param string $id * @return string */ #[ReturnTypeWillChange] public function read($id) { $session = $this->mongoCollection->findOne([ '_id' => $id, $this->options->getNameField() => $this->sessionName, ]); if (null !== $session) { // 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 ''; } } return $session[$this->options->getDataField()]->getData(); } return ''; } /** * Write session data * * @param string $id * @param string $data * @return bool */ #[ReturnTypeWillChange] public function write($id, $data) { $saveOptions = array_replace( $this->options->getSaveOptions(), ['upsert' => true, 'multiple' => false] ); $criteria = [ '_id' => $id, $this->options->getNameField() => $this->sessionName, ]; $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->updateOne($criteria, $newObj, $saveOptions); return $result->isAcknowledged(); } /** * Destroy session * * @param string $id * @return bool */ #[ReturnTypeWillChange] public function destroy($id) { $result = $this->mongoCollection->deleteOne( [ '_id' => $id, $this->options->getNameField() => $this->sessionName, ], $this->options->getSaveOptions() ); return $result->isAcknowledged(); } /** * Garbage collection * * Note: MongoDB 2.2+ supports TTL collections, which may be used in place * of this method by indexing the "modified" field with an * "expireAfterSeconds" option. Regardless of whether TTL collections are * used, consider indexing this field to make the remove query more * efficient. * * @see http://docs.mongodb.org/manual/tutorial/expire-data/ * * @param int $maxlifetime * @return bool */ #[ReturnTypeWillChange] 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. */ $microseconds = floor(microtime(true) * 1000) - $maxlifetime * 1000; $result = $this->mongoCollection->deleteMany( [ $this->options->getModifiedField() => ['$lt' => new UTCDateTime($microseconds)], ], $this->options->getSaveOptions() ); return $result->isAcknowledged(); } }