From 74cf0c088e90a39a2992272351e60caee76779a4 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 6 Dec 2023 21:45:27 +0100 Subject: [PATCH 01/18] Run CI for every PR (#9) --- .github/workflows/ci.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6f5b591..03200f6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,10 +1,7 @@ name: Test on: - pull_request: - branches: - - "*.*" - - "feature/*" + pull_request: ~ push: branches: - "*.*" From af4da3324fd453b6ba89071ac14a40e221aa912c Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Sat, 9 Dec 2023 09:43:01 +0100 Subject: [PATCH 02/18] Add PHP `8.3` to CI matrix (#8) Co-authored-by: Andreas Braun --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 03200f6..5e68d91 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -44,6 +44,7 @@ jobs: php-version: - '8.1' - '8.2' + - '8.3' dependencies: - 'highest' - 'lowest' From 050062ec3d7535891359dd33c5283f18bac927e5 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Sat, 9 Dec 2023 09:47:26 +0100 Subject: [PATCH 03/18] Use snake_case for configuration options (#11) --- README.md | 6 ++--- src/DependencyInjection/Configuration.php | 4 ++-- src/DependencyInjection/MongoDBExtension.php | 4 ++-- .../DependencyInjection/ConfigurationTest.php | 24 +++++++++---------- .../MongoDBExtensionTest.php | 4 ++-- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 27e9c5a..45cfba4 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,11 @@ mongodb: clients: default: uri: '%env(MONGODB_URI)%' - uriOptions: #... - driverOptions: #... + uri_options: #... + driver_options: #... ``` -The `uriOptions` and `driverOptions` are passed directly to the underlying MongoDB driver. +The `uri_options` and `driver_options` are passed directly to the underlying MongoDB driver. See the [documentation](https://www.php.net/manual/en/mongodb-driver-manager.construct.php) for available options. If you want to configure multiple clients, you can do so by adding additional clients to the configuration: diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index a15e512..2e106a5 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -32,11 +32,11 @@ public function getConfigTreeBuilder(): TreeBuilder ->scalarNode('uri') ->info('MongoDB connection string') ->end() - ->arrayNode('uriOptions') + ->arrayNode('uri_options') ->info('Additional connection string options') ->variablePrototype()->end() ->end() - ->arrayNode('driverOptions') + ->arrayNode('driver_options') ->info('Driver-specific options') ->variablePrototype()->end() ->end() diff --git a/src/DependencyInjection/MongoDBExtension.php b/src/DependencyInjection/MongoDBExtension.php index 4be5e36..a8b06e7 100644 --- a/src/DependencyInjection/MongoDBExtension.php +++ b/src/DependencyInjection/MongoDBExtension.php @@ -68,8 +68,8 @@ private function createClients(string $defaultClient, array $clients, ContainerB $clientDefinition = clone $clientPrototype; $clientDefinition->setArgument('$uri', $configuration['uri']); - $clientDefinition->setArgument('$uriOptions', $configuration['uriOptions'] ?? []); - $clientDefinition->setArgument('$driverOptions', $configuration['driverOptions'] ?? []); + $clientDefinition->setArgument('$uriOptions', $configuration['uri_options'] ?? []); + $clientDefinition->setArgument('$driverOptions', $configuration['driver_options'] ?? []); $container->setDefinition($serviceId, $clientDefinition); diff --git a/tests/Unit/DependencyInjection/ConfigurationTest.php b/tests/Unit/DependencyInjection/ConfigurationTest.php index 9a46878..6cfd26c 100644 --- a/tests/Unit/DependencyInjection/ConfigurationTest.php +++ b/tests/Unit/DependencyInjection/ConfigurationTest.php @@ -38,12 +38,12 @@ public function testProcess(): void [ 'id' => 'default', 'uri' => 'mongodb://localhost:27017', - 'uriOptions' => ['readPreference' => 'primary'], + 'uri_options' => ['readPreference' => 'primary'], ], [ 'id' => 'secondary', 'uri' => 'mongodb://localhost:27018', - 'driverOptions' => ['serverApi' => new ServerApi((string) ServerApi::V1)], + 'driver_options' => ['serverApi' => new ServerApi((string) ServerApi::V1)], ], ], ], @@ -58,13 +58,13 @@ public function testProcess(): void $this->assertArrayHasKey('default', $clients); $this->assertSame('mongodb://localhost:27017', $clients['default']['uri']); - $this->assertSame(['readPreference' => 'primary'], $clients['default']['uriOptions']); - $this->assertSame([], $clients['default']['driverOptions']); + $this->assertSame(['readPreference' => 'primary'], $clients['default']['uri_options']); + $this->assertSame([], $clients['default']['driver_options']); $this->assertArrayHasKey('secondary', $clients); $this->assertSame('mongodb://localhost:27018', $clients['secondary']['uri']); - $this->assertSame([], $clients['secondary']['uriOptions']); - $this->assertEquals(['serverApi' => new ServerApi((string) ServerApi::V1)], $clients['secondary']['driverOptions']); + $this->assertSame([], $clients['secondary']['uri_options']); + $this->assertEquals(['serverApi' => new ServerApi((string) ServerApi::V1)], $clients['secondary']['driver_options']); } public function testProcessWithYamlFile(): void @@ -74,11 +74,11 @@ public function testProcessWithYamlFile(): void clients: default: uri: mongodb://localhost:27017 - uriOptions: + uri_options: readPreference: primary secondary: uri: mongodb://localhost:27018 - driverOptions: + driver_options: serverApi: v1 YAML); @@ -91,13 +91,13 @@ public function testProcessWithYamlFile(): void $this->assertArrayHasKey('default', $clients); $this->assertSame('mongodb://localhost:27017', $clients['default']['uri']); - $this->assertSame(['readPreference' => 'primary'], $clients['default']['uriOptions']); - $this->assertSame([], $clients['default']['driverOptions']); + $this->assertSame(['readPreference' => 'primary'], $clients['default']['uri_options']); + $this->assertSame([], $clients['default']['driver_options']); $this->assertArrayHasKey('secondary', $clients); $this->assertSame('mongodb://localhost:27018', $clients['secondary']['uri']); - $this->assertSame([], $clients['secondary']['uriOptions']); - $this->assertSame(['serverApi' => 'v1'], $clients['secondary']['driverOptions']); + $this->assertSame([], $clients['secondary']['uri_options']); + $this->assertSame(['serverApi' => 'v1'], $clients['secondary']['driver_options']); } public function testProcessWithYamlFileWithoutUriKey(): void diff --git a/tests/Unit/DependencyInjection/MongoDBExtensionTest.php b/tests/Unit/DependencyInjection/MongoDBExtensionTest.php index 4a94d4a..c0cb90f 100644 --- a/tests/Unit/DependencyInjection/MongoDBExtensionTest.php +++ b/tests/Unit/DependencyInjection/MongoDBExtensionTest.php @@ -75,12 +75,12 @@ public function testLoadWithMultipleClients(): void [ 'id' => 'default', 'uri' => 'mongodb://localhost:27017', - 'uriOptions' => ['readPreference' => 'primary'], + 'uri_options' => ['readPreference' => 'primary'], ], [ 'id' => 'secondary', 'uri' => 'mongodb://localhost:27018', - 'driverOptions' => ['serverApi' => new ServerApi((string) ServerApi::V1)], + 'driver_options' => ['serverApi' => new ServerApi((string) ServerApi::V1)], ], ], ], From 2d3784afbe00c9e1d2bad1caedaa0ea18a9d7de3 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Sat, 9 Dec 2023 12:51:38 +0100 Subject: [PATCH 04/18] Remove parameter name usage for AutowireDatabase (#13) * Remove parameter name usage for AutowireDatabase * Fix documentation * Update readme --------- Co-authored-by: Oskar Stark --- README.md | 58 ++++++++++++------- src/Attribute/AutowireDatabase.php | 16 +++-- .../Controller/AutowireDatabaseController.php | 6 +- tests/Unit/Attribute/AutowireDatabaseTest.php | 4 +- 4 files changed, 53 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 45cfba4..47cee05 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ mongodb: clients: default: uri: '%env(MONGODB_URI)%' + default_database: #... uri_options: #... driver_options: #... ``` @@ -96,7 +97,8 @@ class MyService } ``` -If you register multiple clients, you can autowire them by name + `Client` suffix: +If you register multiple clients, you can autowire them by using the client name with a `Client` suffix as parameter +name: ```php use MongoDB\Bundle\Attribute\AutowireClient; @@ -105,6 +107,7 @@ use MongoDB\Client; class MyService { public function __construct( + // Will autowire the client with the id "second" private Client $secondClient, ) {} } @@ -119,7 +122,7 @@ use MongoDB\Client; class MyService { public function __construct( - #[AutowireClient('second')] + #[AutowireClient('second')] private Client $client, ) {} } @@ -127,8 +130,8 @@ class MyService ## Database and Collection Usage -The client service provides access to databases and collections. You can access a database by calling the `selectDatabase` -method, passing the database name and potential options: +The client service provides access to databases and collections. You can access a database by calling the +`selectDatabase` method, passing the database name and potential options: ```php use MongoDB\Client; @@ -146,7 +149,7 @@ class MyService } ``` -An alternative to this is using the `AutowireDatabase` attribute, referencing the database name: +An alternative to this is using the `#[AutowireDatabase]` attribute, referencing the database name: ```php use MongoDB\Bundle\Attribute\AutowireDatabase; @@ -161,21 +164,9 @@ class MyService } ``` -You can also omit the `database` option if the property name matches the database name. -In the following example the database name is `myDatabase`, inferred from the property name: - -```php -use MongoDB\Bundle\Attribute\AutowireCollection; -use MongoDB\Collection; - -class MyService -{ - public function __construct( - #[AutowireCollection()] - private Collection $myDatabase, - ) {} -} -``` +If you don't specify a database name in the attribute, the default database name (specified in the `default_database` +configuration option) will be used. If you did not define a default database, the database name has to be specified in +the attribute. If you have more than one client defined, you can also reference the client: @@ -193,7 +184,7 @@ class MyService ``` To inject a collection, you can either call the `selectCollection` method on a `Client` or `Database` instance. -For convenience, the `AutowireCollection` attribute provides a quicker alternative: +For convenience, the `#[AutowireCollection]` attribute provides a quicker alternative: ```php use MongoDB\Bundle\Attribute\AutowireCollection; @@ -230,6 +221,7 @@ class MyService ``` If you have more than one client defined, you can also reference the client: + ```php use MongoDB\Bundle\Attribute\AutowireCollection; use MongoDB\Collection; @@ -245,3 +237,27 @@ class MyService ) {} } ``` + +By specifiying the `default_database` option in the configuration, you can omit the `database` option in the +`AutowireCollection` attribute: + +```diff +mongodb: + clients: + default: + uri: '%env(MONGODB_URI)%' ++ default_database: 'myDatabase' +``` + +```php +use MongoDB\Bundle\Attribute\AutowireCollection; +use MongoDB\Collection; + +class MyService +{ + public function __construct( + #[AutowireCollection] + private Collection $myCollection, + ) {} +} +``` diff --git a/src/Attribute/AutowireDatabase.php b/src/Attribute/AutowireDatabase.php index 1c94f35..c46b17a 100644 --- a/src/Attribute/AutowireDatabase.php +++ b/src/Attribute/AutowireDatabase.php @@ -30,6 +30,7 @@ use Symfony\Component\DependencyInjection\Reference; use function is_string; +use function sprintf; /** * Autowires a MongoDB database. @@ -37,18 +38,20 @@ #[Attribute(Attribute::TARGET_PARAMETER)] final class AutowireDatabase extends AutowireCallable { + private readonly string $serviceId; + public function __construct( private readonly ?string $database = null, ?string $client = null, private readonly array $options = [], bool|string $lazy = false, ) { - $callable = $client === null - ? [new Reference(Client::class), 'selectDatabase'] - : [new Reference(MongoDBExtension::createClientServiceId($client)), 'selectDatabase']; + $this->serviceId = $client === null + ? Client::class + : MongoDBExtension::createClientServiceId($client); parent::__construct( - callable: $callable, + callable: [new Reference($this->serviceId), 'selectDatabase'], lazy: $lazy, ); } @@ -57,7 +60,10 @@ public function buildDefinition(mixed $value, ?string $type, ReflectionParameter { return (new Definition(is_string($this->lazy) ? $this->lazy : ($type ?: Database::class))) ->setFactory($value) - ->setArguments([$this->database ?? $parameter->getName(), $this->options]) + ->setArguments([ + $this->database ?? sprintf('%%%s.default_database%%', $this->serviceId), + $this->options, + ]) ->setLazy($this->lazy); } } diff --git a/tests/TestApplication/src/Controller/AutowireDatabaseController.php b/tests/TestApplication/src/Controller/AutowireDatabaseController.php index 9529ec6..682e0a4 100644 --- a/tests/TestApplication/src/Controller/AutowireDatabaseController.php +++ b/tests/TestApplication/src/Controller/AutowireDatabaseController.php @@ -46,10 +46,10 @@ public function withoutArguments( /** @see AutowireClientTest::testWithCustomClientSetViaOptions() */ #[Route('/with-custom-client')] public function withCustomClient( - #[AutowireDatabase(client: FunctionalTestCase::CLIENT_ID_SECONDARY)] - Database $google, + #[AutowireDatabase(database: 'google', client: FunctionalTestCase::CLIENT_ID_SECONDARY)] + Database $database, ): JsonResponse { - $this->insertDocumentForDatabase($google, FunctionalTestCase::COLLECTION_USERS); + $this->insertDocumentForDatabase($database, FunctionalTestCase::COLLECTION_USERS); return new JsonResponse(); } diff --git a/tests/Unit/Attribute/AutowireDatabaseTest.php b/tests/Unit/Attribute/AutowireDatabaseTest.php index 4ccab63..2f097e2 100644 --- a/tests/Unit/Attribute/AutowireDatabaseTest.php +++ b/tests/Unit/Attribute/AutowireDatabaseTest.php @@ -48,7 +48,7 @@ static function (Database $mydb): void { $this->assertSame(Database::class, $definition->getClass()); $this->assertEquals($autowire->value, $definition->getFactory()); - $this->assertSame('mydb', $definition->getArgument(0)); + $this->assertSame('%MongoDB\Client.default_database%', $definition->getArgument(0)); } public function testDatabase(): void @@ -98,7 +98,7 @@ static function (Database $mydb): void { $this->assertSame(Database::class, $definition->getClass()); $this->assertEquals($autowire->value, $definition->getFactory()); - $this->assertSame('mydb', $definition->getArgument(0)); + $this->assertSame('%mongodb.client.default.default_database%', $definition->getArgument(0)); $this->assertSame(['foo' => 'bar'], $definition->getArgument(1)); } } From 74668e0de6207ed473afa36e80373a1c2efc7b7c Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Sat, 9 Dec 2023 14:58:57 +0100 Subject: [PATCH 05/18] Allow specifying options in AutowireDatabase and AutowireCollection (#14) * Resolve service references for codec collection options * Add codec option to AutowireCollection * Document codec usage * Bump MongoDB dependency to 1.17 * Allow passing all options to AutowireCollection and AutowireDatabase * Use parameter for type maps --- README.md | 31 ++++++- composer.json | 2 +- src/Attribute/AutowireCollection.php | 28 +++++- src/Attribute/AutowireDatabase.php | 27 +++++- tests/Unit/Attribute/AttributeTestCase.php | 87 +++++++++++++++++++ .../Unit/Attribute/AutowireCollectionTest.php | 31 +++++-- tests/Unit/Attribute/AutowireDatabaseTest.php | 31 +++++-- 7 files changed, 219 insertions(+), 18 deletions(-) create mode 100644 tests/Unit/Attribute/AttributeTestCase.php diff --git a/README.md b/README.md index 47cee05..2cf4295 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ class MyService } ``` -## Database and Collection Usage +## Database Usage The client service provides access to databases and collections. You can access a database by calling the `selectDatabase` method, passing the database name and potential options: @@ -183,6 +183,8 @@ class MyService } ``` +## Collection Usage + To inject a collection, you can either call the `selectCollection` method on a `Client` or `Database` instance. For convenience, the `#[AutowireCollection]` attribute provides a quicker alternative: @@ -261,3 +263,30 @@ class MyService ) {} } ``` + +## Specifying options + +When using the `AutowireDatabase` or `AutowireCollection` attributes, you can specify additional options for the +resulting instances. You can pass the following options: +|| Option || Accepted type || +| `codec` | `DocumentCodec` instance | +| `typeMap`| `array` containing type map information | +| `readPreference` | `MongoDB\Driver\ReadPreference` instance | +| `writeConcern` | `MongoDB\Driver\writeConcern` instance | +| `readConcern` | `MongoDB\Driver\ReadConcern` instance | + +In addition to passing an instance, you can also pass a service reference by specifying a string for the given option: + +```php +use MongoDB\Bundle\Attribute\AutowireCollection; +use MongoDB\Collection; +use MongoDB\Driver\ReadPreference; + +class MyService +{ + public function __construct( + #[AutowireCollection(codec: Codec::class, readPreference: new ReadPreference('secondary'))] + private Collection $myCollection, + ) {} +} +``` diff --git a/composer.json b/composer.json index 1a73378..72f42e8 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ ], "require": { "php": ">=8.1", - "mongodb/mongodb": "^1.16", + "mongodb/mongodb": "^1.17", "symfony/config": "^6.3 || ^7.0", "symfony/console": "^6.3 || ^7.0", "symfony/dependency-injection": "^6.3.5 || ^7.0", diff --git a/src/Attribute/AutowireCollection.php b/src/Attribute/AutowireCollection.php index 957d77b..6f99bb4 100644 --- a/src/Attribute/AutowireCollection.php +++ b/src/Attribute/AutowireCollection.php @@ -23,13 +23,18 @@ use Attribute; use MongoDB\Bundle\DependencyInjection\MongoDBExtension; use MongoDB\Client; +use MongoDB\Codec\DocumentCodec; use MongoDB\Collection; +use MongoDB\Driver\ReadConcern; +use MongoDB\Driver\ReadPreference; +use MongoDB\Driver\WriteConcern; use ReflectionParameter; use Symfony\Component\DependencyInjection\Attribute\AutowireCallable; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use function is_string; +use function ltrim; use function sprintf; /** @@ -44,7 +49,11 @@ public function __construct( private readonly ?string $collection = null, private readonly ?string $database = null, ?string $client = null, - private readonly array $options = [], + private readonly string|DocumentCodec|null $codec = null, + private readonly string|array|null $typeMap = null, + private readonly string|ReadPreference|null $readPreference = null, + private readonly string|WriteConcern|null $writeConcern = null, + private readonly string|ReadConcern|null $readConcern = null, bool|string $lazy = false, ) { $this->serviceId = $client === null @@ -59,12 +68,27 @@ public function __construct( public function buildDefinition(mixed $value, ?string $type, ReflectionParameter $parameter): Definition { + $options = []; + foreach (['codec', 'typeMap', 'readPreference', 'writeConcern', 'readConcern'] as $option) { + $optionValue = $this->$option; + if ($optionValue === null) { + continue; + } + + // If a string was given, it may be a service ID or parameter. Handle it accordingly + if (is_string($optionValue)) { + $optionValue = $option === 'typeMap' ? sprintf('%%%s%%', $optionValue) : new Reference($optionValue); + } + + $options[$option] = $optionValue; + } + return (new Definition(is_string($this->lazy) ? $this->lazy : ($type ?: Collection::class))) ->setFactory($value) ->setArguments([ $this->database ?? sprintf('%%%s.default_database%%', $this->serviceId), $this->collection ?? $parameter->getName(), - $this->options, + $options, ]) ->setLazy($this->lazy); } diff --git a/src/Attribute/AutowireDatabase.php b/src/Attribute/AutowireDatabase.php index c46b17a..0b9b56b 100644 --- a/src/Attribute/AutowireDatabase.php +++ b/src/Attribute/AutowireDatabase.php @@ -23,7 +23,11 @@ use Attribute; use MongoDB\Bundle\DependencyInjection\MongoDBExtension; use MongoDB\Client; +use MongoDB\Codec\DocumentCodec; use MongoDB\Database; +use MongoDB\Driver\ReadConcern; +use MongoDB\Driver\ReadPreference; +use MongoDB\Driver\WriteConcern; use ReflectionParameter; use Symfony\Component\DependencyInjection\Attribute\AutowireCallable; use Symfony\Component\DependencyInjection\Definition; @@ -43,7 +47,11 @@ final class AutowireDatabase extends AutowireCallable public function __construct( private readonly ?string $database = null, ?string $client = null, - private readonly array $options = [], + private readonly string|DocumentCodec|null $codec = null, + private readonly string|array|null $typeMap = null, + private readonly string|ReadPreference|null $readPreference = null, + private readonly string|WriteConcern|null $writeConcern = null, + private readonly string|ReadConcern|null $readConcern = null, bool|string $lazy = false, ) { $this->serviceId = $client === null @@ -58,11 +66,26 @@ public function __construct( public function buildDefinition(mixed $value, ?string $type, ReflectionParameter $parameter): Definition { + $options = []; + foreach (['codec', 'typeMap', 'readPreference', 'writeConcern', 'readConcern'] as $option) { + $optionValue = $this->$option; + if ($optionValue === null) { + continue; + } + + // If a string was given, it may be a service ID or parameter. Handle it accordingly + if (is_string($optionValue)) { + $optionValue = $option === 'typeMap' ? sprintf('%%%s%%', $optionValue) : new Reference($optionValue); + } + + $options[$option] = $optionValue; + } + return (new Definition(is_string($this->lazy) ? $this->lazy : ($type ?: Database::class))) ->setFactory($value) ->setArguments([ $this->database ?? sprintf('%%%s.default_database%%', $this->serviceId), - $this->options, + $options, ]) ->setLazy($this->lazy); } diff --git a/tests/Unit/Attribute/AttributeTestCase.php b/tests/Unit/Attribute/AttributeTestCase.php new file mode 100644 index 0000000..e7ee39e --- /dev/null +++ b/tests/Unit/Attribute/AttributeTestCase.php @@ -0,0 +1,87 @@ + $codec, + 'writeConcern' => new WriteConcern(0), + 'readConcern' => new ReadConcern('majority'), + 'readPreference' => new ReadPreference('primary'), + ]; + + foreach ($options as $option => $value) { + yield sprintf('%s option: null', $option) => [ + 'attributeArguments' => [$option => null], + 'expectedOptions' => [], + ]; + + yield sprintf('%s option: instance', $option) => [ + 'attributeArguments' => [$option => $value], + 'expectedOptions' => [$option => $value], + ]; + + yield sprintf('%s option: reference', $option) => [ + 'attributeArguments' => [$option => sprintf('%s_service', $option)], + 'expectedOptions' => [$option => new Reference(sprintf('%s_service', $option))], + ]; + } + + // Type map + yield 'typeMap option: null' => [ + 'attributeArguments' => ['typeMap' => null], + 'expectedOptions' => [], + ]; + + yield 'typeMap option: value' => [ + 'attributeArguments' => ['typeMap' => ['root' => 'bson']], + 'expectedOptions' => ['typeMap' => ['root' => 'bson']], + ]; + + yield 'typeMap option: parameter' => [ + 'attributeArguments' => ['typeMap' => 'default_typeMap'], + 'expectedOptions' => ['typeMap' => '%default_typeMap%'], + ]; + } +} diff --git a/tests/Unit/Attribute/AutowireCollectionTest.php b/tests/Unit/Attribute/AutowireCollectionTest.php index 5f62a79..006a73d 100644 --- a/tests/Unit/Attribute/AutowireCollectionTest.php +++ b/tests/Unit/Attribute/AutowireCollectionTest.php @@ -23,14 +23,13 @@ use MongoDB\Bundle\Attribute\AutowireCollection; use MongoDB\Client; use MongoDB\Collection; -use PHPUnit\Framework\TestCase; use ReflectionParameter; use Symfony\Component\DependencyInjection\Reference; use function sprintf; /** @covers \MongoDB\Bundle\Attribute\AutowireCollection */ -final class AutowireCollectionTest extends TestCase +final class AutowireCollectionTest extends AttributeTestCase { public function testMinimal(): void { @@ -61,7 +60,6 @@ public function testCollection(): void collection: 'test', database: 'mydb', client: 'default', - options: ['foo' => 'bar'], ); $this->assertEquals([new Reference('mongodb.client.default'), 'selectCollection'], $autowire->value); @@ -80,7 +78,7 @@ static function (Collection $collection): void { $this->assertEquals($autowire->value, $definition->getFactory()); $this->assertSame('mydb', $definition->getArgument(0)); $this->assertSame('test', $definition->getArgument(1)); - $this->assertSame(['foo' => 'bar'], $definition->getArgument(2)); + $this->assertSame([], $definition->getArgument(2)); } public function testWithoutCollection(): void @@ -88,7 +86,6 @@ public function testWithoutCollection(): void $autowire = new AutowireCollection( database: 'mydb', client: 'default', - options: ['foo' => 'bar'], ); $this->assertEquals([new Reference('mongodb.client.default'), 'selectCollection'], $autowire->value); @@ -107,6 +104,28 @@ static function (Collection $priceReports): void { $this->assertEquals($autowire->value, $definition->getFactory()); $this->assertSame('mydb', $definition->getArgument(0)); $this->assertSame('priceReports', $definition->getArgument(1)); - $this->assertSame(['foo' => 'bar'], $definition->getArgument(2)); + $this->assertSame([], $definition->getArgument(2)); + } + + /** @dataProvider provideOptions */ + public function testWithOptions(array $attributeArguments, array $expectedOptions): void + { + $autowire = new AutowireCollection( + ...$attributeArguments, + database: 'mydb', + client: 'default', + ); + + $definition = $autowire->buildDefinition( + value: $autowire->value, + type: Collection::class, + parameter: new ReflectionParameter( + static function (Collection $priceReports): void { + }, + 'priceReports', + ), + ); + + $this->assertEquals($expectedOptions, $definition->getArgument(2)); } } diff --git a/tests/Unit/Attribute/AutowireDatabaseTest.php b/tests/Unit/Attribute/AutowireDatabaseTest.php index 2f097e2..1958e20 100644 --- a/tests/Unit/Attribute/AutowireDatabaseTest.php +++ b/tests/Unit/Attribute/AutowireDatabaseTest.php @@ -22,13 +22,13 @@ use MongoDB\Bundle\Attribute\AutowireDatabase; use MongoDB\Client; +use MongoDB\Collection; use MongoDB\Database; -use PHPUnit\Framework\TestCase; use ReflectionParameter; use Symfony\Component\DependencyInjection\Reference; /** @covers \MongoDB\Bundle\Attribute\AutowireDatabase */ -final class AutowireDatabaseTest extends TestCase +final class AutowireDatabaseTest extends AttributeTestCase { public function testMinimal(): void { @@ -56,7 +56,6 @@ public function testDatabase(): void $autowire = new AutowireDatabase( database: 'mydb', client: 'default', - options: ['foo' => 'bar'], ); $this->assertEquals([new Reference('mongodb.client.default'), 'selectDatabase'], $autowire->value); @@ -74,14 +73,13 @@ static function (Database $db): void { $this->assertSame(Database::class, $definition->getClass()); $this->assertEquals($autowire->value, $definition->getFactory()); $this->assertSame('mydb', $definition->getArgument(0)); - $this->assertSame(['foo' => 'bar'], $definition->getArgument(1)); + $this->assertSame([], $definition->getArgument(1)); } public function testWithoutDatabase(): void { $autowire = new AutowireDatabase( client: 'default', - options: ['foo' => 'bar'], ); $this->assertEquals([new Reference('mongodb.client.default'), 'selectDatabase'], $autowire->value); @@ -99,6 +97,27 @@ static function (Database $mydb): void { $this->assertSame(Database::class, $definition->getClass()); $this->assertEquals($autowire->value, $definition->getFactory()); $this->assertSame('%mongodb.client.default.default_database%', $definition->getArgument(0)); - $this->assertSame(['foo' => 'bar'], $definition->getArgument(1)); + $this->assertSame([], $definition->getArgument(1)); + } + + /** @dataProvider provideOptions */ + public function testWithOptions(array $attributeArguments, array $expectedOptions): void + { + $autowire = new AutowireDatabase( + ...$attributeArguments, + client: 'default', + ); + + $definition = $autowire->buildDefinition( + value: $autowire->value, + type: Collection::class, + parameter: new ReflectionParameter( + static function (Database $database): void { + }, + 'database', + ), + ); + + $this->assertEquals($expectedOptions, $definition->getArgument(1)); } } From af537c2a5c002938b58f2af3632bdbcee51ac2ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sat, 9 Dec 2023 16:01:09 +0100 Subject: [PATCH 06/18] Add MongoDB profiler (#4) --- composer.json | 2 + config/services.php | 15 +- src/DataCollector/CommandEventCollector.php | 11 + src/DataCollector/DriverEventSubscriber.php | 116 +++++++++ src/DataCollector/MongoDBDataCollector.php | 145 +++++++++++ .../Compiler/DataCollectorPass.php | 45 ++++ src/DependencyInjection/MongoDBExtension.php | 20 +- src/MongoDBBundle.php | 6 + templates/Collector/icon-minus-square.svg | 1 + templates/Collector/icon-plus-square.svg | 1 + templates/Collector/mongodb.html.twig | 236 ++++++++++++++++++ templates/Collector/mongodb.svg | 3 + .../DriverEventSubscriberTest.php | 103 ++++++++ .../MongoDBDataCollectorTest.php | 105 ++++++++ .../MongoDBExtensionTest.php | 45 +++- 15 files changed, 838 insertions(+), 16 deletions(-) create mode 100644 src/DataCollector/CommandEventCollector.php create mode 100644 src/DataCollector/DriverEventSubscriber.php create mode 100644 src/DataCollector/MongoDBDataCollector.php create mode 100644 src/DependencyInjection/Compiler/DataCollectorPass.php create mode 100644 templates/Collector/icon-minus-square.svg create mode 100644 templates/Collector/icon-plus-square.svg create mode 100644 templates/Collector/mongodb.html.twig create mode 100644 templates/Collector/mongodb.svg create mode 100644 tests/Functional/DataCollector/DriverEventSubscriberTest.php create mode 100644 tests/Functional/DataCollector/MongoDBDataCollectorTest.php diff --git a/composer.json b/composer.json index 72f42e8..c2f1ffa 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,9 @@ "symfony/filesystem": "^6.3 || ^7.0", "symfony/framework-bundle": "^6.3.5 || ^7.0", "symfony/phpunit-bridge": "~6.3.10 || ^6.4.1 || ^7.0.1", + "symfony/stopwatch": "^6.3 || ^7.0", "symfony/yaml": "^6.3 || ^7.0", + "symfony/web-profiler-bundle": "^6.3 || ^7.0", "zenstruck/browser": "^1.6" }, "scripts": { diff --git a/config/services.php b/config/services.php index db2c3f6..4b6a596 100644 --- a/config/services.php +++ b/config/services.php @@ -21,6 +21,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use MongoDB\Bundle\Command\DebugCommand; +use MongoDB\Bundle\DataCollector\MongoDBDataCollector; use MongoDB\Client; return static function (ContainerConfigurator $container): void { @@ -38,9 +39,19 @@ ->tag('console.command'); $services - ->set('mongodb.prototype.client', Client::class) + ->set('mongodb.abstract.client', Client::class) ->arg('$uri', abstract_arg('Should be defined by pass')) ->arg('$uriOptions', abstract_arg('Should be defined by pass')) ->arg('$driverOptions', abstract_arg('Should be defined by pass')) - ->tag('mongodb.client'); + ->abstract(); + + $services + ->set('mongodb.data_collector', MongoDBDataCollector::class) + ->arg('$stopwatch', service('debug.stopwatch')->nullOnInvalid()) + ->arg('$clients', tagged_iterator('mongodb.client', 'name')) + ->tag('data_collector', [ + 'template' => '@MongoDB/Collector/mongodb.html.twig', + 'id' => 'mongodb', + 'priority' => 250, + ]); }; diff --git a/src/DataCollector/CommandEventCollector.php b/src/DataCollector/CommandEventCollector.php new file mode 100644 index 0000000..cb05e21 --- /dev/null +++ b/src/DataCollector/CommandEventCollector.php @@ -0,0 +1,11 @@ + */ + private array $stopwatchEvents = []; + + public function __construct( + private readonly int $clientId, + private readonly CommandEventCollector $collector, + private readonly ?Stopwatch $stopwatch = null, + ) { + } + + public function commandStarted(CommandStartedEvent $event): void + { + $requestId = $event->getRequestId(); + + $command = (array) $event->getCommand(); + unset($command['lsid'], $command['$clusterTime']); + + $data = [ + 'databaseName' => $event->getDatabaseName(), + 'commandName' => $event->getCommandName(), + 'command' => $command, + 'operationId' => $event->getOperationId(), + 'serviceId' => $event->getServiceId(), + 'backtrace' => $this->filterBacktrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)), + ]; + + if ($event->getCommandName() === 'getMore') { + $data['cursorId'] = $event->getCommand()->getMore; + } + + $this->collector->collectCommandEvent($this->clientId, $requestId, $data); + + $this->stopwatchEvents[$requestId] = $this->stopwatch?->start( + 'mongodb.' . $event->getCommandName(), + 'mongodb', + ); + } + + public function commandSucceeded(CommandSucceededEvent $event): void + { + $requestId = $event->getRequestId(); + + $this->stopwatchEvents[$requestId]?->stop(); + unset($this->stopwatchEvents[$requestId]); + + $data = [ + 'durationMicros' => $event->getDurationMicros(), + ]; + + if (isset($event->getReply()->cursor)) { + $data['cursorId'] = $event->getReply()->cursor->id; + } + + $this->collector->collectCommandEvent($this->clientId, $requestId, $data); + } + + public function commandFailed(CommandFailedEvent $event): void + { + $requestId = $event->getRequestId(); + + $this->stopwatchEvents[$requestId]?->stop(); + unset($this->stopwatchEvents[$requestId]); + + $data = [ + 'durationMicros' => $event->getDurationMicros(), + 'error' => (string) $event->getError(), + ]; + + $this->collector->collectCommandEvent($this->clientId, $requestId, $data); + } + + private function filterBacktrace(array $backtrace): array + { + // skip first since it's always the current method + array_shift($backtrace); + + return $backtrace; + } +} diff --git a/src/DataCollector/MongoDBDataCollector.php b/src/DataCollector/MongoDBDataCollector.php new file mode 100644 index 0000000..a14bcd4 --- /dev/null +++ b/src/DataCollector/MongoDBDataCollector.php @@ -0,0 +1,145 @@ +> + */ + private array $requests = []; + + public function __construct( + private readonly ?Stopwatch $stopwatch = null, + /** @var iterable */ + private readonly iterable $clients = [], + ) { + } + + public function configureClient(Client $client): void + { + $client->getManager()->addSubscriber(new DriverEventSubscriber(spl_object_id($client), $this, $this->stopwatch)); + } + + public function collectCommandEvent(int $clientId, string $requestId, array $data): void + { + if (isset($this->requests[$clientId][$requestId])) { + $this->requests[$clientId][$requestId] += $data; + } else { + $this->requests[$clientId][$requestId] = $data; + } + } + + public function collect(Request $request, Response $response, ?Throwable $exception = null): void + { + } + + public function lateCollect(): void + { + $requestCount = 0; + $errorCount = 0; + $durationMicros = 0; + + $clients = []; + $clientIdMap = []; + foreach ($this->clients as $name => $client) { + $clientIdMap[spl_object_id($client)] = $name; + $clients[$name] = [ + 'serverBuildInfo' => array_diff_key( + (array) $client->getManager()->executeCommand('admin', new Command(['buildInfo' => 1]))->toArray()[0], + ['versionArray' => 0, 'ok' => 0], + ), + 'clientInfo' => array_diff_key($client->__debugInfo(), ['manager' => 0]), + ]; + } + + $requests = []; + foreach ($this->requests as $clientId => $requestsByClientId) { + $clientName = $clientIdMap[$clientId] ?? throw new LogicException('Client not found'); + foreach ($requestsByClientId as $requestId => $request) { + $requests[$clientName][$requestId] = $request; + $requestCount++; + $durationMicros += $request['durationMicros'] ?? 0; + $errorCount += isset($request['error']) ? 1 : 0; + } + } + + $this->data = [ + 'clients' => $clients, + 'requests' => $requests, + 'requestCount' => $requestCount, + 'errorCount' => $errorCount, + 'durationMicros' => $durationMicros, + ]; + } + + public function getRequestCount(): int + { + return $this->data['requestCount']; + } + + public function getErrorCount(): int + { + return $this->data['errorCount']; + } + + public function getTime(): int + { + return $this->data['durationMicros']; + } + + public function getRequests(): array + { + return $this->data['requests']; + } + + public function getClients(): array + { + return $this->data['clients']; + } + + public function getName(): string + { + return 'mongodb'; + } + + public function reset(): void + { + $this->requests = []; + $this->data = []; + } +} diff --git a/src/DependencyInjection/Compiler/DataCollectorPass.php b/src/DependencyInjection/Compiler/DataCollectorPass.php new file mode 100644 index 0000000..8fdd230 --- /dev/null +++ b/src/DependencyInjection/Compiler/DataCollectorPass.php @@ -0,0 +1,45 @@ +has('profiler')) { + return; + } + + /** + * Add a subscriber to each client to collect driver events. + * + * @see \MongoDB\Bundle\DataCollector\MongoDBDataCollector::configureClient() + */ + foreach ($container->findTaggedServiceIds('mongodb.client', true) as $clientId => $attributes) { + $container->getDefinition($clientId)->setConfigurator([new Reference('mongodb.data_collector'), 'configureClient']); + } + } +} diff --git a/src/DependencyInjection/MongoDBExtension.php b/src/DependencyInjection/MongoDBExtension.php index a8b06e7..3b4ce85 100644 --- a/src/DependencyInjection/MongoDBExtension.php +++ b/src/DependencyInjection/MongoDBExtension.php @@ -23,6 +23,7 @@ use InvalidArgumentException; use MongoDB\Client; use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; @@ -61,16 +62,14 @@ public static function createClientServiceId(string $clientId): string private function createClients(string $defaultClient, array $clients, ContainerBuilder $container): void { - $clientPrototype = $container->getDefinition('mongodb.prototype.client'); - foreach ($clients as $client => $configuration) { $serviceId = self::createClientServiceId($client); - $clientDefinition = clone $clientPrototype; + $clientDefinition = new ChildDefinition('mongodb.abstract.client'); $clientDefinition->setArgument('$uri', $configuration['uri']); $clientDefinition->setArgument('$uriOptions', $configuration['uri_options'] ?? []); $clientDefinition->setArgument('$driverOptions', $configuration['driver_options'] ?? []); - + $clientDefinition->addTag('mongodb.client', ['name' => $client]); $container->setDefinition($serviceId, $clientDefinition); if (isset($configuration['default_database'])) { @@ -84,14 +83,13 @@ private function createClients(string $defaultClient, array $clients, ContainerB // Register an autowiring alias for the default client $container->setAlias(Client::class, self::createClientServiceId($defaultClient)); - if (isset($clients[$defaultClient]['default_database'])) { - $container->setParameter( - sprintf('%s.default_database', Client::class), - $clients[$defaultClient]['default_database'], - ); + if (! isset($clients[$defaultClient]['default_database'])) { + return; } - // Remove the prototype definition as it's tagged as client - $container->removeDefinition('mongodb.prototype.client'); + $container->setParameter( + sprintf('%s.default_database', Client::class), + $clients[$defaultClient]['default_database'], + ); } } diff --git a/src/MongoDBBundle.php b/src/MongoDBBundle.php index 127c1bc..5dac19a 100644 --- a/src/MongoDBBundle.php +++ b/src/MongoDBBundle.php @@ -21,6 +21,7 @@ namespace MongoDB\Bundle; use MongoDB\Bundle\DependencyInjection\MongoDBExtension; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\HttpKernel\Bundle\AbstractBundle; @@ -30,4 +31,9 @@ public function getContainerExtension(): ?ExtensionInterface { return new MongoDBExtension(); } + + public function build(ContainerBuilder $container): void + { + $container->addCompilerPass(new DependencyInjection\Compiler\DataCollectorPass()); + } } diff --git a/templates/Collector/icon-minus-square.svg b/templates/Collector/icon-minus-square.svg new file mode 100644 index 0000000..471c274 --- /dev/null +++ b/templates/Collector/icon-minus-square.svg @@ -0,0 +1 @@ + diff --git a/templates/Collector/icon-plus-square.svg b/templates/Collector/icon-plus-square.svg new file mode 100644 index 0000000..2f5c3b3 --- /dev/null +++ b/templates/Collector/icon-plus-square.svg @@ -0,0 +1 @@ + diff --git a/templates/Collector/mongodb.html.twig b/templates/Collector/mongodb.html.twig new file mode 100644 index 0000000..9459e74 --- /dev/null +++ b/templates/Collector/mongodb.html.twig @@ -0,0 +1,236 @@ +{% extends request.isXmlHttpRequest ? '@WebProfiler/Profiler/ajax_layout.html.twig' : '@WebProfiler/Profiler/layout.html.twig' %} + +{% import _self as helper %} + +{% block head %} + {{ parent() }} + + +{% endblock %} + +{% block toolbar %} + {% if collector.requestCount > 0 %} + + {% set icon %} + {{ source('@MongoDB/Collector/mongodb.svg') }} + + {{ collector.requestCount }} + + in + {{ '%0.2f'|format(collector.time / 1000) }} + ms + + {% endset %} + + {% set text %} +
+ MongoDB Requests + {{ collector.requestCount }} +
+
+ Total errors + {{ collector.errorCount }} +
+
+ Request time + {{ '%0.2f'|format(collector.time / 1000) }} ms +
+ {% endset %} + + + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status|default('') }) }} + {% endif %} +{% endblock %} + +{% block menu %} + + {{ include('@MongoDB/Collector/mongodb.svg') }} + MongoDB + {% if collector.requestCount %} + + {{ collector.requestCount }} + + {% endif %} + +{% endblock %} + +{% block panel %} + +

