diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index 8828bb8c3ad0b..92b4f5b84b822 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -72,8 +72,8 @@ class MongoDbStore implements BlockingStoreInterface * driverOptions: Array of driver options. [used when $mongo is a URI] * * When using a URI string: - * the database is determined from the "database" option, otherwise the uri's path is used. - * the collection is determined from the "collection" option, otherwise the uri's "collection" querystring parameter is used. + * The database is determined from the uri's path, otherwise the "database" option is used. To specify an alternate authentication database; "authSource" uriOption or querystring parameter must be used. + * The collection is determined from the uri's "collection" querystring parameter, otherwise the "collection" option is used. * * For example: mongodb://myuser:mypass@myhost/mydatabase?collection=mycollection * @@ -104,34 +104,20 @@ public function __construct($mongo, array $options = [], float $initialTtl = 300 if ($mongo instanceof Collection) { $this->collection = $mongo; } elseif ($mongo instanceof Client) { - if (null === $this->options['database']) { - throw new InvalidArgumentException(sprintf('"%s()" requires the "database" option when constructing with a "%s".', __METHOD__, Client::class)); - } - if (null === $this->options['collection']) { - throw new InvalidArgumentException(sprintf('"%s()" requires the "collection" option when constructing with a "%s".', __METHOD__, Client::class)); - } - $this->client = $mongo; } elseif (\is_string($mongo)) { - if (false === $parsedUrl = parse_url($mongo)) { - throw new InvalidArgumentException(sprintf('The given MongoDB Connection URI "%s" is invalid.', $mongo)); - } - $query = []; - if (isset($parsedUrl['query'])) { - parse_str($parsedUrl['query'], $query); - } - $this->options['collection'] = $query['collection'] ?? $this->options['collection'] ?? null; - $this->options['database'] = ltrim($parsedUrl['path'] ?? '', '/') ?: $this->options['database'] ?? null; + $this->uri = $this->skimUri($mongo); + } else { + throw new InvalidArgumentException(sprintf('"%s()" requires "%s" or "%s" or URI as first argument, "%s" given.', __METHOD__, Collection::class, Client::class, get_debug_type($mongo))); + } + + if (!($mongo instanceof Collection)) { if (null === $this->options['database']) { - throw new InvalidArgumentException(sprintf('"%s()" requires the "database" in the URI path or option when constructing with a URI.', __METHOD__)); + throw new InvalidArgumentException(sprintf('"%s()" requires the "database" in the URI path or option.', __METHOD__)); } if (null === $this->options['collection']) { - throw new InvalidArgumentException(sprintf('"%s()" requires the "collection" in the URI querystring or option when constructing with a URI.', __METHOD__)); + throw new InvalidArgumentException(sprintf('"%s()" requires the "collection" in the URI querystring or option.', __METHOD__)); } - - $this->uri = $mongo; - } else { - throw new InvalidArgumentException(sprintf('"%s()" requires "%s" or "%s" or URI as first argument, "%s" given.', __METHOD__, Collection::class, Client::class, get_debug_type($mongo))); } if ($this->options['gcProbablity'] < 0.0 || $this->options['gcProbablity'] > 1.0) { @@ -143,6 +129,45 @@ public function __construct($mongo, array $options = [], float $initialTtl = 300 } } + /** + * Extract default database and collection from given connection URI and remove collection querystring. + * + * Non-standard parameters are removed from the URI to improve libmongoc's re-use of connections. + * + * @see https://www.php.net/manual/en/mongodb.connection-handling.php + */ + private function skimUri(string $uri): string + { + if (false === $parsedUrl = parse_url($uri)) { + throw new InvalidArgumentException(sprintf('The given MongoDB Connection URI "%s" is invalid.', $uri)); + } + + $query = []; + if (isset($parsedUrl['query'])) { + parse_str($parsedUrl['query'], $query); + } + + if (isset($query['collection'])) { + $this->options['collection'] = $query['collection']; + $queryStringPos = strrpos($uri, $parsedUrl['query']); + unset($query['collection']); + $prefix = substr($uri, 0, $queryStringPos); + $newQuery = http_build_query($query, '', '&', PHP_QUERY_RFC3986); + if (empty($newQuery)) { + $prefix = rtrim($prefix, '?'); + } + $suffix = substr($uri, $queryStringPos + \strlen($parsedUrl['query'])); + $uri = $prefix.$newQuery.$suffix; + } + + $pathDb = ltrim($parsedUrl['path'] ?? '', '/') ?: null; + if (null !== $pathDb) { + $this->options['database'] = $pathDb; + } + + return $uri; + } + /** * Creates a TTL index to automatically remove expired locks. * diff --git a/src/Symfony/Component/Lock/Tests/Store/MongoDbStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/MongoDbStoreTest.php index 34dd1182c9da8..a3e0c9f0e3e7d 100644 --- a/src/Symfony/Component/Lock/Tests/Store/MongoDbStoreTest.php +++ b/src/Symfony/Component/Lock/Tests/Store/MongoDbStoreTest.php @@ -121,17 +121,22 @@ public function provideConstructorArgs() yield ['mongodb://localhost/', ['database' => 'test', 'collection' => 'lock']]; } - public function testDsnPrecedence() + public function testUriPrecedence() { $client = self::getMongoClient(); - $store = new MongoDbStore('mongodb://localhost/test_dsn?collection=lock_dns', ['collection' => 'lock_option', 'database' => 'test_option']); - $r = new \ReflectionObject($store); - $p = $r->getProperty('options'); - $p->setAccessible(true); - $options = $p->getValue($store); - $this->assertSame('lock_dns', $options['collection']); - $this->assertSame('test_dsn', $options['database']); + $store = new MongoDbStore('mongodb://localhost/test_uri?collection=lock_uri', [ + 'database' => 'test_option', + 'collection' => 'lock_option', + ]); + $storeReflection = new \ReflectionObject($store); + + $optionsProperty = $storeReflection->getProperty('options'); + $optionsProperty->setAccessible(true); + $options = $optionsProperty->getValue($store); + + $this->assertSame('test_uri', $options['database']); + $this->assertSame('lock_uri', $options['collection']); } /** @@ -154,4 +159,33 @@ public function provideInvalidConstructorArgs() yield ['mongodb://localhost/test', []]; yield ['mongodb://localhost/', []]; } + + /** + * @dataProvider provideUriCollectionStripArgs + */ + public function testUriCollectionStrip(string $uri, array $options, string $driverUri) + { + $client = self::getMongoClient(); + + $store = new MongoDbStore($uri, $options); + $storeReflection = new \ReflectionObject($store); + + $uriProperty = $storeReflection->getProperty('uri'); + $uriProperty->setAccessible(true); + $uri = $uriProperty->getValue($store); + $this->assertSame($driverUri, $uri); + } + + public function provideUriCollectionStripArgs() + { + yield ['mongodb://localhost/?collection=lock', ['database' => 'test'], 'mongodb://localhost/']; + yield ['mongodb://localhost/', ['database' => 'test', 'collection' => 'lock'], 'mongodb://localhost/']; + yield ['mongodb://localhost/test?collection=lock', [], 'mongodb://localhost/test']; + yield ['mongodb://localhost/test', ['collection' => 'lock'], 'mongodb://localhost/test']; + + yield ['mongodb://localhost/?collection=lock&replicaSet=repl', ['database' => 'test'], 'mongodb://localhost/?replicaSet=repl']; + yield ['mongodb://localhost/?replicaSet=repl', ['database' => 'test', 'collection' => 'lock'], 'mongodb://localhost/?replicaSet=repl']; + yield ['mongodb://localhost/test?collection=lock&replicaSet=repl', [], 'mongodb://localhost/test?replicaSet=repl']; + yield ['mongodb://localhost/test?replicaSet=repl', ['collection' => 'lock'], 'mongodb://localhost/test?replicaSet=repl']; + } }