diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6f5b591..4756bda 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: - "*.*" @@ -15,20 +12,20 @@ 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 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" @@ -47,6 +44,10 @@ jobs: php-version: - '8.1' - '8.2' + - '8.3' + - '8.4' + driver-version: + - 'stable' dependencies: - 'highest' - 'lowest' @@ -57,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 @@ -66,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" @@ -81,7 +89,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 }} diff --git a/README.md b/README.md index 27e9c5a..71fbdd7 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,12 @@ mongodb: clients: default: uri: '%env(MONGODB_URI)%' - uriOptions: #... - driverOptions: #... + default_database: #... + 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: @@ -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,16 +122,31 @@ use MongoDB\Client; class MyService { public function __construct( - #[AutowireClient('second')] + #[AutowireClient('second')] private Client $client, ) {} } ``` -## Database and Collection Usage +You can also use the `#[Target]` attribute: -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; +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 +`getDatabase` method, passing the database name and potential options: ```php use MongoDB\Client; @@ -141,12 +159,12 @@ class MyService public function __construct( Client $client, ) { - $this->database = $client->selectDatabase('myDatabase'); + $this->database = $client->getDatabase('myDatabase'); } } ``` -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 +179,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: @@ -192,8 +198,10 @@ 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: +## Collection Usage + +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 use MongoDB\Bundle\Attribute\AutowireCollection; @@ -230,6 +238,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 +254,54 @@ 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, + ) {} +} +``` + +## 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..7f64792 100644 --- a/composer.json +++ b/composer.json @@ -13,20 +13,23 @@ ], "require": { "php": ">=8.1", - "mongodb/mongodb": "^1.16", + "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", "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", - "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", "symfony/phpunit-bridge": "~6.3.10 || ^6.4.1 || ^7.0.1", + "symfony/stopwatch": "^6.3 || ^7.0", + "symfony/web-profiler-bundle": "^6.3 || ^7.0", "symfony/yaml": "^6.3 || ^7.0", "zenstruck/browser": "^1.6" }, 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/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/Attribute/AutowireCollection.php b/src/Attribute/AutowireCollection.php index 957d77b..5627157 100644 --- a/src/Attribute/AutowireCollection.php +++ b/src/Attribute/AutowireCollection.php @@ -23,7 +23,11 @@ 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; @@ -44,7 +48,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 @@ -52,19 +60,34 @@ public function __construct( : MongoDBExtension::createClientServiceId($client); parent::__construct( - callable: [new Reference($this->serviceId), 'selectCollection'], + callable: [new Reference($this->serviceId), 'getCollection'], lazy: $lazy, ); } 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 1c94f35..422102c 100644 --- a/src/Attribute/AutowireDatabase.php +++ b/src/Attribute/AutowireDatabase.php @@ -23,13 +23,18 @@ 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; use Symfony\Component\DependencyInjection\Reference; use function is_string; +use function sprintf; /** * Autowires a MongoDB database. @@ -37,27 +42,51 @@ #[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 = [], + 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, ) { - $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), 'getDatabase'], lazy: $lazy, ); } 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 ?? $parameter->getName(), $this->options]) + ->setArguments([ + $this->database ?? sprintf('%%%s.default_database%%', $this->serviceId), + $options, + ]) ->setLazy($this->lazy); } } 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..c83f24a --- /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, 'builderEncoder' => 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/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..ff23ce3 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['uriOptions'] ?? []); - $clientDefinition->setArgument('$driverOptions', $configuration['driverOptions'] ?? []); - + $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'])) { @@ -79,19 +78,19 @@ 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 $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..a9e1ce6 100644 --- a/src/MongoDBBundle.php +++ b/src/MongoDBBundle.php @@ -20,7 +20,9 @@ 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; use Symfony\Component\HttpKernel\Bundle\AbstractBundle; @@ -30,4 +32,9 @@ public function getContainerExtension(): ?ExtensionInterface { return new MongoDBExtension(); } + + public function build(ContainerBuilder $container): void + { + $container->addCompilerPass(new 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/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/Functional/DataCollector/DriverEventSubscriberTest.php b/tests/Functional/DataCollector/DriverEventSubscriberTest.php new file mode 100644 index 0000000..2f9a2ce --- /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()->getCollection('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/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/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')] 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/AttributeTestCase.php b/tests/Unit/Attribute/AttributeTestCase.php new file mode 100644 index 0000000..0a9ec3a --- /dev/null +++ b/tests/Unit/Attribute/AttributeTestCase.php @@ -0,0 +1,89 @@ + $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..4a79d1b 100644 --- a/tests/Unit/Attribute/AutowireCollectionTest.php +++ b/tests/Unit/Attribute/AutowireCollectionTest.php @@ -23,20 +23,19 @@ 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 { $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, @@ -61,10 +60,9 @@ public function testCollection(): void collection: 'test', database: 'mydb', client: 'default', - options: ['foo' => 'bar'], ); - $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, @@ -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,10 +86,9 @@ public function testWithoutCollection(): void $autowire = new AutowireCollection( database: 'mydb', client: 'default', - options: ['foo' => 'bar'], ); - $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, @@ -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 4ccab63..d6bc805 100644 --- a/tests/Unit/Attribute/AutowireDatabaseTest.php +++ b/tests/Unit/Attribute/AutowireDatabaseTest.php @@ -22,19 +22,19 @@ 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 { $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, @@ -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 @@ -56,10 +56,9 @@ public function testDatabase(): void $autowire = new AutowireDatabase( database: 'mydb', client: 'default', - options: ['foo' => 'bar'], ); - $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, @@ -74,17 +73,16 @@ 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); + $this->assertEquals([new Reference('mongodb.client.default'), 'getDatabase'], $autowire->value); $definition = $autowire->buildDefinition( value: $autowire->value, @@ -98,7 +96,28 @@ 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(['foo' => 'bar'], $definition->getArgument(1)); + $this->assertSame('%mongodb.client.default.default_database%', $definition->getArgument(0)); + $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)); } } 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..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([[ @@ -75,15 +107,17 @@ 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)], ], ], ], + ], [ + '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);