Request Metrics

+ +
+
+
+ {{ collector.requestCount }} + Requests +
+ +
+ {{ collector.errorCount }} + Errors +
+ +
+ {{ '%0.2f'|format(collector.time / 1000) }} ms + Request time +
+
+
+ +
+
+

+ Requests +

+ +
+ {% if collector.requests is empty %} +
+

No executed requests.

+
+ {% else %} + {% for client, requests in collector.requests %} + {% if collector.clients|length > 1 %} +

{{ client }} client

+ {% endif %} + + {% if requests is empty %} +
+

No database requests were performed.

+
+ {% else %} + + + + + + + + + + {% for i, request in requests %} + + + + + + {% endfor %} + +
#TimeInfo
{{ loop.index }}{{ '%0.2f'|format(request.durationMicros / 1000) }} ms + {{ dump(request.command) }} + +
+ View formatted command + + {% if request.backtrace is defined %} +    + View command backtrace + {% endif %} +
+ + + + {% if request.backtrace is defined %} + + {% endif %} +
+ {% endif %} + {% endfor %} + {% endif %} +
+
+ +
+

+ Clients + {{ collector.clients|length }} +

+ + +
+ {% if collector.clients is empty %} +
+

No clients were used.

+
+ {% endif %} + {% for clientName, client in collector.clients %} +

