Skip to content

Commit 1213aa3

Browse files
GromNaNfabpot
authored andcommitted
[HttpFoundation][Lock] Makes MongoDB adapters usable with ext-mongodb only
1 parent 3c30a5b commit 1213aa3

File tree

5 files changed

+201
-92
lines changed

5 files changed

+201
-92
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
6.4
5+
---
6+
7+
* Make `MongoDbStore` instantiable with the mongodb extension directly
8+
49
6.3
510
---
611

Store/MongoDbStore.php

Lines changed: 77 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@
1414
use MongoDB\BSON\UTCDateTime;
1515
use MongoDB\Client;
1616
use MongoDB\Collection;
17+
use MongoDB\Database;
18+
use MongoDB\Driver\BulkWrite;
19+
use MongoDB\Driver\Command;
1720
use MongoDB\Driver\Exception\WriteException;
18-
use MongoDB\Driver\ReadPreference;
21+
use MongoDB\Driver\Manager;
22+
use MongoDB\Driver\Query;
1923
use MongoDB\Exception\DriverRuntimeException;
2024
use MongoDB\Exception\InvalidArgumentException as MongoInvalidArgumentException;
2125
use MongoDB\Exception\UnsupportedException;
@@ -44,21 +48,22 @@
4448
* @see https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit
4549
*
4650
* @author Joe Bennett <joe@assimtech.com>
51+
* @author Jérôme Tamarelle <jerome@tamarelle.net>
4752
*/
4853
class MongoDbStore implements PersistingStoreInterface
4954
{
5055
use ExpiringStoreTrait;
5156

52-
private Collection $collection;
53-
private Client $client;
57+
private Manager $manager;
58+
private string $namespace;
5459
private string $uri;
5560
private array $options;
5661
private float $initialTtl;
5762

5863
/**
59-
* @param Collection|Client|string $mongo An instance of a Collection or Client or URI @see https://docs.mongodb.com/manual/reference/connection-string/
60-
* @param array $options See below
61-
* @param float $initialTtl The expiration delay of locks in seconds
64+
* @param Collection|Client|Manager|string $mongo An instance of a Collection or Client or URI @see https://docs.mongodb.com/manual/reference/connection-string/
65+
* @param array $options See below
66+
* @param float $initialTtl The expiration delay of locks in seconds
6267
*
6368
* @throws InvalidArgumentException If required options are not provided
6469
* @throws InvalidTtlException When the initial ttl is not valid
@@ -88,7 +93,7 @@ class MongoDbStore implements PersistingStoreInterface
8893
* readPreference is primary for all queries.
8994
* @see https://docs.mongodb.com/manual/applications/replication/
9095
*/
91-
public function __construct(Collection|Client|string $mongo, array $options = [], float $initialTtl = 300.0)
96+
public function __construct(Collection|Database|Client|Manager|string $mongo, array $options = [], float $initialTtl = 300.0)
9297
{
9398
if (isset($options['gcProbablity'])) {
9499
trigger_deprecation('symfony/lock', '6.3', 'The "gcProbablity" option (notice the typo in its name) is deprecated in "%s"; use the "gcProbability" option instead.', __CLASS__);
@@ -108,21 +113,27 @@ public function __construct(Collection|Client|string $mongo, array $options = []
108113
$this->initialTtl = $initialTtl;
109114

110115
if ($mongo instanceof Collection) {
111-
$this->collection = $mongo;
116+
$this->options['database'] ??= $mongo->getDatabaseName();
117+
$this->options['collection'] ??= $mongo->getCollectionName();
118+
$this->manager = $mongo->getManager();
119+
} elseif ($mongo instanceof Database) {
120+
$this->options['database'] ??= $mongo->getDatabaseName();
121+
$this->manager = $mongo->getManager();
112122
} elseif ($mongo instanceof Client) {
113-
$this->client = $mongo;
123+
$this->manager = $mongo->getManager();
124+
} elseif ($mongo instanceof Manager) {
125+
$this->manager = $mongo;
114126
} else {
115127
$this->uri = $this->skimUri($mongo);
116128
}
117129

118-
if (!($mongo instanceof Collection)) {
119-
if (null === $this->options['database']) {
120-
throw new InvalidArgumentException(sprintf('"%s()" requires the "database" in the URI path or option.', __METHOD__));
121-
}
122-
if (null === $this->options['collection']) {
123-
throw new InvalidArgumentException(sprintf('"%s()" requires the "collection" in the URI querystring or option.', __METHOD__));
124-
}
130+
if (null === $this->options['database']) {
131+
throw new InvalidArgumentException(sprintf('"%s()" requires the "database" in the URI path or option.', __METHOD__));
132+
}
133+
if (null === $this->options['collection']) {
134+
throw new InvalidArgumentException(sprintf('"%s()" requires the "collection" in the URI querystring or option.', __METHOD__));
125135
}
136+
$this->namespace = $this->options['database'].'.'.$this->options['collection'];
126137

127138
if ($this->options['gcProbability'] < 0.0 || $this->options['gcProbability'] > 1.0) {
128139
throw new InvalidArgumentException(sprintf('"%s()" gcProbability must be a float from 0.0 to 1.0, "%f" given.', __METHOD__, $this->options['gcProbability']));
@@ -142,6 +153,10 @@ public function __construct(Collection|Client|string $mongo, array $options = []
142153
*/
143154
private function skimUri(string $uri): string
144155
{
156+
if (!str_starts_with($uri, 'mongodb://') && !str_starts_with($uri, 'mongodb+srv://')) {
157+
throw new InvalidArgumentException(sprintf('The given MongoDB Connection URI "%s" is invalid. Expecting "mongodb://" or "mongodb+srv://".', $uri));
158+
}
159+
145160
if (false === $parsedUrl = parse_url($uri)) {
146161
throw new InvalidArgumentException(sprintf('The given MongoDB Connection URI "%s" is invalid.', $uri));
147162
}
@@ -195,14 +210,19 @@ private function skimUri(string $uri): string
195210
*/
196211
public function createTtlIndex(int $expireAfterSeconds = 0)
197212
{
198-
$this->getCollection()->createIndex(
199-
[ // key
200-
'expires_at' => 1,
213+
$server = $this->getManager()->selectServer();
214+
$server->executeCommand($this->options['database'], new Command([
215+
'createIndexes' => $this->options['collection'],
216+
'indexes' => [
217+
[
218+
'key' => [
219+
'expires_at' => 1,
220+
],
221+
'name' => 'expires_at_1',
222+
'expireAfterSeconds' => $expireAfterSeconds,
223+
],
201224
],
202-
[ // options
203-
'expireAfterSeconds' => $expireAfterSeconds,
204-
]
205-
);
225+
]));
206226
}
207227

208228
/**
@@ -257,23 +277,35 @@ public function putOffExpiration(Key $key, float $ttl)
257277
*/
258278
public function delete(Key $key)
259279
{
260-
$this->getCollection()->deleteOne([ // filter
261-
'_id' => (string) $key,
262-
'token' => $this->getUniqueToken($key),
263-
]);
280+
$write = new BulkWrite();
281+
$write->delete(
282+
[
283+
'_id' => (string) $key,
284+
'token' => $this->getUniqueToken($key),
285+
],
286+
['limit' => 1]
287+
);
288+
289+
$this->getManager()->executeBulkWrite($this->namespace, $write);
264290
}
265291

266292
public function exists(Key $key): bool
267293
{
268-
return null !== $this->getCollection()->findOne([ // filter
269-
'_id' => (string) $key,
270-
'token' => $this->getUniqueToken($key),
271-
'expires_at' => [
272-
'$gt' => $this->createMongoDateTime(microtime(true)),
294+
$cursor = $this->manager->executeQuery($this->namespace, new Query(
295+
[
296+
'_id' => (string) $key,
297+
'token' => $this->getUniqueToken($key),
298+
'expires_at' => [
299+
'$gt' => $this->createMongoDateTime(microtime(true)),
300+
],
273301
],
274-
], [
275-
'readPreference' => new ReadPreference(\defined(ReadPreference::PRIMARY) ? ReadPreference::PRIMARY : ReadPreference::RP_PRIMARY),
276-
]);
302+
[
303+
'limit' => 1,
304+
'projection' => ['_id' => 1],
305+
]
306+
));
307+
308+
return [] !== $cursor->toArray();
277309
}
278310

279311
/**
@@ -286,8 +318,9 @@ private function upsert(Key $key, float $ttl): void
286318
$now = microtime(true);
287319
$token = $this->getUniqueToken($key);
288320

289-
$this->getCollection()->updateOne(
290-
[ // filter
321+
$write = new BulkWrite();
322+
$write->update(
323+
[
291324
'_id' => (string) $key,
292325
'$or' => [
293326
[
@@ -300,17 +333,19 @@ private function upsert(Key $key, float $ttl): void
300333
],
301334
],
302335
],
303-
[ // update
336+
[
304337
'$set' => [
305338
'_id' => (string) $key,
306339
'token' => $token,
307340
'expires_at' => $this->createMongoDateTime($now + $ttl),
308341
],
309342
],
310-
[ // options
343+
[
311344
'upsert' => true,
312345
]
313346
);
347+
348+
$this->getManager()->executeBulkWrite($this->namespace, $write);
314349
}
315350

316351
private function isDuplicateKeyException(WriteException $e): bool
@@ -326,20 +361,9 @@ private function isDuplicateKeyException(WriteException $e): bool
326361
return 11000 === $code;
327362
}
328363

329-
private function getCollection(): Collection
364+
private function getManager(): Manager
330365
{
331-
if (isset($this->collection)) {
332-
return $this->collection;
333-
}
334-
335-
$this->client ??= new Client($this->uri, $this->options['uriOptions'], $this->options['driverOptions']);
336-
337-
$this->collection = $this->client->selectCollection(
338-
$this->options['database'],
339-
$this->options['collection']
340-
);
341-
342-
return $this->collection;
366+
return $this->manager ??= new Manager($this->uri, $this->options['uriOptions'], $this->options['driverOptions']);
343367
}
344368

345369
/**
@@ -351,7 +375,7 @@ private function createMongoDateTime(float $seconds): UTCDateTime
351375
}
352376

353377
/**
354-
* Retrieves an unique token for the given key namespaced to this store.
378+
* Retrieves a unique token for the given key namespaced to this store.
355379
*
356380
* @param Key $key lock state container
357381
*/

Tests/Store/MongoDbStoreFactoryTest.php

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,34 @@
1212
namespace Symfony\Component\Lock\Tests\Store;
1313

1414
use MongoDB\Collection;
15-
use MongoDB\Client;
16-
use PHPUnit\Framework\SkippedTestSuiteError;
15+
use MongoDB\Driver\Manager;
1716
use PHPUnit\Framework\TestCase;
1817
use Symfony\Component\Lock\Store\MongoDbStore;
1918
use Symfony\Component\Lock\Store\StoreFactory;
2019

20+
require_once __DIR__.'/stubs/mongodb.php';
21+
2122
/**
2223
* @author Alexandre Daubois <alex.daubois@gmail.com>
2324
*
2425
* @requires extension mongodb
2526
*/
2627
class MongoDbStoreFactoryTest extends TestCase
2728
{
28-
public static function setupBeforeClass(): void
29-
{
30-
if (!class_exists(Client::class)) {
31-
throw new SkippedTestSuiteError('The mongodb/mongodb package is required.');
32-
}
33-
}
34-
3529
public function testCreateMongoDbCollectionStore()
3630
{
37-
$store = StoreFactory::createStore($this->createMock(Collection::class));
31+
$collection = $this->createMock(Collection::class);
32+
$collection->expects($this->once())
33+
->method('getManager')
34+
->willReturn(new Manager());
35+
$collection->expects($this->once())
36+
->method('getCollectionName')
37+
->willReturn('lock');
38+
$collection->expects($this->once())
39+
->method('getDatabaseName')
40+
->willReturn('test');
41+
42+
$store = StoreFactory::createStore($collection);
3843

3944
$this->assertInstanceOf(MongoDbStore::class, $store);
4045
}

0 commit comments

Comments
 (0)