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)); } }