Client {{ clientName }}

+ + + + + + + + + + + + + + + + + + +
KeyValue
Client Debug Info{{ dump(client.clientInfo) }}
Server Build Info{{ dump(client.serverBuildInfo) }}
+ {% endfor %} +
+
+
+{% endblock %} diff --git a/templates/Collector/mongodb.svg b/templates/Collector/mongodb.svg new file mode 100644 index 0000000..83a00a9 --- /dev/null +++ b/templates/Collector/mongodb.svg @@ -0,0 +1,3 @@ + + + diff --git a/tests/Functional/DataCollector/DriverEventSubscriberTest.php b/tests/Functional/DataCollector/DriverEventSubscriberTest.php new file mode 100644 index 0000000..03e31ff --- /dev/null +++ b/tests/Functional/DataCollector/DriverEventSubscriberTest.php @@ -0,0 +1,103 @@ +collector = new class implements CommandEventCollector + { + public array $events; + + public function collectCommandEvent(int $clientId, string $requestId, array $data): void + { + $this->events[] = ['clientId' => $clientId, 'requestId' => $requestId, 'data' => $data]; + } + }; + + $this->stopwatch = new Stopwatch();; + } + + public function testCommandSucceeded(): void + { + $this->getClient()->selectCollection('database1', 'collection1')->find(); + + // The 2 events are commandStarted and commandSucceeded + $this->assertCount(2, $this->collector->events); + + // ClientId + $this->assertSame(self::CLIENT_ID, $this->collector->events[0]['clientId']); + $this->assertSame(self::CLIENT_ID, $this->collector->events[1]['clientId']); + + // RequestId + $this->assertSame($this->collector->events[0]['requestId'], $this->collector->events[1]['requestId'], 'Same $requestId'); + + // Data 1st event + $this->assertSame('database1', $this->collector->events[0]['data']['databaseName']); + $this->assertSame('find', $this->collector->events[0]['data']['commandName']); + $this->assertArrayHasKey('command', $this->collector->events[0]['data']); + $this->assertArrayHasKey('backtrace', $this->collector->events[0]['data']); + + // Data 2nd event + $this->assertArrayHasKey('durationMicros', $this->collector->events[1]['data']); + $this->assertArrayNotHasKey('backtrace', $this->collector->events[1]['data']); + } + + public function testCommandFailed(): void + { + try { + $this->getClient()->getManager()->executeCommand('database1', new Command(['invalid' => 'command'])); + $this->fail('Expected exception to be thrown'); + } catch (ServerException $e) { + $message = $e->getMessage(); + } + + // The 2 events are commandStarted and commandFailed + $this->assertCount(2, $this->collector->events); + + // ClientId + $this->assertSame(self::CLIENT_ID, $this->collector->events[0]['clientId']); + $this->assertSame(self::CLIENT_ID, $this->collector->events[1]['clientId']); + + // RequestId + $this->assertSame($this->collector->events[0]['requestId'], $this->collector->events[1]['requestId'], 'Same $requestId'); + + // Data 1st event + $this->assertSame('database1', $this->collector->events[0]['data']['databaseName']); + $this->assertSame('invalid', $this->collector->events[0]['data']['commandName']); + $this->assertArrayHasKey('command', $this->collector->events[0]['data']); + $this->assertArrayHasKey('backtrace', $this->collector->events[0]['data']); + + // Data 2nd event + $this->assertArrayHasKey('durationMicros', $this->collector->events[1]['data']); + $this->assertArrayHasKey('error', $this->collector->events[1]['data']); + $this->assertStringContainsString($message, $this->collector->events[1]['data']['error']); + $this->assertArrayNotHasKey('backtrace', $this->collector->events[1]['data']); + } + + public function getClient(): Client + { + $subscriber = new DriverEventSubscriber(self::CLIENT_ID, $this->collector, $this->stopwatch); + + $client = new Client($_SERVER['MONGODB_PRIMARY_URL'] ?? 'mongodb://localhost:27017'); + $client->getManager()->addSubscriber($subscriber); + + return $client; + } +} diff --git a/tests/Functional/DataCollector/MongoDBDataCollectorTest.php b/tests/Functional/DataCollector/MongoDBDataCollectorTest.php new file mode 100644 index 0000000..b0059d8 --- /dev/null +++ b/tests/Functional/DataCollector/MongoDBDataCollectorTest.php @@ -0,0 +1,105 @@ + $client1, 'client2' => $client2])); + + $client1Id = spl_object_id($client1); + $client2Id = spl_object_id($client2); + + // Successful command on Client 1 + $dataCollector->collectCommandEvent($client1Id, 'request1', [ + 'clientName' => 'client1', + 'databaseName' => 'database1', + 'commandName' => 'find', + 'command' => ['find' => 'collection1', 'filter' => []], + ]); + $dataCollector->collectCommandEvent($client1Id, 'request1', ['durationMicros' => 1000]); + + // Error on Client 1 + $dataCollector->collectCommandEvent($client1Id, 'request2', [ + 'clientName' => 'client1', + 'databaseName' => 'database1', + 'commandName' => 'insert', + 'command' => ['insert' => 'collection1', 'documents' => []], + ]); + $dataCollector->collectCommandEvent($client1Id, 'request2', [ + 'durationMicros' => 500, + 'error' => 'Error message', + ]); + + // Successful command on Client 2 + $dataCollector->collectCommandEvent($client2Id, 'request3', [ + 'clientName' => 'client2', + 'databaseName' => 'database2', + 'commandName' => 'aggregate', + 'command' => ['aggregate' => 'collection2', 'pipeline' => []], + ]); + $dataCollector->collectCommandEvent($client2Id, 'request3', ['durationMicros' => 800]); + + $dataCollector->lateCollect(); + + // Data is serialized and unserialized by the profiler + $dataCollector = unserialize(serialize($dataCollector)); + + $this->assertSame('mongodb', $dataCollector->getName()); + $this->assertCount(2, $dataCollector->getClients()); + $this->assertSame(2300, $dataCollector->getTime()); + $this->assertSame(3, $dataCollector->getRequestCount()); + $this->assertSame(1, $dataCollector->getErrorCount()); + + $requests = $dataCollector->getRequests(); + $this->assertSame(['client1', 'client2'], array_keys($requests)); + $this->assertSame([ + 'request1' => [ + 'clientName' => 'client1', + 'databaseName' => 'database1', + 'commandName' => 'find', + 'command' => ['find' => 'collection1', 'filter' => []], + 'durationMicros' => 1000, + ], + 'request2' => [ + 'clientName' => 'client1', + 'databaseName' => 'database1', + 'commandName' => 'insert', + 'command' => ['insert' => 'collection1', 'documents' => []], + 'durationMicros' => 500, + 'error' => 'Error message', + ], + ], $requests['client1']); + $this->assertSame([ + 'request3' => [ + 'clientName' => 'client2', + 'databaseName' => 'database2', + 'commandName' => 'aggregate', + 'command' => ['aggregate' => 'collection2', 'pipeline' => []], + 'durationMicros' => 800, + ], + ], $requests['client2']); + + $clients = $dataCollector->getClients(); + $this->assertSame(['client1', 'client2'], array_keys($clients)); + $this->assertSame(['serverBuildInfo', 'clientInfo'], array_keys($clients['client1'])); + } +} diff --git a/tests/Unit/DependencyInjection/MongoDBExtensionTest.php b/tests/Unit/DependencyInjection/MongoDBExtensionTest.php index c0cb90f..7e9afe3 100644 --- a/tests/Unit/DependencyInjection/MongoDBExtensionTest.php +++ b/tests/Unit/DependencyInjection/MongoDBExtensionTest.php @@ -21,13 +21,20 @@ namespace MongoDB\Bundle\Tests\Unit\DependencyInjection; use InvalidArgumentException; +use MongoDB\Bundle\DependencyInjection\Compiler\DataCollectorPass; use MongoDB\Bundle\DependencyInjection\MongoDBExtension; use MongoDB\Client; use MongoDB\Driver\ServerApi; use PHPUnit\Framework\TestCase; +use stdClass; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; +use function is_a; +use function sprintf; + /** @covers \MongoDB\Bundle\DependencyInjection\MongoDBExtension */ final class MongoDBExtensionTest extends TestCase { @@ -51,6 +58,8 @@ public function testLoadWithSingleClient(): void ['id' => 'default', 'uri' => 'mongodb://localhost:27017'], ], ], + ], [ + 'profiler' => new Definition(stdClass::class), ]); $this->assertTrue($container->hasDefinition('mongodb.client.default')); @@ -59,14 +68,37 @@ public function testLoadWithSingleClient(): void // Check service definition $definition = $container->getDefinition('mongodb.client.default'); - $this->assertSame(Client::class, $definition->getClass()); $this->assertSame('mongodb://localhost:27017', $definition->getArgument('$uri')); + $this->assertNotNull($definition->getConfigurator()); + $this->assertInstanceOf(ChildDefinition::class, $definition); + $this->assertSame('mongodb.abstract.client', $definition->getParent()); + $parentDefinition = $container->getDefinition($definition->getParent()); + $this->assertTrue(is_a($parentDefinition->getClass(), Client::class, true), sprintf('Expected "%s" to be a "%s"', $definition->getClass(), Client::class)); + $this->assertTrue($parentDefinition->isAbstract()); // Check alias definition $alias = $container->getAlias(Client::class); $this->assertSame('mongodb.client.default', (string) $alias); } + public function testLoadWithoutProfiler(): void + { + $container = $this->getContainer([[ + 'clients' => [ + ['id' => 'default', 'uri' => 'mongodb://localhost:27017'], + ], + ], + ]); + + // Check service definition + $definition = $container->getDefinition('mongodb.client.default'); + $this->assertFalse($definition->hasMethodCall('addSubscriber')); + + // Check data collector + $definition = $container->getDefinition('mongodb.data_collector'); + $this->assertFalse($definition->hasMethodCall('addClient')); + } + public function testLoadWithMultipleClients(): void { $container = $this->getContainer([[ @@ -84,6 +116,8 @@ public function testLoadWithMultipleClients(): void ], ], ], + ], [ + 'profiler' => new Definition(stdClass::class), ]); $this->assertTrue($container->hasDefinition('mongodb.client.default')); @@ -93,14 +127,18 @@ public function testLoadWithMultipleClients(): void // Check service definitions $definition = $container->getDefinition('mongodb.client.default'); - $this->assertSame(Client::class, $definition->getClass()); + $this->assertInstanceOf(ChildDefinition::class, $definition); + $this->assertSame('mongodb.abstract.client', $definition->getParent()); $this->assertSame('mongodb://localhost:27017', $definition->getArgument('$uri')); $this->assertSame(['readPreference' => 'primary'], $definition->getArgument('$uriOptions')); + $this->assertNotNull($definition->getConfigurator()); $definition = $container->getDefinition('mongodb.client.secondary'); - $this->assertSame(Client::class, $definition->getClass()); + $this->assertInstanceOf(ChildDefinition::class, $definition); + $this->assertSame('mongodb.abstract.client', $definition->getParent()); $this->assertSame('mongodb://localhost:27018', $definition->getArgument('$uri')); $this->assertEquals(['serverApi' => new ServerApi((string) ServerApi::V1)], $definition->getArgument('$driverOptions')); + $this->assertNotNull($definition->getConfigurator()); } private function getContainer(array $config = [], array $thirdPartyDefinitions = []): ContainerBuilder @@ -113,6 +151,7 @@ private function getContainer(array $config = [], array $thirdPartyDefinitions = $container->getCompilerPassConfig()->setOptimizationPasses([]); $container->getCompilerPassConfig()->setRemovingPasses([]); + $container->addCompilerPass(new DataCollectorPass()); $loader = new MongoDBExtension(); $loader->load($config, $container); From fd8db9118a3ccaf864408b1ba1a7a7db19814d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 4 Mar 2025 21:50:00 +0100 Subject: [PATCH 07/18] Fix data collector (#23) builderEncoder uses a WeakMap that cannot be serialized --- composer.json | 3 ++- src/DataCollector/MongoDBDataCollector.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index c2f1ffa..a47bec3 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ ], "require": { "php": ">=8.1", - "mongodb/mongodb": "^1.17", + "ext-mongodb": "^1.21", + "mongodb/mongodb": "^1.21", "symfony/config": "^6.3 || ^7.0", "symfony/console": "^6.3 || ^7.0", "symfony/dependency-injection": "^6.3.5 || ^7.0", diff --git a/src/DataCollector/MongoDBDataCollector.php b/src/DataCollector/MongoDBDataCollector.php index a14bcd4..c83f24a 100644 --- a/src/DataCollector/MongoDBDataCollector.php +++ b/src/DataCollector/MongoDBDataCollector.php @@ -83,7 +83,7 @@ public function lateCollect(): void (array) $client->getManager()->executeCommand('admin', new Command(['buildInfo' => 1]))->toArray()[0], ['versionArray' => 0, 'ok' => 0], ), - 'clientInfo' => array_diff_key($client->__debugInfo(), ['manager' => 0]), + 'clientInfo' => array_diff_key($client->__debugInfo(), ['manager' => 0, 'builderEncoder' => 0]), ]; } From f37094b47916fdedfc96830ea61d70f365c83b62 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 4 Mar 2025 21:53:35 +0100 Subject: [PATCH 08/18] Use `ramsey/composer-install` action v3 (#21) --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5e68d91..c4ef44e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -79,7 +79,7 @@ jobs: run: composer require ${{ matrix.variant }} --no-update - name: Install Composer dependencies (${{ matrix.dependencies }}) - uses: ramsey/composer-install@v2 + uses: ramsey/composer-install@v3 with: dependency-versions: ${{ matrix.dependencies }} From 2a303e4adbdad9243ef7817b39c6d82cd2d9a38c Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 4 Mar 2025 21:54:00 +0100 Subject: [PATCH 09/18] Use MongoDB 8 in CI (#20) --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c4ef44e..adb39a3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,14 +18,14 @@ jobs: services: mongodb_primary: - image: mongo:7-jammy + image: mongo:8 env: MONGO_INITDB_ROOT_USERNAME: "primary" MONGO_INITDB_ROOT_PASSWORD: "password" ports: - "27017:27017" mongodb_secondary: - image: mongo:7-jammy + image: mongo:8 env: MONGO_INITDB_ROOT_USERNAME: "secondary" MONGO_INITDB_ROOT_PASSWORD: "password" From d48585beefba6d370f004c5b8975fa6511efdd20 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 4 Mar 2025 21:54:24 +0100 Subject: [PATCH 10/18] Require safe symfony/runtime versions (#18) See vulnerability https://github.com/symfony/symfony/security/advisories/GHSA-x8vp-gf4q-mw5j --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a47bec3..0db6ad0 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "symfony/console": "^6.3 || ^7.0", "symfony/dependency-injection": "^6.3.5 || ^7.0", "symfony/http-kernel": "^6.3.5 || ^7.0", - "symfony/runtime": "^6.3 || ^7.0" + "symfony/runtime": "^6.4.14 || ^7.1.7" }, "require-dev": { "doctrine/coding-standard": "^12.0", From db8649f8ea682fd7b873d2234dac7b31ac6c68a9 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 4 Mar 2025 21:54:44 +0100 Subject: [PATCH 11/18] Sort dependencies in `composer.json` (#17) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 0db6ad0..8d3535e 100644 --- a/composer.json +++ b/composer.json @@ -29,8 +29,8 @@ "symfony/framework-bundle": "^6.3.5 || ^7.0", "symfony/phpunit-bridge": "~6.3.10 || ^6.4.1 || ^7.0.1", "symfony/stopwatch": "^6.3 || ^7.0", - "symfony/yaml": "^6.3 || ^7.0", "symfony/web-profiler-bundle": "^6.3 || ^7.0", + "symfony/yaml": "^6.3 || ^7.0", "zenstruck/browser": "^1.6" }, "scripts": { From cabfa913e7b6073274bfee7ae0308b10d0f89d96 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 4 Mar 2025 21:55:18 +0100 Subject: [PATCH 12/18] Require `rector/rector` 2 (#16) --- composer.json | 2 +- rector.php | 2 +- src/MongoDBBundle.php | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 8d3535e..cba6e6b 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ }, "require-dev": { "doctrine/coding-standard": "^12.0", - "rector/rector": "^0.18", + "rector/rector": "^2.0", "symfony/browser-kit": "^6.3 || ^7.0", "symfony/filesystem": "^6.3 || ^7.0", "symfony/framework-bundle": "^6.3.5 || ^7.0", diff --git a/rector.php b/rector.php index bb32881..5f72f61 100644 --- a/rector.php +++ b/rector.php @@ -19,9 +19,9 @@ */ use Rector\Config\RectorConfig; -use Rector\Core\ValueObject\PhpVersion; use Rector\Set\ValueObject\LevelSetList; use Rector\Set\ValueObject\SetList; +use Rector\ValueObject\PhpVersion; return static function (RectorConfig $rectorConfig): void { $rectorConfig->parallel(); diff --git a/src/MongoDBBundle.php b/src/MongoDBBundle.php index 5dac19a..a9e1ce6 100644 --- a/src/MongoDBBundle.php +++ b/src/MongoDBBundle.php @@ -20,6 +20,7 @@ namespace MongoDB\Bundle; +use MongoDB\Bundle\DependencyInjection\Compiler\DataCollectorPass; use MongoDB\Bundle\DependencyInjection\MongoDBExtension; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; @@ -34,6 +35,6 @@ public function getContainerExtension(): ?ExtensionInterface public function build(ContainerBuilder $container): void { - $container->addCompilerPass(new DependencyInjection\Compiler\DataCollectorPass()); + $container->addCompilerPass(new DataCollectorPass()); } } From 41dc635d5763ac3fa8e1bf9bd8c5d2ebb221882e Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 4 Mar 2025 21:56:24 +0100 Subject: [PATCH 13/18] Add PHP 8.4 to test matrix (#22) --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index adb39a3..10fdd38 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -45,6 +45,7 @@ jobs: - '8.1' - '8.2' - '8.3' + - '8.4' dependencies: - 'highest' - 'lowest' From 5beff1726cfd5b72769121e31754aaa088a02def Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 4 Mar 2025 21:58:36 +0100 Subject: [PATCH 14/18] Use `getDatabase` and `getCollection` (#19) selectDatabase and selectCollection are deprecated since 1.21 Follows https://github.com/mongodb/mongo-php-library/releases/tag/1.21.0 --- README.md | 6 +++--- src/Attribute/AutowireCollection.php | 2 +- src/Attribute/AutowireDatabase.php | 2 +- .../Functional/DataCollector/DriverEventSubscriberTest.php | 2 +- tests/Functional/FunctionalTestCase.php | 6 +++--- .../src/Controller/AbstractMongoDBController.php | 6 +++--- tests/Unit/Attribute/AutowireCollectionTest.php | 6 +++--- tests/Unit/Attribute/AutowireDatabaseTest.php | 6 +++--- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 2cf4295..d451ecc 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ class MyService ## Database Usage The client service provides access to databases and collections. You can access a database by calling the -`selectDatabase` method, passing the database name and potential options: +`getDatabase` method, passing the database name and potential options: ```php use MongoDB\Client; @@ -144,7 +144,7 @@ class MyService public function __construct( Client $client, ) { - $this->database = $client->selectDatabase('myDatabase'); + $this->database = $client->getDatabase('myDatabase'); } } ``` @@ -185,7 +185,7 @@ class MyService ## Collection Usage -To inject a collection, you can either call the `selectCollection` method on a `Client` or `Database` instance. +To inject a collection, you can either call the `getCollection` method on a `Client` or `Database` instance. For convenience, the `#[AutowireCollection]` attribute provides a quicker alternative: ```php diff --git a/src/Attribute/AutowireCollection.php b/src/Attribute/AutowireCollection.php index 6f99bb4..a442bd7 100644 --- a/src/Attribute/AutowireCollection.php +++ b/src/Attribute/AutowireCollection.php @@ -61,7 +61,7 @@ public function __construct( : MongoDBExtension::createClientServiceId($client); parent::__construct( - callable: [new Reference($this->serviceId), 'selectCollection'], + callable: [new Reference($this->serviceId), 'getCollection'], lazy: $lazy, ); } diff --git a/src/Attribute/AutowireDatabase.php b/src/Attribute/AutowireDatabase.php index 0b9b56b..422102c 100644 --- a/src/Attribute/AutowireDatabase.php +++ b/src/Attribute/AutowireDatabase.php @@ -59,7 +59,7 @@ public function __construct( : MongoDBExtension::createClientServiceId($client); parent::__construct( - callable: [new Reference($this->serviceId), 'selectDatabase'], + callable: [new Reference($this->serviceId), 'getDatabase'], lazy: $lazy, ); } diff --git a/tests/Functional/DataCollector/DriverEventSubscriberTest.php b/tests/Functional/DataCollector/DriverEventSubscriberTest.php index 03e31ff..b729e89 100644 --- a/tests/Functional/DataCollector/DriverEventSubscriberTest.php +++ b/tests/Functional/DataCollector/DriverEventSubscriberTest.php @@ -36,7 +36,7 @@ public function collectCommandEvent(int $clientId, string $requestId, array $dat public function testCommandSucceeded(): void { - $this->getClient()->selectCollection('database1', 'collection1')->find(); + $this->getClient()->getCollection('database1', 'collection1')->find(); // The 2 events are commandStarted and commandSucceeded $this->assertCount(2, $this->collector->events); diff --git a/tests/Functional/FunctionalTestCase.php b/tests/Functional/FunctionalTestCase.php index 016abbf..598468a 100644 --- a/tests/Functional/FunctionalTestCase.php +++ b/tests/Functional/FunctionalTestCase.php @@ -68,9 +68,9 @@ public function assertNumberOfDocuments(int $expected, string $clientId, string { $client = self::getContainer()->get(MongoDBExtension::createClientServiceId($clientId)); assert($client instanceof Client); - $db = $client->selectDatabase($database); + $db = $client->getDatabase($database); assert($db instanceof Database); - $collection = $db->selectCollection($collection); + $collection = $db->getCollection($collection); assert($collection instanceof Collection); $this->assertSame($expected, $collection->countDocuments()); @@ -80,7 +80,7 @@ private function dropDatabase(string $clientId, string $database): void { $client = self::getContainer()->get(MongoDBExtension::createClientServiceId($clientId)); assert($client instanceof Client); - $db = $client->selectDatabase($database); + $db = $client->getDatabase($database); assert($db instanceof Database); $db->drop(); } diff --git a/tests/TestApplication/src/Controller/AbstractMongoDBController.php b/tests/TestApplication/src/Controller/AbstractMongoDBController.php index 990ac84..5426fb9 100644 --- a/tests/TestApplication/src/Controller/AbstractMongoDBController.php +++ b/tests/TestApplication/src/Controller/AbstractMongoDBController.php @@ -67,11 +67,11 @@ final public function insertDocumentForClient(Client|string $client, ?string $da assert($database !== null); - $db = $client->selectDatabase($database); + $db = $client->getDatabase($database); $db->drop(); if (! $collection instanceof Collection) { - $collection = $db->selectCollection($collection); + $collection = $db->getCollection($collection); } $collection->insertOne(['foo' => 'bar']); @@ -80,7 +80,7 @@ final public function insertDocumentForClient(Client|string $client, ?string $da final public function insertDocumentForDatabase(Database $database, string $collection): void { $database->drop(); - $collection = $database->selectCollection($collection); + $collection = $database->getCollection($collection); $collection->insertOne(['foo' => 'bar']); } diff --git a/tests/Unit/Attribute/AutowireCollectionTest.php b/tests/Unit/Attribute/AutowireCollectionTest.php index 006a73d..4a79d1b 100644 --- a/tests/Unit/Attribute/AutowireCollectionTest.php +++ b/tests/Unit/Attribute/AutowireCollectionTest.php @@ -35,7 +35,7 @@ public function testMinimal(): void { $autowire = new AutowireCollection(); - $this->assertEquals([new Reference($client = Client::class), 'selectCollection'], $autowire->value); + $this->assertEquals([new Reference($client = Client::class), 'getCollection'], $autowire->value); $definition = $autowire->buildDefinition( value: $autowire->value, @@ -62,7 +62,7 @@ public function testCollection(): void client: 'default', ); - $this->assertEquals([new Reference('mongodb.client.default'), 'selectCollection'], $autowire->value); + $this->assertEquals([new Reference('mongodb.client.default'), 'getCollection'], $autowire->value); $definition = $autowire->buildDefinition( value: $autowire->value, @@ -88,7 +88,7 @@ public function testWithoutCollection(): void client: 'default', ); - $this->assertEquals([new Reference('mongodb.client.default'), 'selectCollection'], $autowire->value); + $this->assertEquals([new Reference('mongodb.client.default'), 'getCollection'], $autowire->value); $definition = $autowire->buildDefinition( value: $autowire->value, diff --git a/tests/Unit/Attribute/AutowireDatabaseTest.php b/tests/Unit/Attribute/AutowireDatabaseTest.php index 1958e20..d6bc805 100644 --- a/tests/Unit/Attribute/AutowireDatabaseTest.php +++ b/tests/Unit/Attribute/AutowireDatabaseTest.php @@ -34,7 +34,7 @@ public function testMinimal(): void { $autowire = new AutowireDatabase(); - $this->assertEquals([new Reference(Client::class), 'selectDatabase'], $autowire->value); + $this->assertEquals([new Reference(Client::class), 'getDatabase'], $autowire->value); $definition = $autowire->buildDefinition( value: $autowire->value, @@ -58,7 +58,7 @@ public function testDatabase(): void client: 'default', ); - $this->assertEquals([new Reference('mongodb.client.default'), 'selectDatabase'], $autowire->value); + $this->assertEquals([new Reference('mongodb.client.default'), 'getDatabase'], $autowire->value); $definition = $autowire->buildDefinition( value: $autowire->value, @@ -82,7 +82,7 @@ public function testWithoutDatabase(): void client: 'default', ); - $this->assertEquals([new Reference('mongodb.client.default'), 'selectDatabase'], $autowire->value); + $this->assertEquals([new Reference('mongodb.client.default'), 'getDatabase'], $autowire->value); $definition = $autowire->buildDefinition( value: $autowire->value, From 1cbc4c562c02057aaa3b40e9cfd146dca4f3250c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 5 Mar 2025 08:22:00 +0100 Subject: [PATCH 15/18] Allow usage of `#[Target]` attribute for the MongoDB client (#15) * Allow usage of #[Target] attribute for the MongoDB client * Add test to autowire client using #[Target] attribute --- README.md | 15 +++++++++++++++ src/DependencyInjection/MongoDBExtension.php | 1 + tests/Functional/Attribute/AutowireClientTest.php | 3 +++ .../src/Controller/AutowireClientController.php | 11 +++++++++++ 4 files changed, 30 insertions(+) diff --git a/README.md b/README.md index d451ecc..71fbdd7 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,21 @@ class MyService } ``` +You can also use the `#[Target]` attribute: + +```php +use MongoDB\Client; +use Symfony\Component\DependencyInjection\Attribute\Target; + +class MyService +{ + public function __construct( + #[Target('second')] + private Client $client, + ) {} +} +``` + ## Database Usage The client service provides access to databases and collections. You can access a database by calling the diff --git a/src/DependencyInjection/MongoDBExtension.php b/src/DependencyInjection/MongoDBExtension.php index 3b4ce85..ff23ce3 100644 --- a/src/DependencyInjection/MongoDBExtension.php +++ b/src/DependencyInjection/MongoDBExtension.php @@ -78,6 +78,7 @@ private function createClients(string $defaultClient, array $clients, ContainerB // Allows to autowire the client using the name $container->registerAliasForArgument($serviceId, Client::class, sprintf('%sClient', $client)); + $container->registerAliasForArgument($serviceId, Client::class, $client); } // Register an autowiring alias for the default client diff --git a/tests/Functional/Attribute/AutowireClientTest.php b/tests/Functional/Attribute/AutowireClientTest.php index cd2ce86..7fc6b9c 100644 --- a/tests/Functional/Attribute/AutowireClientTest.php +++ b/tests/Functional/Attribute/AutowireClientTest.php @@ -53,6 +53,9 @@ public static function autowireClientProvider(): iterable /** @see AutowireClientController::viaNamedClient() */ yield 'via-named-client' => ['/autowire-client/via-named-client', self::CLIENT_ID_SECONDARY, self::DB_CUSTOMER_GOOGLE, self::COLLECTION_USERS]; + + /** @see AutowireClientController::viaTarget() */ + yield 'via-target' => ['/autowire-client/via-target', self::CLIENT_ID_SECONDARY, self::DB_CUSTOMER_GOOGLE, self::COLLECTION_USERS]; } /** diff --git a/tests/TestApplication/src/Controller/AutowireClientController.php b/tests/TestApplication/src/Controller/AutowireClientController.php index 820c45b..ef88184 100644 --- a/tests/TestApplication/src/Controller/AutowireClientController.php +++ b/tests/TestApplication/src/Controller/AutowireClientController.php @@ -24,6 +24,7 @@ use MongoDB\Bundle\Tests\Functional\Attribute\AutowireClientTest; use MongoDB\Bundle\Tests\Functional\FunctionalTestCase; use MongoDB\Client; +use Symfony\Component\DependencyInjection\Attribute\Target; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Routing\Annotation\Route; @@ -69,6 +70,16 @@ public function viaNamedClient( return new JsonResponse(); } + #[Route('/via-target')] + public function viaTarget( + #[Target('secondary')] + Client $client, + ): JsonResponse { + $this->insertDocumentForClient($client, FunctionalTestCase::DB_CUSTOMER_GOOGLE, FunctionalTestCase::COLLECTION_USERS); + + return new JsonResponse(); + } + #[Route('/with-unknown-client')] public function withUnknownClient( #[AutowireClient(client: 'foo-bar-baz')] From 7797702b63631656dd639366cdfc76210e4e84d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 5 Mar 2025 08:41:46 +0100 Subject: [PATCH 16/18] Fix CS --- src/Attribute/AutowireCollection.php | 1 - tests/Functional/DataCollector/DriverEventSubscriberTest.php | 2 +- tests/Unit/Attribute/AttributeTestCase.php | 2 ++ 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Attribute/AutowireCollection.php b/src/Attribute/AutowireCollection.php index a442bd7..5627157 100644 --- a/src/Attribute/AutowireCollection.php +++ b/src/Attribute/AutowireCollection.php @@ -34,7 +34,6 @@ use Symfony\Component\DependencyInjection\Reference; use function is_string; -use function ltrim; use function sprintf; /** diff --git a/tests/Functional/DataCollector/DriverEventSubscriberTest.php b/tests/Functional/DataCollector/DriverEventSubscriberTest.php index b729e89..2f9a2ce 100644 --- a/tests/Functional/DataCollector/DriverEventSubscriberTest.php +++ b/tests/Functional/DataCollector/DriverEventSubscriberTest.php @@ -31,7 +31,7 @@ public function collectCommandEvent(int $clientId, string $requestId, array $dat } }; - $this->stopwatch = new Stopwatch();; + $this->stopwatch = new Stopwatch(); } public function testCommandSucceeded(): void diff --git a/tests/Unit/Attribute/AttributeTestCase.php b/tests/Unit/Attribute/AttributeTestCase.php index e7ee39e..0a9ec3a 100644 --- a/tests/Unit/Attribute/AttributeTestCase.php +++ b/tests/Unit/Attribute/AttributeTestCase.php @@ -1,5 +1,7 @@ Date: Fri, 21 Mar 2025 10:12:09 +0100 Subject: [PATCH 17/18] Support MongoDB Driver 2.0 (#24) * Support version 2.0 of the MongoDB driver * Test with driver version 2.x in CI --- .github/workflows/ci.yaml | 11 ++++++++++- composer.json | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 10fdd38..4756bda 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,7 @@ permissions: jobs: test: - name: PHP ${{ matrix.php-version }} + ${{ matrix.dependencies }} + ${{ matrix.variant }} + name: PHP ${{ matrix.php-version }} + ${{ matrix.driver-version }} + ${{ matrix.dependencies }} + ${{ matrix.variant }} runs-on: ubuntu-latest @@ -46,6 +46,8 @@ jobs: - '8.2' - '8.3' - '8.4' + driver-version: + - 'stable' dependencies: - 'highest' - 'lowest' @@ -56,6 +58,12 @@ jobs: - 'symfony/symfony:"6.3.*"' - 'symfony/symfony:"6.4.*"' - 'symfony/symfony:"7.0.*"' + include: + - php-version: '8.4' + driver-version: 'mongodb/mongo-php-driver@v2.x' + dependencies: 'highest' + symfony-require: '' + variant: 'normal' steps: - name: Checkout @@ -65,6 +73,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} + extensions: "mongodb-${{ matrix.driver-version }}" - name: Add PHPUnit matcher run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" diff --git a/composer.json b/composer.json index cba6e6b..a5a803e 100644 --- a/composer.json +++ b/composer.json @@ -13,8 +13,8 @@ ], "require": { "php": ">=8.1", - "ext-mongodb": "^1.21", - "mongodb/mongodb": "^1.21", + "ext-mongodb": "^1.21 || ^2.0@dev", + "mongodb/mongodb": "^1.21 || ^2.0@dev", "symfony/config": "^6.3 || ^7.0", "symfony/console": "^6.3 || ^7.0", "symfony/dependency-injection": "^6.3.5 || ^7.0", From 3bc4d861b787783ed52479d51caa6b83a2518ed1 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 24 Jun 2025 09:50:07 +0200 Subject: [PATCH 18/18] Update mongodb extension and library version 2 constraint (#26) --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index a5a803e..7f64792 100644 --- a/composer.json +++ b/composer.json @@ -13,8 +13,8 @@ ], "require": { "php": ">=8.1", - "ext-mongodb": "^1.21 || ^2.0@dev", - "mongodb/mongodb": "^1.21 || ^2.0@dev", + "ext-mongodb": "^1.21 || ^2.0", + "mongodb/mongodb": "^1.21 || ^2.0", "symfony/config": "^6.3 || ^7.0", "symfony/console": "^6.3 || ^7.0", "symfony/dependency-injection": "^6.3.5 || ^7.0",