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