diff --git a/DataCollector/DoctrineDataCollector.php b/DataCollector/DoctrineDataCollector.php index 4c211d7c3..63ecb799d 100644 --- a/DataCollector/DoctrineDataCollector.php +++ b/DataCollector/DoctrineDataCollector.php @@ -2,311 +2,37 @@ namespace Doctrine\Bundle\DoctrineBundle\DataCollector; -use Doctrine\DBAL\Types\Type; -use Doctrine\ORM\Cache\CacheConfiguration; -use Doctrine\ORM\Cache\Logging\CacheLoggerChain; -use Doctrine\ORM\Cache\Logging\StatisticsCacheLogger; -use Doctrine\ORM\Configuration; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Mapping\ClassMetadataInfo; -use Doctrine\ORM\Tools\SchemaValidator; use Doctrine\Persistence\ManagerRegistry; -use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory; use Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector as BaseCollector; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Throwable; +use Symfony\Bridge\Doctrine\Middleware\Debug\DebugDataHolder; -use function array_map; -use function array_sum; -use function assert; -use function count; -use function usort; +use function class_exists; -/** - * @psalm-type QueryType = array{ - * executionMS: float, - * explainable: bool, - * sql: string, - * params: ?array, - * runnable: bool, - * types: ?array, - * } - * @psalm-type DataType = array{ - * caches: array{ - * enabled: bool, - * counts: array<"puts"|"hits"|"misses", int>, - * log_enabled: bool, - * regions: array<"puts"|"hits"|"misses", array>, - * }, - * connections: list, - * entities: array>, - * errors: array>>, - * managers: list, - * queries: array>, - * } - * @psalm-property DataType $data - */ -class DoctrineDataCollector extends BaseCollector -{ - /** @var ManagerRegistry */ - private $registry; - - /** @var int|null */ - private $invalidEntityCount; - - /** - * @var mixed[][] - * @psalm-var ?array> - */ - private $groupedQueries; - - /** @var bool */ - private $shouldValidateSchema; - - public function __construct(ManagerRegistry $registry, bool $shouldValidateSchema = true) - { - $this->registry = $registry; - $this->shouldValidateSchema = $shouldValidateSchema; - - parent::__construct($registry); - } - - /** - * {@inheritdoc} - */ - public function collect(Request $request, Response $response, ?Throwable $exception = null) - { - parent::collect($request, $response, $exception); - - $errors = []; - $entities = []; - $caches = [ - 'enabled' => false, - 'log_enabled' => false, - 'counts' => [ - 'puts' => 0, - 'hits' => 0, - 'misses' => 0, - ], - 'regions' => [ - 'puts' => [], - 'hits' => [], - 'misses' => [], - ], - ]; - - foreach ($this->registry->getManagers() as $name => $em) { - assert($em instanceof EntityManagerInterface); - if ($this->shouldValidateSchema) { - $entities[$name] = []; - - $factory = $em->getMetadataFactory(); - $validator = new SchemaValidator($em); - - assert($factory instanceof AbstractClassMetadataFactory); - - foreach ($factory->getLoadedMetadata() as $class) { - assert($class instanceof ClassMetadataInfo); - if (isset($entities[$name][$class->getName()])) { - continue; - } - - $classErrors = $validator->validateClass($class); - $entities[$name][$class->getName()] = $class->getName(); - - if (empty($classErrors)) { - continue; - } - - $errors[$name][$class->getName()] = $classErrors; - } - } - - $emConfig = $em->getConfiguration(); - assert($emConfig instanceof Configuration); - $slcEnabled = $emConfig->isSecondLevelCacheEnabled(); - - if (! $slcEnabled) { - continue; - } - - $caches['enabled'] = true; - - $cacheConfiguration = $emConfig->getSecondLevelCacheConfiguration(); - assert($cacheConfiguration instanceof CacheConfiguration); - $cacheLoggerChain = $cacheConfiguration->getCacheLogger(); - assert($cacheLoggerChain instanceof CacheLoggerChain || $cacheLoggerChain === null); - - if (! $cacheLoggerChain || ! $cacheLoggerChain->getLogger('statistics')) { - continue; - } - - $cacheLoggerStats = $cacheLoggerChain->getLogger('statistics'); - assert($cacheLoggerStats instanceof StatisticsCacheLogger); - $caches['log_enabled'] = true; - - $caches['counts']['puts'] += $cacheLoggerStats->getPutCount(); - $caches['counts']['hits'] += $cacheLoggerStats->getHitCount(); - $caches['counts']['misses'] += $cacheLoggerStats->getMissCount(); - - foreach ($cacheLoggerStats->getRegionsPut() as $key => $value) { - if (! isset($caches['regions']['puts'][$key])) { - $caches['regions']['puts'][$key] = 0; - } - - $caches['regions']['puts'][$key] += $value; - } - - foreach ($cacheLoggerStats->getRegionsHit() as $key => $value) { - if (! isset($caches['regions']['hits'][$key])) { - $caches['regions']['hits'][$key] = 0; - } - - $caches['regions']['hits'][$key] += $value; - } - - foreach ($cacheLoggerStats->getRegionsMiss() as $key => $value) { - if (! isset($caches['regions']['misses'][$key])) { - $caches['regions']['misses'][$key] = 0; - } - - $caches['regions']['misses'][$key] += $value; - } - } - - $this->data['entities'] = $entities; - $this->data['errors'] = $errors; - $this->data['caches'] = $caches; - $this->groupedQueries = null; - } - - /** @return array> */ - public function getEntities() - { - return $this->data['entities']; - } - - /** @return array>> */ - public function getMappingErrors() +// phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses +if (class_exists(DebugDataHolder::class)) { + class DoctrineDataCollector extends BaseCollector { - return $this->data['errors']; - } - - /** @return int */ - public function getCacheHitsCount() - { - return $this->data['caches']['counts']['hits']; - } - - /** @return int */ - public function getCachePutsCount() - { - return $this->data['caches']['counts']['puts']; - } + use DoctrineDataCollectorCommon; - /** @return int */ - public function getCacheMissesCount() - { - return $this->data['caches']['counts']['misses']; - } + public function __construct(ManagerRegistry $registry, bool $shouldValidateSchema = true, ?DebugDataHolder $debugDataHolder = null) + { + $this->registry = $registry; + $this->shouldValidateSchema = $shouldValidateSchema; - /** @return bool */ - public function getCacheEnabled() - { - return $this->data['caches']['enabled']; - } - - /** - * @return array> - * @psalm-return array<"puts"|"hits"|"misses", array> - */ - public function getCacheRegions() - { - return $this->data['caches']['regions']; - } - - /** @return array */ - public function getCacheCounts() - { - return $this->data['caches']['counts']; - } - - /** @return int */ - public function getInvalidEntityCount() - { - if ($this->invalidEntityCount === null) { - $this->invalidEntityCount = array_sum(array_map('count', $this->data['errors'])); + parent::__construct($registry, $debugDataHolder); } - - return $this->invalidEntityCount; - } - - /** - * @return string[][] - * @psalm-return array> - */ - public function getGroupedQueries() - { - if ($this->groupedQueries !== null) { - return $this->groupedQueries; - } - - $this->groupedQueries = []; - $totalExecutionMS = 0; - foreach ($this->data['queries'] as $connection => $queries) { - $connectionGroupedQueries = []; - foreach ($queries as $i => $query) { - $key = $query['sql']; - if (! isset($connectionGroupedQueries[$key])) { - $connectionGroupedQueries[$key] = $query; - $connectionGroupedQueries[$key]['executionMS'] = 0; - $connectionGroupedQueries[$key]['count'] = 0; - $connectionGroupedQueries[$key]['index'] = $i; // "Explain query" relies on query index in 'queries'. - } - - $connectionGroupedQueries[$key]['executionMS'] += $query['executionMS']; - $connectionGroupedQueries[$key]['count']++; - $totalExecutionMS += $query['executionMS']; - } - - usort($connectionGroupedQueries, static function ($a, $b) { - if ($a['executionMS'] === $b['executionMS']) { - return 0; - } - - return $a['executionMS'] < $b['executionMS'] ? 1 : -1; - }); - $this->groupedQueries[$connection] = $connectionGroupedQueries; - } - - foreach ($this->groupedQueries as $connection => $queries) { - foreach ($queries as $i => $query) { - $this->groupedQueries[$connection][$i]['executionPercent'] = - $this->executionTimePercentage($query['executionMS'], $totalExecutionMS); - } - } - - return $this->groupedQueries; } - - private function executionTimePercentage(float $executionTimeMS, float $totalExecutionTimeMS): float +} else { + class DoctrineDataCollector extends BaseCollector { - if (! $totalExecutionTimeMS) { - return 0; - } + use DoctrineDataCollectorCommon; - return $executionTimeMS / $totalExecutionTimeMS * 100; - } + public function __construct(ManagerRegistry $registry, bool $shouldValidateSchema = true) + { + $this->registry = $registry; + $this->shouldValidateSchema = $shouldValidateSchema; - /** @return int */ - public function getGroupedQueryCount() - { - $count = 0; - foreach ($this->getGroupedQueries() as $connectionGroupedQueries) { - $count += count($connectionGroupedQueries); + parent::__construct($registry); } - - return $count; } } diff --git a/DataCollector/DoctrineDataCollectorCommon.php b/DataCollector/DoctrineDataCollectorCommon.php new file mode 100644 index 000000000..848951919 --- /dev/null +++ b/DataCollector/DoctrineDataCollectorCommon.php @@ -0,0 +1,302 @@ +, + * runnable: bool, + * types: ?array, + * } + * @psalm-type DataType = array{ + * caches: array{ + * enabled: bool, + * counts: array<"puts"|"hits"|"misses", int>, + * log_enabled: bool, + * regions: array<"puts"|"hits"|"misses", array>, + * }, + * connections: list, + * entities: array>, + * errors: array>>, + * managers: list, + * queries: array>, + * } + * @psalm-property DataType $data + */ +trait DoctrineDataCollectorCommon +{ + /** @var ManagerRegistry */ + private $registry; + + /** @var int|null */ + private $invalidEntityCount; + + /** + * @var mixed[][] + * @psalm-var ?array> + */ + private $groupedQueries; + + /** @var bool */ + private $shouldValidateSchema; + + /** + * {@inheritdoc} + */ + public function collect(Request $request, Response $response, ?Throwable $exception = null) + { + parent::collect($request, $response, $exception); + + $errors = []; + $entities = []; + $caches = [ + 'enabled' => false, + 'log_enabled' => false, + 'counts' => [ + 'puts' => 0, + 'hits' => 0, + 'misses' => 0, + ], + 'regions' => [ + 'puts' => [], + 'hits' => [], + 'misses' => [], + ], + ]; + + foreach ($this->registry->getManagers() as $name => $em) { + assert($em instanceof EntityManagerInterface); + if ($this->shouldValidateSchema) { + $entities[$name] = []; + + $factory = $em->getMetadataFactory(); + $validator = new SchemaValidator($em); + + assert($factory instanceof AbstractClassMetadataFactory); + + foreach ($factory->getLoadedMetadata() as $class) { + assert($class instanceof ClassMetadataInfo); + if (isset($entities[$name][$class->getName()])) { + continue; + } + + $classErrors = $validator->validateClass($class); + $entities[$name][$class->getName()] = $class->getName(); + + if (empty($classErrors)) { + continue; + } + + $errors[$name][$class->getName()] = $classErrors; + } + } + + $emConfig = $em->getConfiguration(); + assert($emConfig instanceof Configuration); + $slcEnabled = $emConfig->isSecondLevelCacheEnabled(); + + if (! $slcEnabled) { + continue; + } + + $caches['enabled'] = true; + + $cacheConfiguration = $emConfig->getSecondLevelCacheConfiguration(); + assert($cacheConfiguration instanceof CacheConfiguration); + $cacheLoggerChain = $cacheConfiguration->getCacheLogger(); + assert($cacheLoggerChain instanceof CacheLoggerChain || $cacheLoggerChain === null); + + if (! $cacheLoggerChain || ! $cacheLoggerChain->getLogger('statistics')) { + continue; + } + + $cacheLoggerStats = $cacheLoggerChain->getLogger('statistics'); + assert($cacheLoggerStats instanceof StatisticsCacheLogger); + $caches['log_enabled'] = true; + + $caches['counts']['puts'] += $cacheLoggerStats->getPutCount(); + $caches['counts']['hits'] += $cacheLoggerStats->getHitCount(); + $caches['counts']['misses'] += $cacheLoggerStats->getMissCount(); + + foreach ($cacheLoggerStats->getRegionsPut() as $key => $value) { + if (! isset($caches['regions']['puts'][$key])) { + $caches['regions']['puts'][$key] = 0; + } + + $caches['regions']['puts'][$key] += $value; + } + + foreach ($cacheLoggerStats->getRegionsHit() as $key => $value) { + if (! isset($caches['regions']['hits'][$key])) { + $caches['regions']['hits'][$key] = 0; + } + + $caches['regions']['hits'][$key] += $value; + } + + foreach ($cacheLoggerStats->getRegionsMiss() as $key => $value) { + if (! isset($caches['regions']['misses'][$key])) { + $caches['regions']['misses'][$key] = 0; + } + + $caches['regions']['misses'][$key] += $value; + } + } + + $this->data['entities'] = $entities; + $this->data['errors'] = $errors; + $this->data['caches'] = $caches; + $this->groupedQueries = null; + } + + /** @return array> */ + public function getEntities() + { + return $this->data['entities']; + } + + /** @return array>> */ + public function getMappingErrors() + { + return $this->data['errors']; + } + + /** @return int */ + public function getCacheHitsCount() + { + return $this->data['caches']['counts']['hits']; + } + + /** @return int */ + public function getCachePutsCount() + { + return $this->data['caches']['counts']['puts']; + } + + /** @return int */ + public function getCacheMissesCount() + { + return $this->data['caches']['counts']['misses']; + } + + /** @return bool */ + public function getCacheEnabled() + { + return $this->data['caches']['enabled']; + } + + /** + * @return array> + * @psalm-return array<"puts"|"hits"|"misses", array> + */ + public function getCacheRegions() + { + return $this->data['caches']['regions']; + } + + /** @return array */ + public function getCacheCounts() + { + return $this->data['caches']['counts']; + } + + /** @return int */ + public function getInvalidEntityCount() + { + if ($this->invalidEntityCount === null) { + $this->invalidEntityCount = array_sum(array_map('count', $this->data['errors'])); + } + + return $this->invalidEntityCount; + } + + /** + * @return string[][] + * @psalm-return array> + */ + public function getGroupedQueries() + { + if ($this->groupedQueries !== null) { + return $this->groupedQueries; + } + + $this->groupedQueries = []; + $totalExecutionMS = 0; + foreach ($this->data['queries'] as $connection => $queries) { + $connectionGroupedQueries = []; + foreach ($queries as $i => $query) { + $key = $query['sql']; + if (! isset($connectionGroupedQueries[$key])) { + $connectionGroupedQueries[$key] = $query; + $connectionGroupedQueries[$key]['executionMS'] = 0; + $connectionGroupedQueries[$key]['count'] = 0; + $connectionGroupedQueries[$key]['index'] = $i; // "Explain query" relies on query index in 'queries'. + } + + $connectionGroupedQueries[$key]['executionMS'] += $query['executionMS']; + $connectionGroupedQueries[$key]['count']++; + $totalExecutionMS += $query['executionMS']; + } + + usort($connectionGroupedQueries, static function ($a, $b) { + if ($a['executionMS'] === $b['executionMS']) { + return 0; + } + + return $a['executionMS'] < $b['executionMS'] ? 1 : -1; + }); + $this->groupedQueries[$connection] = $connectionGroupedQueries; + } + + foreach ($this->groupedQueries as $connection => $queries) { + foreach ($queries as $i => $query) { + $this->groupedQueries[$connection][$i]['executionPercent'] = + $this->executionTimePercentage($query['executionMS'], $totalExecutionMS); + } + } + + return $this->groupedQueries; + } + + private function executionTimePercentage(float $executionTimeMS, float $totalExecutionTimeMS): float + { + if (! $totalExecutionTimeMS) { + return 0; + } + + return $executionTimeMS / $totalExecutionTimeMS * 100; + } + + /** @return int */ + public function getGroupedQueryCount() + { + $count = 0; + foreach ($this->getGroupedQueries() as $connectionGroupedQueries) { + $count += count($connectionGroupedQueries); + } + + return $count; + } +} diff --git a/DependencyInjection/DoctrineExtension.php b/DependencyInjection/DoctrineExtension.php index 206d9fb8e..231c6e03f 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; @@ -115,9 +115,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) @@ -146,12 +143,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); } @@ -172,7 +179,7 @@ protected function dbalLoad(array $config, ContainerBuilder $container) }); } - $this->useMiddlewaresIfAvailable($container, $connWithLogging); + $this->useMiddlewaresIfAvailable($container, $connWithLogging, $connWithProfiling, $connWithBacktrace); } /** @@ -186,7 +193,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 (! $this->isSfDebugMiddlewareAvailable() && $connection['logging']) { $logger = new Reference('doctrine.dbal.logger'); } @@ -195,7 +204,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'; @@ -1109,24 +1118,51 @@ 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->removeDefinition('doctrine.dbal.logger'); + $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..0796cc850 --- /dev/null +++ b/Middleware/BacktraceDebugDataHolder.php @@ -0,0 +1,94 @@ +connWithBacktraces = $connWithBacktraces; + } + + public function reset(): void + { + parent::reset(); + + $this->backtraces = []; + } + + public function addQuery(string $connectionName, Query $query): void + { + parent::addQuery($connectionName, $query); + + // array_slice to skip middleware calls in the trace + if (! in_array($connectionName, $this->connWithBacktraces, true)) { + return; + } + + $this->backtraces[$connectionName][] = array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), 2); + } + + /** @return mixed[][][] */ + 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; + } + + return array_merge( + $record, + ['backtrace' => $this->backtraces[$connectionName][$idx]] + ); + } +} 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/dbal.xml b/Resources/config/dbal.xml index ef46a32d3..bc8bb09d4 100644 --- a/Resources/config/dbal.xml +++ b/Resources/config/dbal.xml @@ -5,7 +5,6 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - Doctrine\DBAL\Logging\LoggerChain Doctrine\DBAL\Logging\DebugStack Symfony\Bridge\Doctrine\Logger\DbalLogger Doctrine\DBAL\Configuration @@ -23,10 +22,6 @@ - - - - @@ -40,6 +35,7 @@ true + 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 5348ac15e..087a399d4 100644 --- a/Tests/ContainerTest.php +++ b/Tests/ContainerTest.php @@ -9,6 +9,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; @@ -20,6 +21,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; @@ -37,10 +39,15 @@ public function testContainerWithDbalOnly(): void $kernel->boot(); $container = $kernel->getContainer(); - $this->assertInstanceOf( - LoggerChain::class, - $container->get('doctrine.dbal.logger.chain.default') - ); + + /** @psalm-suppress UndefinedClass */ + if (! interface_exists(MiddlewareInterface::class) || ! class_exists(SfDebugMiddleware::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 { @@ -56,7 +63,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 2be5b5258..ee35921ba 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; @@ -20,6 +21,7 @@ use PDO; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass\RegisterEventListenersAndSubscribersPass; +use Symfony\Bridge\Doctrine\Middleware\Debug\Middleware as SfDebugMiddleware; use Symfony\Bundle\DoctrineBundle\Tests\DependencyInjection\TestHydrator; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; @@ -38,6 +40,7 @@ use function array_keys; use function array_values; use function assert; +use function class_exists; use function end; use function interface_exists; use function is_dir; @@ -446,6 +449,11 @@ public function testLoadMultipleConnections(): void public function testLoadLogging(): void { + /** @psalm-suppress UndefinedClass */ + if (interface_exists(Middleware::class) && class_exists(SfDebugMiddleware::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 fe5881001..f41efb509 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; @@ -779,9 +781,7 @@ public function testAnnotationsBundleMappingDetection(): void ]); } - /** - * @requires PHP 8 - */ + /** @requires PHP 8 */ public function testAttributesBundleMappingDetection(): void { $container = $this->getContainer(['AttributesBundle']); @@ -1158,25 +1158,89 @@ 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(); @@ -1188,10 +1252,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, ], ], ]) @@ -1200,27 +1273,229 @@ 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)); + + $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)); + } - $this->assertSame($middlewareRegistered, $container->hasDefinition('doctrine.dbal.logging_middleware')); + /** @psalm-suppress UndefinedClass */ + if (! class_exists(SfDebugMiddleware::class)) { + $this->markTestSkipped(sprintf('%s needs %s to 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->assertFalse($container->hasDefinition('doctrine.dbal.logger')); + $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], + ]; + } + + /** + * @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)) { + $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'), 'doctrine.dbal.logger not removed'); + $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..e757b8148 --- /dev/null +++ b/Tests/Middleware/BacktraceDebugDataHolderTest.php @@ -0,0 +1,76 @@ +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']); + } + + 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..798e91b37 --- /dev/null +++ b/Tests/Middleware/DebugMiddlewareTest.php @@ -0,0 +1,64 @@ +markTestSkipped(sprintf('This test needs %s to exist', DebugDataHolder::class)); + } + } + + public function testData(): void + { + $configuration = new Configuration(); + /** @psalm-suppress MissingDependency */ + $debugDataHolder = new BacktraceDebugDataHolder(); + $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'] ?? []); + } +}