Skip to content

Commit

Permalink
feature #52336 [HttpFoundation][Lock] Makes MongoDB adapters usable w…
Browse files Browse the repository at this point in the history
…ith `ext-mongodb` only (GromNaN)

This PR was squashed before being merged into the 6.4 branch.

Discussion
----------

[HttpFoundation][Lock] Makes MongoDB adapters usable with `ext-mongodb` only

| Q             | A
| ------------- | ---
| Branch?       | 6.4
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Issues        | -
| License       | MIT

[`mongodb/mongodb`](https://packagist.org/packages/mongodb/mongodb) is complex to handle for libraries with optional support of MongoDB, as it requires `ext-mongodb`. In order to reduce complexity for maintainers, I reimplemented the session and lock adapters to use only the C driver classes.

Some features of `MongoDB\Client` are missing (server selection, session, transaction). But they are not necessary to store Sessions and Lock.

Changes:
- Lock & Session accept a `MongoDB\Driver\Manager`
- The lock uses exclusively UTC date. Before, there was a mix of `time()` and `UTCDatetime` objects.
- Session tests require a mongo server.
- `mongodb/mongodb` not needed in the CI

And of course also allows developers to use this adapters without installing `mongodb/mongodb` if they want, with the same features as before.

Commits
-------

bc24cb3 [HttpFoundation][Lock] Makes MongoDB adapters usable with `ext-mongodb` only
  • Loading branch information
fabpot committed Nov 2, 2023
2 parents 8b13301 + bc24cb3 commit 8a69f67
Show file tree
Hide file tree
Showing 12 changed files with 402 additions and 256 deletions.
1 change: 0 additions & 1 deletion .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@ jobs:
echo COMPOSER_ROOT_VERSION=$COMPOSER_ROOT_VERSION >> $GITHUB_ENV
echo "::group::composer update"
composer require --dev --no-update mongodb/mongodb
composer update --no-progress --ansi
echo "::endgroup::"
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,6 @@ jobs:
exit 0
fi
(cd src/Symfony/Component/HttpFoundation; cp composer.json composer.bak; composer require --dev --no-update mongodb/mongodb)
(cd src/Symfony/Component/Lock; cp composer.json composer.bak; composer require --dev --no-update mongodb/mongodb)
# matrix.mode = high-deps
echo "$COMPONENTS" | xargs -n1 | parallel -j +3 "_run_tests {} 'cd {} && $COMPOSER_UP && $PHPUNIT$LEGACY'" || X=1
Expand All @@ -211,8 +208,6 @@ jobs:
git fetch --depth=2 origin $SYMFONY_VERSION
git checkout -m FETCH_HEAD
PATCHED_COMPONENTS=$(echo "$PATCHED_COMPONENTS" | xargs dirname | xargs -n1 -I{} bash -c "[ -e '{}/phpunit.xml.dist' ] && echo '{}'" | sort || true)
(cd src/Symfony/Component/HttpFoundation; composer require --dev --no-update mongodb/mongodb)
(cd src/Symfony/Component/Lock; composer require --dev --no-update mongodb/mongodb)
if [[ $PATCHED_COMPONENTS ]]; then
echo "::group::install phpunit"
./phpunit install
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpFoundation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ CHANGELOG
* Add `UriSigner` from the HttpKernel component
* Add `partitioned` flag to `Cookie` (CHIPS Cookie)
* Add argument `bool $flush = true` to `Response::send()`
* Make `MongoDbSessionHandler` instantiable with the mongodb extension directly

6.3
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,22 @@
use MongoDB\BSON\Binary;
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
use MongoDB\Collection;
use MongoDB\Driver\BulkWrite;
use MongoDB\Driver\Manager;
use MongoDB\Driver\Query;

/**
* Session handler using the mongodb/mongodb package and MongoDB driver extension.
* Session handler using the MongoDB driver extension.
*
* @author Markus Bachmann <markus.bachmann@bachi.biz>
* @author Jérôme Tamarelle <jerome@tamarelle.net>
*
* @see https://packagist.org/packages/mongodb/mongodb
* @see https://php.net/mongodb
*/
class MongoDbSessionHandler extends AbstractSessionHandler
{
private Client $mongo;
private Collection $collection;
private Manager $manager;
private string $namespace;
private array $options;
private int|\Closure|null $ttl;

Expand Down Expand Up @@ -62,13 +64,18 @@ class MongoDbSessionHandler extends AbstractSessionHandler
*
* @throws \InvalidArgumentException When "database" or "collection" not provided
*/
public function __construct(Client $mongo, array $options)
public function __construct(Client|Manager $mongo, array $options)
{
if (!isset($options['database']) || !isset($options['collection'])) {
throw new \InvalidArgumentException('You must provide the "database" and "collection" option for MongoDBSessionHandler.');
}

$this->mongo = $mongo;
if ($mongo instanceof Client) {
$mongo = $mongo->getManager();
}

$this->manager = $mongo;
$this->namespace = $options['database'].'.'.$options['collection'];

$this->options = array_merge([
'id_field' => '_id',
Expand All @@ -86,77 +93,94 @@ public function close(): bool

protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool
{
$this->getCollection()->deleteOne([
$this->options['id_field'] => $sessionId,
]);
$write = new BulkWrite();
$write->delete(
[$this->options['id_field'] => $sessionId],
['limit' => 1]
);

$this->manager->executeBulkWrite($this->namespace, $write);

return true;
}

public function gc(int $maxlifetime): int|false
{
return $this->getCollection()->deleteMany([
$this->options['expiry_field'] => ['$lt' => new UTCDateTime()],
])->getDeletedCount();
$write = new BulkWrite();
$write->delete(
[$this->options['expiry_field'] => ['$lt' => $this->getUTCDateTime()]],
);
$result = $this->manager->executeBulkWrite($this->namespace, $write);

return $result->getDeletedCount() ?? false;
}

protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool
{
$ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime');
$expiry = new UTCDateTime((time() + (int) $ttl) * 1000);
$expiry = $this->getUTCDateTime($ttl);

$fields = [
$this->options['time_field'] => new UTCDateTime(),
$this->options['time_field'] => $this->getUTCDateTime(),
$this->options['expiry_field'] => $expiry,
$this->options['data_field'] => new Binary($data, Binary::TYPE_OLD_BINARY),
$this->options['data_field'] => new Binary($data, Binary::TYPE_GENERIC),
];

$this->getCollection()->updateOne(
$write = new BulkWrite();
$write->update(
[$this->options['id_field'] => $sessionId],
['$set' => $fields],
['upsert' => true]
);

$this->manager->executeBulkWrite($this->namespace, $write);

return true;
}

public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool
{
$ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime');
$expiry = new UTCDateTime((time() + (int) $ttl) * 1000);
$expiry = $this->getUTCDateTime($ttl);

$this->getCollection()->updateOne(
$write = new BulkWrite();
$write->update(
[$this->options['id_field'] => $sessionId],
['$set' => [
$this->options['time_field'] => new UTCDateTime(),
$this->options['time_field'] => $this->getUTCDateTime(),
$this->options['expiry_field'] => $expiry,
]]
]],
['multi' => false],
);

$this->manager->executeBulkWrite($this->namespace, $write);

return true;
}

protected function doRead(#[\SensitiveParameter] string $sessionId): string
{
$dbData = $this->getCollection()->findOne([
$cursor = $this->manager->executeQuery($this->namespace, new Query([
$this->options['id_field'] => $sessionId,
$this->options['expiry_field'] => ['$gte' => new UTCDateTime()],
]);

if (null === $dbData) {
return '';
$this->options['expiry_field'] => ['$gte' => $this->getUTCDateTime()],
], [
'projection' => [
'_id' => false,
$this->options['data_field'] => true,
],
'limit' => 1,
]));

foreach ($cursor as $document) {
return (string) $document->{$this->options['data_field']} ?? '';
}

return $dbData[$this->options['data_field']]->getData();
}

private function getCollection(): Collection
{
return $this->collection ??= $this->mongo->selectCollection($this->options['database'], $this->options['collection']);
// Not found
return '';
}

protected function getMongo(): Client
private function getUTCDateTime(int $additionalSeconds = 0): UTCDateTime
{
return $this->mongo;
return new UTCDateTime((time() + $additionalSeconds) * 1000);
}
}
Loading

0 comments on commit 8a69f67

Please sign in to comment.