Skip to content

Commit

Permalink
Allow specifying options in AutowireDatabase and AutowireCollection (#14
Browse files Browse the repository at this point in the history
)

* 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
  • Loading branch information
alcaeus authored Dec 9, 2023
1 parent 2d3784a commit 74668e0
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 18 deletions.
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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,
) {}
}
```
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
28 changes: 26 additions & 2 deletions src/Attribute/AutowireCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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
Expand All @@ -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);
}
Expand Down
27 changes: 25 additions & 2 deletions src/Attribute/AutowireDatabase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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);
}
Expand Down
87 changes: 87 additions & 0 deletions tests/Unit/Attribute/AttributeTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

namespace MongoDB\Bundle\Tests\Unit\Attribute;

use Generator;
use MongoDB\BSON\Document;
use MongoDB\Codec\DecodeIfSupported;
use MongoDB\Codec\DocumentCodec;
use MongoDB\Codec\EncodeIfSupported;
use MongoDB\Driver\ReadConcern;
use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\WriteConcern;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Reference;

use function sprintf;

abstract class AttributeTestCase extends TestCase
{
public static function provideOptions(): Generator
{
$codec = new class implements DocumentCodec {
use DecodeIfSupported;
use EncodeIfSupported;

public function canDecode($value): bool
{
return $value instanceof Document;
}

public function canEncode($value): bool
{
return $value instanceof Document;
}

public function decode($value): Document
{
return $value;
}

public function encode($value): Document
{
return $value;
}
};

$options = [
'codec' => $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%'],
];
}
}
31 changes: 25 additions & 6 deletions tests/Unit/Attribute/AutowireCollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
Expand All @@ -80,15 +78,14 @@ 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
{
$autowire = new AutowireCollection(
database: 'mydb',
client: 'default',
options: ['foo' => 'bar'],
);

$this->assertEquals([new Reference('mongodb.client.default'), 'selectCollection'], $autowire->value);
Expand All @@ -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));
}
}
31 changes: 25 additions & 6 deletions tests/Unit/Attribute/AutowireDatabaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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));
}
}

0 comments on commit 74668e0

Please sign in to comment.