diff --git a/DataCollector/DoctrineDataCollector.php b/DataCollector/DoctrineDataCollector.php index 4c211d7c3..871de2486 100644 --- a/DataCollector/DoctrineDataCollector.php +++ b/DataCollector/DoctrineDataCollector.php @@ -13,6 +13,7 @@ use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory; use Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector as BaseCollector; +use Symfony\Bridge\Doctrine\Middleware\Debug\DebugDataHolder; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Throwable; @@ -64,12 +65,18 @@ class DoctrineDataCollector extends BaseCollector /** @var bool */ private $shouldValidateSchema; - public function __construct(ManagerRegistry $registry, bool $shouldValidateSchema = true) + /** @psalm-suppress UndefinedClass */ + public function __construct(ManagerRegistry $registry, bool $shouldValidateSchema = true, ?DebugDataHolder $debugDataHolder = null) { $this->registry = $registry; $this->shouldValidateSchema = $shouldValidateSchema; - parent::__construct($registry); + if ($debugDataHolder === null) { + parent::__construct($registry); + } else { + /** @psalm-suppress TooManyArguments */ + parent::__construct($registry, $debugDataHolder); + } } /** diff --git a/DependencyInjection/DoctrineExtension.php b/DependencyInjection/DoctrineExtension.php index 32635920e..02fda0b63 100644 --- a/DependencyInjection/DoctrineExtension.php +++ b/DependencyInjection/DoctrineExtension.php @@ -16,7 +16,6 @@ use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection; use Doctrine\DBAL\Driver\Middleware as MiddlewareInterface; use Doctrine\DBAL\Logging\LoggerChain; -use Doctrine\DBAL\Logging\Middleware; use Doctrine\DBAL\Sharding\PoolingShardConnection; use Doctrine\DBAL\Sharding\PoolingShardManager; use Doctrine\DBAL\Tools\Console\Command\ImportCommand; @@ -34,6 +33,7 @@ use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator; use Symfony\Bridge\Doctrine\Messenger\DoctrineClearEntityManagerWorkerSubscriber; use Symfony\Bridge\Doctrine\Messenger\DoctrineTransactionMiddleware; +use Symfony\Bridge\Doctrine\Middleware\Debug\Middleware as SfDebugMiddleware; use Symfony\Bridge\Doctrine\PropertyInfo\DoctrineExtractor; use Symfony\Bridge\Doctrine\SchemaListener\DoctrineDbalCacheAdapterSchemaSubscriber; use Symfony\Bridge\Doctrine\SchemaListener\MessengerTransportDoctrineSchemaSubscriber; @@ -116,9 +116,6 @@ protected function dbalLoad(array $config, ContainerBuilder $container) { $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $loader->load('dbal.xml'); - $chainLogger = $container->getDefinition('doctrine.dbal.logger.chain'); - $logger = new Reference('doctrine.dbal.logger'); - $chainLogger->addArgument([$logger]); if (class_exists(ImportCommand::class)) { $container->register('doctrine.database_import_command', ImportDoctrineCommand::class) @@ -147,12 +144,22 @@ protected function dbalLoad(array $config, ContainerBuilder $container) $container->setParameter('doctrine.connections', $connections); $container->setParameter('doctrine.default_connection', $this->defaultConnection); - $connWithLogging = []; + $connWithLogging = []; + $connWithProfiling = []; + $connWithBacktrace = []; foreach ($config['connections'] as $name => $connection) { if ($connection['logging']) { $connWithLogging[] = $name; } + if ($connection['profiling']) { + $connWithProfiling[] = $name; + + if ($connection['profiling_collect_backtrace']) { + $connWithBacktrace[] = $name; + } + } + $this->loadDbalConnection($name, $connection, $container); } @@ -173,7 +180,7 @@ protected function dbalLoad(array $config, ContainerBuilder $container) }); } - $this->useMiddlewaresIfAvailable($container, $connWithLogging); + $this->useMiddlewaresIfAvailable($container, $connWithLogging, $connWithProfiling, $connWithBacktrace); } /** @@ -187,7 +194,9 @@ protected function loadDbalConnection($name, array $connection, ContainerBuilder { $configuration = $container->setDefinition(sprintf('doctrine.dbal.%s_connection.configuration', $name), new ChildDefinition('doctrine.dbal.connection.configuration')); $logger = null; - if ($connection['logging']) { + + /** @psalm-suppress UndefinedClass */ + if (! interface_exists(MiddlewareInterface::class) && $connection['logging']) { $logger = new Reference('doctrine.dbal.logger'); } @@ -196,7 +205,7 @@ protected function loadDbalConnection($name, array $connection, ContainerBuilder $dataCollectorDefinition = $container->getDefinition('data_collector.doctrine'); $dataCollectorDefinition->replaceArgument(1, $connection['profiling_collect_schema_errors']); - if ($connection['profiling']) { + if (! $this->isSfDebugMiddlewareAvailable() && $connection['profiling']) { $profilingAbstractId = $connection['profiling_collect_backtrace'] ? 'doctrine.dbal.logger.backtrace' : 'doctrine.dbal.logger.profiling'; @@ -1116,24 +1125,50 @@ private function createArrayAdapterCachePool(ContainerBuilder $container, string return $id; } - /** @param string[] $connWithLogging */ - private function useMiddlewaresIfAvailable(ContainerBuilder $container, array $connWithLogging): void - { + /** + * @param string[] $connWithLogging + * @param string[] $connWithProfiling + * @param string[] $connWithBacktrace + */ + private function useMiddlewaresIfAvailable( + ContainerBuilder $container, + array $connWithLogging, + array $connWithProfiling, + array $connWithBacktrace + ): void { /** @psalm-suppress UndefinedClass */ - if (! class_exists(Middleware::class)) { + if (! interface_exists(MiddlewareInterface::class)) { return; } + $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); + $loader->load('middlewares.xml'); + $container ->getDefinition('doctrine.dbal.logger') ->replaceArgument(0, null); - $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); - $loader->load('middlewares.xml'); - $loggingMiddlewareAbstractDef = $container->getDefinition('doctrine.dbal.logging_middleware'); foreach ($connWithLogging as $connName) { $loggingMiddlewareAbstractDef->addTag('doctrine.middleware', ['connection' => $connName]); } + + if ($this->isSfDebugMiddlewareAvailable()) { + $container->getDefinition('doctrine.debug_data_holder')->replaceArgument(0, $connWithBacktrace); + $debugMiddlewareAbstractDef = $container->getDefinition('doctrine.dbal.debug_middleware'); + foreach ($connWithProfiling as $connName) { + $debugMiddlewareAbstractDef + ->addTag('doctrine.middleware', ['connection' => $connName]); + } + } else { + $container->removeDefinition('doctrine.dbal.debug_middleware'); + $container->removeDefinition('doctrine.debug_data_holder'); + } + } + + private function isSfDebugMiddlewareAvailable(): bool + { + /** @psalm-suppress UndefinedClass */ + return interface_exists(MiddlewareInterface::class) && class_exists(SfDebugMiddleware::class); } } diff --git a/Middleware/BacktraceDebugDataHolder.php b/Middleware/BacktraceDebugDataHolder.php new file mode 100644 index 000000000..0d075a943 --- /dev/null +++ b/Middleware/BacktraceDebugDataHolder.php @@ -0,0 +1,92 @@ +[]> */ + private $backtraces = []; + + /** @param string[] $connWithBacktraces */ + public function __construct(array $connWithBacktraces) + { + $this->connWithBacktraces = $connWithBacktraces; + } + + public function reset(): void + { + parent::reset(); + + $this->backtraces = []; + } + + public function addQuery(string $connectionName, Query $query): void + { + parent::addQuery($connectionName, $query); + + if (! in_array($connectionName, $this->connWithBacktraces, true)) { + return; + } + + // array_slice to skip middleware calls in the trace + $this->backtraces[$connectionName][] = array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), 2); + } + + /** @return array[]> */ + public function getData(): array + { + $dataWithBacktraces = []; + + $data = parent::getData(); + foreach ($data as $connectionName => $dataForConn) { + $dataWithBacktraces[$connectionName] = $this->getDataForConnection($connectionName, $dataForConn); + } + + return $dataWithBacktraces; + } + + /** + * @param mixed[][] $dataForConn + * + * @return mixed[][] + */ + private function getDataForConnection(string $connectionName, array $dataForConn): array + { + $data = []; + + foreach ($dataForConn as $idx => $record) { + $data[] = $this->addBacktracesIfAvailable($connectionName, $record, $idx); + } + + return $data; + } + + /** + * @param mixed[] $record + * + * @return mixed[] + */ + private function addBacktracesIfAvailable(string $connectionName, array $record, int $idx): array + { + if (! isset($this->backtraces[$connectionName])) { + return $record; + } + + $record['backtrace'] = $this->backtraces[$connectionName][$idx]; + + return $record; + } +} diff --git a/Middleware/DebugMiddleware.php b/Middleware/DebugMiddleware.php new file mode 100644 index 000000000..8026fdc02 --- /dev/null +++ b/Middleware/DebugMiddleware.php @@ -0,0 +1,37 @@ +debugDataHolder = $debugDataHolder; + $this->stopwatch = $stopwatch; + } + + public function setConnectionName(string $name): void + { + $this->connectionName = $name; + } + + public function wrap(DriverInterface $driver): DriverInterface + { + return new Driver($driver, $this->debugDataHolder, $this->stopwatch, $this->connectionName); + } +} diff --git a/Resources/config/middlewares.xml b/Resources/config/middlewares.xml index a0d408c52..8ba027c45 100644 --- a/Resources/config/middlewares.xml +++ b/Resources/config/middlewares.xml @@ -9,5 +9,12 @@ + + + + + + + diff --git a/Tests/ContainerTest.php b/Tests/ContainerTest.php index 2600face0..5503e43ff 100644 --- a/Tests/ContainerTest.php +++ b/Tests/ContainerTest.php @@ -10,6 +10,7 @@ use Doctrine\Common\EventManager; use Doctrine\DBAL\Configuration as DBALConfiguration; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Driver\Middleware as MiddlewareInterface; use Doctrine\DBAL\Logging\LoggerChain; use Doctrine\DBAL\Tools\Console\Command\ImportCommand; use Doctrine\DBAL\Types\Type; @@ -21,6 +22,7 @@ use Symfony\Bridge\Doctrine\CacheWarmer\ProxyCacheWarmer; use Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector; use Symfony\Bridge\Doctrine\Logger\DbalLogger; +use Symfony\Bridge\Doctrine\Middleware\Debug\Middleware as SfDebugMiddleware; use Symfony\Bridge\Doctrine\PropertyInfo\DoctrineExtractor; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator; use Symfony\Bridge\Doctrine\Validator\DoctrineLoader; @@ -32,16 +34,22 @@ class ContainerTest extends TestCase { + /** @group legacy */ public function testContainerWithDbalOnly(): void { $kernel = new DbalTestKernel(); $kernel->boot(); $container = $kernel->getContainer(); - $this->assertInstanceOf( - LoggerChain::class, - $container->get('doctrine.dbal.logger.chain.default') - ); + + /** @psalm-suppress UndefinedClass */ + if (! interface_exists(MiddlewareInterface::class)) { + $this->assertInstanceOf( + LoggerChain::class, + $container->get('doctrine.dbal.logger.chain.default') + ); + } + if (class_exists(ImportCommand::class)) { self::assertTrue($container->has('doctrine.database_import_command')); } else { @@ -57,7 +65,11 @@ public function testContainer(): void $container = $this->createXmlBundleTestContainer(); - $this->assertInstanceOf(DbalLogger::class, $container->get('doctrine.dbal.logger')); + /** @psalm-suppress UndefinedClass */ + if (! interface_exists(MiddlewareInterface::class) || ! class_exists(SfDebugMiddleware::class)) { + $this->assertInstanceOf(DbalLogger::class, $container->get('doctrine.dbal.logger')); + } + $this->assertInstanceOf(DoctrineDataCollector::class, $container->get('data_collector.doctrine')); $this->assertInstanceOf(DBALConfiguration::class, $container->get('doctrine.dbal.default_connection.configuration')); $this->assertInstanceOf(EventManager::class, $container->get('doctrine.dbal.default_connection.event_manager')); diff --git a/Tests/DependencyInjection/AbstractDoctrineExtensionTest.php b/Tests/DependencyInjection/AbstractDoctrineExtensionTest.php index 9c58ec35c..a5e30c0ec 100644 --- a/Tests/DependencyInjection/AbstractDoctrineExtensionTest.php +++ b/Tests/DependencyInjection/AbstractDoctrineExtensionTest.php @@ -12,6 +12,7 @@ use Doctrine\Common\Cache\Psr6\DoctrineProvider; use Doctrine\DBAL\Configuration; use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection; +use Doctrine\DBAL\Driver\Middleware; use Doctrine\DBAL\DriverManager; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManagerInterface; @@ -447,6 +448,11 @@ public function testLoadMultipleConnections(): void public function testLoadLogging(): void { + /** @psalm-suppress UndefinedClass */ + if (interface_exists(Middleware::class)) { + $this->markTestSkipped('This test requires Debug middleware to not be activated'); + } + $container = $this->loadContainer('dbal_logging'); $definition = $container->getDefinition('doctrine.dbal.log_connection.configuration'); diff --git a/Tests/DependencyInjection/DoctrineExtensionTest.php b/Tests/DependencyInjection/DoctrineExtensionTest.php index 146d22827..3468ff73c 100644 --- a/Tests/DependencyInjection/DoctrineExtensionTest.php +++ b/Tests/DependencyInjection/DoctrineExtensionTest.php @@ -16,7 +16,7 @@ use Doctrine\Common\Cache\XcacheCache; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver\Connection as DriverConnection; -use Doctrine\DBAL\Logging\Middleware; +use Doctrine\DBAL\Driver\Middleware as MiddlewareInterface; use Doctrine\DBAL\Sharding\PoolingShardManager; use Doctrine\DBAL\Sharding\SQLAzure\SQLAzureShardManager; use Doctrine\ORM\Cache\CacheConfiguration; @@ -39,6 +39,8 @@ use PHPUnit\Framework\TestCase; use ReflectionClass; use Symfony\Bridge\Doctrine\Messenger\DoctrineClearEntityManagerWorkerSubscriber; +use Symfony\Bridge\Doctrine\Middleware\Debug\DebugDataHolder; +use Symfony\Bridge\Doctrine\Middleware\Debug\Middleware as SfDebugMiddleware; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; use Symfony\Component\DependencyInjection\ChildDefinition; @@ -50,6 +52,7 @@ use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Messenger\MessageBusInterface; +use function array_merge; use function array_values; use function class_exists; use function in_array; @@ -1159,25 +1162,176 @@ public function testAsEntityListenerAttribute() $this->assertSame([$expected], $definition->getTag('doctrine.orm.entity_listener')); } - /** - * @return array - */ - public function provideDefinitionsToLogQueries(): array + /** @return bool[][] */ + public function provideRegistrationsWithoutMiddlewares(): array { return [ - 'with middlewares' => [true, false, true], - 'without middlewares' => [false, true, false], + 'SfDebugMiddleware not exists' => [false], + 'SfDebugMiddleware exists' => [true], ]; } /** - * @dataProvider provideDefinitionsToLogQueries + * @dataProvider provideRegistrationsWithoutMiddlewares */ - public function testDefinitionsToLogQueries(bool $withMiddleware, bool $loggerInjected, bool $middlewareRegistered): void + public function testRegistrationsWithoutMiddlewares(bool $sfDebugMiddlewareExists): void { /** @psalm-suppress UndefinedClass */ - if ($withMiddleware !== class_exists(Middleware::class)) { - $this->markTestSkipped(sprintf('%s needs %s to not exist', __METHOD__, Middleware::class)); + if (interface_exists(MiddlewareInterface::class)) { + $this->markTestSkipped(sprintf('%s needs %s to not exist', __METHOD__, MiddlewareInterface::class)); + } + + /** @psalm-suppress UndefinedClass */ + if ($sfDebugMiddlewareExists === ! class_exists(DebugDataHolder::class)) { // Can't verify SfDebugMiddleware existence directly since it implements MiddlewareInterface that is not available + $format = $sfDebugMiddlewareExists ? '%s needs %s to exist' : '%s needs %s to not exist'; + $this->markTestSkipped(sprintf($format, __METHOD__, SfDebugMiddleware::class)); + } + + $container = $this->getContainer(); + $extension = new DoctrineExtension(); + + $config = BundleConfigurationBuilder::createBuilderWithBaseValues() + ->addConnection([ + 'connections' => [ + 'conn1' => [ + 'password' => 'foo', + 'logging' => true, + 'profiling' => false, + ], + 'conn2' => [ + 'password' => 'bar', + 'logging' => false, + 'profiling' => true, + 'profiling_collect_backtrace' => false, + ], + 'conn3' => [ + 'password' => 'bar', + 'logging' => false, + 'profiling' => true, + 'profiling_collect_backtrace' => true, + ], + ], + ]) + ->addBaseEntityManager() + ->build(); + + $extension->load([$config], $container); + + $this->assertTrue($container->hasDefinition('doctrine.dbal.logger')); + $loggerDef = $container->getDefinition('doctrine.dbal.logger'); + $this->assertNotNull($loggerDef->getArgument(0)); + $this->assertFalse($container->hasDefinition('doctrine.dbal.logging_middleware')); + + $this->assertFalse($container->hasDefinition('doctrine.dbal.debug_middleware')); + $this->assertFalse($container->hasDefinition('doctrine.debug_data_holder')); + + $this->assertFalse($container->hasDefinition('doctrine.dbal.logger.profiling.conn1')); + $this->assertFalse($container->hasDefinition('doctrine.dbal.logger.backtrace.conn1')); + $this->assertTrue($container->hasDefinition('doctrine.dbal.logger.profiling.conn2')); + $this->assertFalse($container->hasDefinition('doctrine.dbal.logger.backtrace.conn2')); + $this->assertFalse($container->hasDefinition('doctrine.dbal.logger.profiling.conn3')); + $this->assertTrue($container->hasDefinition('doctrine.dbal.logger.backtrace.conn3')); + + $this->assertFalse($container->hasDefinition('doctrine.dbal.debug_middleware')); + } + + public function testRegistrationsWithMiddlewaresButWithoutSfDebugMiddleware(): void + { + /** @psalm-suppress UndefinedClass */ + if (! interface_exists(MiddlewareInterface::class)) { + $this->markTestSkipped(sprintf('%s needs %s to exist', __METHOD__, MiddlewareInterface::class)); + } + + /** @psalm-suppress UndefinedClass */ + if (class_exists(SfDebugMiddleware::class)) { + $this->markTestSkipped(sprintf('%s needs %s to not exist', __METHOD__, SfDebugMiddleware::class)); + } + + $container = $this->getContainer(); + $extension = new DoctrineExtension(); + + $config = BundleConfigurationBuilder::createBuilderWithBaseValues() + ->addConnection([ + 'connections' => [ + 'conn1' => [ + 'password' => 'foo', + 'logging' => true, + 'profiling' => false, + ], + 'conn2' => [ + 'password' => 'bar', + 'logging' => false, + 'profiling' => true, + 'profiling_collect_backtrace' => false, + ], + 'conn3' => [ + 'password' => 'bar', + 'logging' => false, + 'profiling' => true, + 'profiling_collect_backtrace' => true, + ], + ], + ]) + ->addBaseEntityManager() + ->build(); + + $extension->load([$config], $container); + + $this->assertTrue($container->hasDefinition('doctrine.dbal.logger')); + $loggerDef = $container->getDefinition('doctrine.dbal.logger'); + $this->assertNull($loggerDef->getArgument(0)); + + $methodCalls = array_merge( + $container->getDefinition('doctrine.dbal.conn1_connection.configuration')->getMethodCalls(), + $container->getDefinition('doctrine.dbal.conn2_connection.configuration')->getMethodCalls(), + $container->getDefinition('doctrine.dbal.conn3_connection.configuration')->getMethodCalls() + ); + + foreach ($methodCalls as $methodCall) { + if ($methodCall[0] !== 'setSQLLogger' || ! (($methodCall[1][0] ?? null) instanceof Reference) || (string) $methodCall[1][0] !== 'doctrine.dbal.logger') { + continue; + } + + $this->fail('doctrine.dbal.logger should not be referenced on configurations'); + } + + $this->assertTrue($container->hasDefinition('doctrine.dbal.logging_middleware')); + + $abstractMiddlewareDefTags = $container->getDefinition('doctrine.dbal.logging_middleware')->getTags(); + $loggingMiddlewareTagAttributes = []; + foreach ($abstractMiddlewareDefTags as $tag => $attributes) { + if ($tag !== 'doctrine.middleware') { + continue; + } + + $loggingMiddlewareTagAttributes = $attributes; + } + + $this->assertTrue(in_array(['connection' => 'conn1'], $loggingMiddlewareTagAttributes, true)); + $this->assertFalse(in_array(['connection' => 'conn2'], $loggingMiddlewareTagAttributes, true)); + $this->assertFalse(in_array(['connection' => 'conn3'], $loggingMiddlewareTagAttributes, true)); + + $this->assertFalse($container->hasDefinition('doctrine.dbal.debug_middleware')); + $this->assertFalse($container->hasDefinition('doctrine.debug_data_holder')); + + $this->assertFalse($container->hasDefinition('doctrine.dbal.logger.profiling.conn1')); + $this->assertFalse($container->hasDefinition('doctrine.dbal.logger.backtrace.conn1')); + $this->assertTrue($container->hasDefinition('doctrine.dbal.logger.profiling.conn2')); + $this->assertFalse($container->hasDefinition('doctrine.dbal.logger.backtrace.conn2')); + $this->assertFalse($container->hasDefinition('doctrine.dbal.logger.profiling.conn3')); + $this->assertTrue($container->hasDefinition('doctrine.dbal.logger.backtrace.conn3')); + } + + public function testRegistrationsWithMiddlewaresAndSfDebugMiddleware(): void + { + /** @psalm-suppress UndefinedClass */ + if (! interface_exists(MiddlewareInterface::class)) { + $this->markTestSkipped(sprintf('%s needs %s to exist', __METHOD__, MiddlewareInterface::class)); + } + + /** @psalm-suppress UndefinedClass */ + if (! class_exists(SfDebugMiddleware::class)) { + $this->markTestSkipped(sprintf('%s needs %s to exist', __METHOD__, SfDebugMiddleware::class)); } $container = $this->getContainer(); @@ -1189,10 +1343,19 @@ public function testDefinitionsToLogQueries(bool $withMiddleware, bool $loggerIn 'conn1' => [ 'password' => 'foo', 'logging' => true, + 'profiling' => false, ], 'conn2' => [ 'password' => 'bar', 'logging' => false, + 'profiling' => true, + 'profiling_collect_backtrace' => false, + ], + 'conn3' => [ + 'password' => 'bar', + 'logging' => false, + 'profiling' => true, + 'profiling_collect_backtrace' => true, ], ], ]) @@ -1201,27 +1364,172 @@ public function testDefinitionsToLogQueries(bool $withMiddleware, bool $loggerIn $extension->load([$config], $container); + $this->assertTrue($container->hasDefinition('doctrine.dbal.logger')); $loggerDef = $container->getDefinition('doctrine.dbal.logger'); - $this->assertSame($loggerInjected, $loggerDef->getArgument(0) !== null); + $this->assertNull($loggerDef->getArgument(0)); + + $methodCalls = array_merge( + $container->getDefinition('doctrine.dbal.conn1_connection.configuration')->getMethodCalls(), + $container->getDefinition('doctrine.dbal.conn2_connection.configuration')->getMethodCalls(), + $container->getDefinition('doctrine.dbal.conn3_connection.configuration')->getMethodCalls() + ); + + foreach ($methodCalls as $methodCall) { + if ($methodCall[0] !== 'setSQLLogger' || ! (($methodCall[1][0] ?? null) instanceof Reference) || (string) $methodCall[1][0] !== 'doctrine.dbal.logger') { + continue; + } + + $this->fail('doctrine.dbal.logger should not be referenced on configurations'); + } + + $this->assertTrue($container->hasDefinition('doctrine.dbal.logging_middleware')); + + $abstractMiddlewareDefTags = $container->getDefinition('doctrine.dbal.logging_middleware')->getTags(); + $loggingMiddlewareTagAttributes = []; + foreach ($abstractMiddlewareDefTags as $tag => $attributes) { + if ($tag !== 'doctrine.middleware') { + continue; + } + + $loggingMiddlewareTagAttributes = $attributes; + } + + $this->assertTrue(in_array(['connection' => 'conn1'], $loggingMiddlewareTagAttributes, true)); + $this->assertFalse(in_array(['connection' => 'conn2'], $loggingMiddlewareTagAttributes, true)); + $this->assertFalse(in_array(['connection' => 'conn3'], $loggingMiddlewareTagAttributes, true)); + + $this->assertTrue($container->hasDefinition('doctrine.dbal.debug_middleware')); + $this->assertTrue($container->hasDefinition('doctrine.debug_data_holder')); + + $this->assertFalse($container->hasDefinition('doctrine.dbal.logger.profiling.conn1')); + $this->assertFalse($container->hasDefinition('doctrine.dbal.logger.backtrace.conn1')); + $this->assertFalse($container->hasDefinition('doctrine.dbal.logger.profiling.conn2')); + $this->assertFalse($container->hasDefinition('doctrine.dbal.logger.backtrace.conn2')); + $this->assertFalse($container->hasDefinition('doctrine.dbal.logger.profiling.conn3')); + $this->assertFalse($container->hasDefinition('doctrine.dbal.logger.backtrace.conn3')); + + $abstractMiddlewareDefTags = $container->getDefinition('doctrine.dbal.debug_middleware')->getTags(); + $debugMiddlewareTagAttributes = []; + foreach ($abstractMiddlewareDefTags as $tag => $attributes) { + if ($tag !== 'doctrine.middleware') { + continue; + } + + $debugMiddlewareTagAttributes = $attributes; + } + + $this->assertFalse(in_array(['connection' => 'conn1'], $debugMiddlewareTagAttributes, true)); + $this->assertTrue(in_array(['connection' => 'conn2'], $debugMiddlewareTagAttributes, true)); + $this->assertTrue(in_array(['connection' => 'conn3'], $debugMiddlewareTagAttributes, true)); + + $arguments = $container->getDefinition('doctrine.debug_data_holder')->getArguments(); + $this->assertCount(1, $arguments); + $this->assertSame(['conn3'], $arguments[0]); + } + + /** + * @return array + */ + public function provideDefinitionsToLogAndProfile(): array + { + return [ + 'with middlewares, with debug middleware' => [true, true, null, true], + 'with middlewares, without debug middleware' => [true, false, false, true], + 'without middlewares, with debug middleware' => [false, true, true, false], + 'without middlewares, without debug middleware' => [false, false, true, false], + ]; + } - $this->assertSame($middlewareRegistered, $container->hasDefinition('doctrine.dbal.logging_middleware')); + /** + * @dataProvider provideDefinitionsToLogAndProfile + */ + public function testDefinitionsToLogAndProfile( + bool $withMiddleware, + bool $withDebugMiddleware, + ?bool $loggerInjected, + bool $loggingMiddlewareRegistered + ): void { + /** @psalm-suppress UndefinedClass */ + if ($withMiddleware !== interface_exists(MiddlewareInterface::class)) { + $this->markTestSkipped(sprintf('%s needs %s to not exist', __METHOD__, MiddlewareInterface::class)); + } + + /** @psalm-suppress UndefinedClass */ + if ($withDebugMiddleware !== class_exists(SfDebugMiddleware::class, false)) { + $this->markTestSkipped(sprintf('%s needs %s to not exist', __METHOD__, SfDebugMiddleware::class)); + } + + $container = $this->getContainer(); + $extension = new DoctrineExtension(); + + $config = BundleConfigurationBuilder::createBuilderWithBaseValues() + ->addConnection([ + 'connections' => [ + 'conn1' => [ + 'password' => 'foo', + 'logging' => true, + 'profiling' => false, + ], + 'conn2' => [ + 'password' => 'bar', + 'logging' => false, + 'profiling' => true, + ], + ], + ]) + ->addBaseEntityManager() + ->build(); + + $extension->load([$config], $container); + + if ($loggerInjected !== null) { + $loggerDef = $container->getDefinition('doctrine.dbal.logger'); + $this->assertSame($loggerInjected, $loggerDef->getArgument(0) !== null); + } + + $this->assertSame($loggingMiddlewareRegistered, $container->hasDefinition('doctrine.dbal.logging_middleware')); if (! $withMiddleware) { return; } - $abstractMiddlewareDefTags = $container->getDefinition('doctrine.dbal.logging_middleware')->getTags(); - $middleWareTagAttributes = []; + $abstractMiddlewareDefTags = $container->getDefinition('doctrine.dbal.logging_middleware')->getTags(); + $loggingMiddlewareTagAttributes = []; + foreach ($abstractMiddlewareDefTags as $tag => $attributes) { + if ($tag !== 'doctrine.middleware') { + continue; + } + + $loggingMiddlewareTagAttributes = $attributes; + } + + $this->assertTrue(in_array(['connection' => 'conn1'], $loggingMiddlewareTagAttributes, true), 'Tag with connection conn1 not found for doctrine.dbal.logging_middleware'); + $this->assertFalse(in_array(['connection' => 'conn2'], $loggingMiddlewareTagAttributes, true), 'Tag with connection conn2 found for doctrine.dbal.logging_middleware'); + + if (! $withDebugMiddleware) { + $this->assertFalse($container->hasDefinition('doctrine.dbal.debug_middleware'), 'doctrine.dbal.debug_middleware not removed'); + $this->assertFalse($container->hasDefinition('doctrine.debug_data_holder'), 'doctrine.debug_data_holder not removed'); + $this->assertFalse($container->hasDefinition('doctrine.dbal.logger.profiling.conn1')); + $this->assertTrue($container->hasDefinition('doctrine.dbal.logger.profiling.conn2')); + + return; + } + + $this->assertFalse($container->hasDefinition('doctrine.dbal.logger.profiling.conn1')); + $this->assertFalse($container->hasDefinition('doctrine.dbal.logger.profiling.conn2')); + + $abstractMiddlewareDefTags = $container->getDefinition('doctrine.dbal.debug_middleware')->getTags(); + $debugMiddlewareTagAttributes = []; foreach ($abstractMiddlewareDefTags as $tag => $attributes) { if ($tag !== 'doctrine.middleware') { continue; } - $middleWareTagAttributes = $attributes; + $debugMiddlewareTagAttributes = $attributes; } - $this->assertTrue(in_array(['connection' => 'conn1'], $middleWareTagAttributes, true), 'Tag with connection conn1 not found'); - $this->assertFalse(in_array(['connection' => 'conn2'], $middleWareTagAttributes, true), 'Tag with connection conn2 found'); + $this->assertFalse(in_array(['connection' => 'conn1'], $debugMiddlewareTagAttributes, true), 'Tag with connection conn1 found for doctrine.dbal.debug_middleware'); + $this->assertTrue(in_array(['connection' => 'conn2'], $debugMiddlewareTagAttributes, true), 'Tag with connection conn2 not found for doctrine.dbal.debug_middleware'); } public function testDefinitionsToLogQueriesLoggingFalse(): void diff --git a/Tests/Middleware/BacktraceDebugDataHolderTest.php b/Tests/Middleware/BacktraceDebugDataHolderTest.php new file mode 100644 index 000000000..dcb822283 --- /dev/null +++ b/Tests/Middleware/BacktraceDebugDataHolderTest.php @@ -0,0 +1,81 @@ +markTestSkipped(sprintf('This test needs %s to exist', DebugDataHolder::class)); + } + + public function testAddAndRetrieveData(): void + { + $sut = new BacktraceDebugDataHolder([]); + $sut->addQuery('myconn', new Query('SELECT * FROM product')); + + $data = $sut->getData(); + $this->assertCount(1, $data['myconn'] ?? []); + $current = $data['myconn'][0]; + + $this->assertSame(0, strpos($current['sql'] ?? '', 'SELECT * FROM product')); + $this->assertSame([], $current['params'] ?? null); + $this->assertSame([], $current['types'] ?? null); + } + + public function testReset(): void + { + $sut = new BacktraceDebugDataHolder([]); + $sut->addQuery('myconn', new Query('SELECT * FROM product')); + + $this->assertCount(1, $sut->getData()['myconn'] ?? []); + $sut->reset(); + $this->assertCount(0, $sut->getData()['myconn'] ?? []); + } + + public function testBacktracesEnabled(): void + { + $sut = new BacktraceDebugDataHolder(['myconn2']); + $this->funcForBacktraceGeneration($sut); + + $data = $sut->getData(); + $this->assertCount(1, $data['myconn1'] ?? []); + $this->assertCount(1, $data['myconn2'] ?? []); + $currentConn1 = $data['myconn1'][0]; + $currentConn2 = $data['myconn2'][0]; + + $this->assertCount(0, $currentConn1['backtrace'] ?? []); + $this->assertGreaterThan(0, count($currentConn2['backtrace'] ?? [])); + + $lastCall = $currentConn2['backtrace'][0]; + $this->assertSame(self::class, $lastCall['class']); + $this->assertSame(__FUNCTION__, $lastCall['function']); + } + + /** @psalm-suppress MissingDependency */ + private function funcForBacktraceGeneration(BacktraceDebugDataHolder $sut): void + { + $sut->addQuery('myconn1', new Query('SELECT * FROM product')); + $sut->addQuery('myconn2', new Query('SELECT * FROM car')); + } +} diff --git a/Tests/Middleware/DebugMiddlewareTest.php b/Tests/Middleware/DebugMiddlewareTest.php new file mode 100644 index 000000000..42857e592 --- /dev/null +++ b/Tests/Middleware/DebugMiddlewareTest.php @@ -0,0 +1,79 @@ +markTestSkipped(sprintf('This test needs %s to exist', Middleware::class)); + } + + if (class_exists(DebugDataHolder::class)) { + return; + } + + $this->markTestSkipped(sprintf('This test needs %s to exist', DebugDataHolder::class)); + } + + public function testData(): void + { + $configuration = new Configuration(); + $debugDataHolder = new BacktraceDebugDataHolder(['default']); + /** + * @psalm-suppress UndefinedMethod + * @psalm-suppress InvalidArgument + */ + $configuration->setMiddlewares([new DebugMiddleware($debugDataHolder, null)]); + + $conn = DriverManager::getConnection([ + 'driver' => 'pdo_sqlite', + 'memory' => true, + ], $configuration); + + $conn->executeQuery(<<getData(); + $this->assertCount(1, $data['default'] ?? []); + + $current = $data['default'][0]; + + $this->assertSame(0, strpos($current['sql'] ?? '', 'CREATE TABLE products')); + $this->assertSame([], $current['params'] ?? null); + $this->assertSame([], $current['types'] ?? null); + $this->assertGreaterThan(0, $current['executionMS'] ?? 0); + $this->assertSame(Connection::class, $current['backtrace'][0]['class'] ?? ''); + $this->assertSame('executeQuery', $current['backtrace'][0]['function'] ?? ''); + + $debugDataHolder->reset(); + $data = $debugDataHolder->getData(); + $this->assertCount(0, $data['default'] ?? []); + } +}