diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 20df2e87ee3cd..dd3fac396ecf8 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -75,13 +75,14 @@ Cache\IntegrationTests Doctrine\Common\Cache - Symfony\Component\Cache - Symfony\Component\Cache\Tests\Fixtures - Symfony\Component\Cache\Tests\Traits - Symfony\Component\Cache\Traits - Symfony\Component\Console - Symfony\Component\HttpFoundation - Symfony\Component\Uid + Symfony\Bridge\Doctrine\Middleware\Debug + Symfony\Component\Cache + Symfony\Component\Cache\Tests\Fixtures + Symfony\Component\Cache\Tests\Traits + Symfony\Component\Cache\Traits + Symfony\Component\Console + Symfony\Component\HttpFoundation + Symfony\Component\Uid diff --git a/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php b/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php index 8e292cf36b8d7..8e500b56c1fe3 100644 --- a/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php +++ b/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php @@ -15,6 +15,7 @@ use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\Type; use Doctrine\Persistence\ManagerRegistry; +use Symfony\Bridge\Doctrine\Middleware\Debug\DebugDataHolder; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; @@ -31,17 +32,19 @@ class DoctrineDataCollector extends DataCollector private $registry; private $connections; private $managers; + private $debugDataHolder; /** * @var DebugStack[] */ private $loggers = []; - public function __construct(ManagerRegistry $registry) + public function __construct(ManagerRegistry $registry, DebugDataHolder $debugDataHolder = null) { $this->registry = $registry; $this->connections = $registry->getConnectionNames(); $this->managers = $registry->getManagerNames(); + $this->debugDataHolder = $debugDataHolder; } /** @@ -56,23 +59,43 @@ public function addLogger(string $name, DebugStack $logger) * {@inheritdoc} */ public function collect(Request $request, Response $response, \Throwable $exception = null) + { + $this->data = [ + 'queries' => $this->collectQueries(), + 'connections' => $this->connections, + 'managers' => $this->managers, + ]; + } + + private function collectQueries(): array { $queries = []; + + if (null !== $this->debugDataHolder) { + foreach ($this->debugDataHolder->getData() as $name => $data) { + $queries[$name] = $this->sanitizeQueries($name, $data); + } + + return $queries; + } + foreach ($this->loggers as $name => $logger) { $queries[$name] = $this->sanitizeQueries($name, $logger->queries); } - $this->data = [ - 'queries' => $queries, - 'connections' => $this->connections, - 'managers' => $this->managers, - ]; + return $queries; } public function reset() { $this->data = []; + if (null !== $this->debugDataHolder) { + $this->debugDataHolder->reset(); + + return; + } + foreach ($this->loggers as $logger) { $logger->queries = []; $logger->currentQuery = 0; diff --git a/src/Symfony/Bridge/Doctrine/Middleware/Debug/Connection.php b/src/Symfony/Bridge/Doctrine/Middleware/Debug/Connection.php new file mode 100644 index 0000000000000..d085b0af0e3de --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Middleware/Debug/Connection.php @@ -0,0 +1,186 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Middleware\Debug; + +use Doctrine\DBAL\Driver\Connection as ConnectionInterface; +use Doctrine\DBAL\Driver\Middleware\AbstractConnectionMiddleware; +use Doctrine\DBAL\Driver\Result; +use Doctrine\DBAL\Driver\Statement as DriverStatement; +use Symfony\Component\Stopwatch\Stopwatch; + +/** + * @author Laurent VOULLEMIER + * + * @internal + */ +final class Connection extends AbstractConnectionMiddleware +{ + private $nestingLevel = 0; + private $debugDataHolder; + private $stopwatch; + private $connectionName; + + public function __construct(ConnectionInterface $connection, DebugDataHolder $debugDataHolder, ?Stopwatch $stopwatch, string $connectionName) + { + parent::__construct($connection); + + $this->debugDataHolder = $debugDataHolder; + $this->stopwatch = $stopwatch; + $this->connectionName = $connectionName; + } + + public function prepare(string $sql): DriverStatement + { + return new Statement( + parent::prepare($sql), + $this->debugDataHolder, + $this->connectionName, + $sql + ); + } + + public function query(string $sql): Result + { + $this->debugDataHolder->addQuery($this->connectionName, $query = new Query($sql)); + + if (null !== $this->stopwatch) { + $this->stopwatch->start('doctrine', 'doctrine'); + } + + $query->start(); + + try { + $result = parent::query($sql); + } finally { + $query->stop(); + + if (null !== $this->stopwatch) { + $this->stopwatch->stop('doctrine'); + } + } + + return $result; + } + + public function exec(string $sql): int + { + $this->debugDataHolder->addQuery($this->connectionName, $query = new Query($sql)); + + if (null !== $this->stopwatch) { + $this->stopwatch->start('doctrine', 'doctrine'); + } + + $query->start(); + + try { + $affectedRows = parent::exec($sql); + } finally { + $query->stop(); + + if (null !== $this->stopwatch) { + $this->stopwatch->stop('doctrine'); + } + } + + return $affectedRows; + } + + public function beginTransaction(): bool + { + $query = null; + if (1 === ++$this->nestingLevel) { + $this->debugDataHolder->addQuery($this->connectionName, $query = new Query('"START TRANSACTION"')); + } + + if (null !== $this->stopwatch) { + $this->stopwatch->start('doctrine', 'doctrine'); + } + + if (null !== $query) { + $query->start(); + } + + try { + $ret = parent::beginTransaction(); + } finally { + if (null !== $query) { + $query->stop(); + } + + if (null !== $this->stopwatch) { + $this->stopwatch->stop('doctrine'); + } + } + + return $ret; + } + + public function commit(): bool + { + $query = null; + if (1 === $this->nestingLevel--) { + $this->debugDataHolder->addQuery($this->connectionName, $query = new Query('"COMMIT"')); + } + + if (null !== $this->stopwatch) { + $this->stopwatch->start('doctrine', 'doctrine'); + } + + if (null !== $query) { + $query->start(); + } + + try { + $ret = parent::commit(); + } finally { + if (null !== $query) { + $query->stop(); + } + + if (null !== $this->stopwatch) { + $this->stopwatch->stop('doctrine'); + } + } + + return $ret; + } + + public function rollBack(): bool + { + $query = null; + if (1 === $this->nestingLevel--) { + $this->debugDataHolder->addQuery($this->connectionName, $query = new Query('"ROLLBACK"')); + } + + if (null !== $this->stopwatch) { + $this->stopwatch->start('doctrine', 'doctrine'); + } + + if (null !== $query) { + $query->start(); + } + + try { + $ret = parent::rollBack(); + } finally { + if (null !== $query) { + $query->stop(); + } + + if (null !== $this->stopwatch) { + $this->stopwatch->stop('doctrine'); + } + } + + return $ret; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Middleware/Debug/DebugDataHolder.php b/src/Symfony/Bridge/Doctrine/Middleware/Debug/DebugDataHolder.php new file mode 100644 index 0000000000000..2643cc7493830 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Middleware/Debug/DebugDataHolder.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Middleware\Debug; + +/** + * @author Laurent VOULLEMIER + */ +class DebugDataHolder +{ + private $data = []; + + public function addQuery(string $connectionName, Query $query): void + { + $this->data[$connectionName][] = [ + 'sql' => $query->getSql(), + 'params' => $query->getParams(), + 'types' => $query->getTypes(), + 'executionMS' => [$query, 'getDuration'], // stop() may not be called at this point + ]; + } + + public function getData(): array + { + foreach ($this->data as $connectionName => $dataForConn) { + foreach ($dataForConn as $idx => $data) { + if (\is_callable($data['executionMS'])) { + $this->data[$connectionName][$idx]['executionMS'] = $data['executionMS'](); + } + } + } + + return $this->data; + } + + public function reset(): void + { + $this->data = []; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Middleware/Debug/Driver.php b/src/Symfony/Bridge/Doctrine/Middleware/Debug/Driver.php new file mode 100644 index 0000000000000..7f7fdd3bf0d8d --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Middleware/Debug/Driver.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Middleware\Debug; + +use Doctrine\DBAL\Driver as DriverInterface; +use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware; +use Symfony\Component\Stopwatch\Stopwatch; + +/** + * @author Laurent VOULLEMIER + * + * @internal + */ +final class Driver extends AbstractDriverMiddleware +{ + private $debugDataHolder; + private $stopwatch; + private $connectionName; + + public function __construct(DriverInterface $driver, DebugDataHolder $debugDataHolder, ?Stopwatch $stopwatch, string $connectionName) + { + parent::__construct($driver); + + $this->debugDataHolder = $debugDataHolder; + $this->stopwatch = $stopwatch; + $this->connectionName = $connectionName; + } + + public function connect(array $params): Connection + { + return new Connection( + parent::connect($params), + $this->debugDataHolder, + $this->stopwatch, + $this->connectionName + ); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Middleware/Debug/Middleware.php b/src/Symfony/Bridge/Doctrine/Middleware/Debug/Middleware.php new file mode 100644 index 0000000000000..18f6a58d5e7a2 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Middleware/Debug/Middleware.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Middleware\Debug; + +use Doctrine\DBAL\Driver as DriverInterface; +use Doctrine\DBAL\Driver\Middleware as MiddlewareInterface; +use Symfony\Component\Stopwatch\Stopwatch; + +/** + * Middleware to collect debug data. + * + * @author Laurent VOULLEMIER + */ +final class Middleware implements MiddlewareInterface +{ + private $debugDataHolder; + private $stopwatch; + private $connectionName; + + public function __construct(DebugDataHolder $debugDataHolder, ?Stopwatch $stopwatch, string $connectionName = 'default') + { + $this->debugDataHolder = $debugDataHolder; + $this->stopwatch = $stopwatch; + $this->connectionName = $connectionName; + } + + public function wrap(DriverInterface $driver): DriverInterface + { + return new Driver($driver, $this->debugDataHolder, $this->stopwatch, $this->connectionName); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Middleware/Debug/Query.php b/src/Symfony/Bridge/Doctrine/Middleware/Debug/Query.php new file mode 100644 index 0000000000000..d652f620ce2e8 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Middleware/Debug/Query.php @@ -0,0 +1,121 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Middleware\Debug; + +use Doctrine\DBAL\ParameterType; + +/** + * @author Laurent VOULLEMIER + * + * @internal + */ +class Query +{ + private $params = []; + private $types = []; + + private $start; + private $duration; + + private $sql; + + public function __construct(string $sql) + { + $this->sql = $sql; + } + + public function start(): void + { + $this->start = microtime(true); + } + + public function stop(): void + { + if (null !== $this->start) { + $this->duration = microtime(true) - $this->start; + } + } + + /** + * @param string|int $param + * @param string|int|float|bool|null $variable + */ + public function setParam($param, &$variable, int $type): void + { + // Numeric indexes start at 0 in profiler + $idx = \is_int($param) ? $param - 1 : $param; + + $this->params[$idx] = &$variable; + $this->types[$idx] = $type; + } + + /** + * @param string|int $param + * @param string|int|float|bool|null $value + */ + public function setValue($param, $value, int $type): void + { + // Numeric indexes start at 0 in profiler + $idx = \is_int($param) ? $param - 1 : $param; + + $this->params[$idx] = $value; + $this->types[$idx] = $type; + } + + /** + * @param array $values + */ + public function setValues(array $values): void + { + foreach ($values as $param => $value) { + $this->setValue($param, $value, ParameterType::STRING); + } + } + + public function getSql(): string + { + return $this->sql; + } + + /** + * @return array + */ + public function getParams(): array + { + return $this->params; + } + + /** + * @return array + */ + public function getTypes(): array + { + return $this->types; + } + + /** + * Query duration in seconds. + */ + public function getDuration(): ?float + { + return $this->duration; + } + + public function __clone() + { + $copy = []; + foreach ($this->params as $param => $valueOrVariable) { + $copy[$param] = $valueOrVariable; + } + $this->params = $copy; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Middleware/Debug/Statement.php b/src/Symfony/Bridge/Doctrine/Middleware/Debug/Statement.php new file mode 100644 index 0000000000000..e52530e906dc2 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Middleware/Debug/Statement.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Middleware\Debug; + +use Doctrine\DBAL\Driver\Middleware\AbstractStatementMiddleware; +use Doctrine\DBAL\Driver\Result as ResultInterface; +use Doctrine\DBAL\Driver\Statement as StatementInterface; +use Doctrine\DBAL\ParameterType; + +/** + * @author Laurent VOULLEMIER + * + * @internal + */ +final class Statement extends AbstractStatementMiddleware +{ + private $debugDataHolder; + private $connectionName; + private $query; + + public function __construct(StatementInterface $statement, DebugDataHolder $debugDataHolder, string $connectionName, string $sql) + { + parent::__construct($statement); + + $this->debugDataHolder = $debugDataHolder; + $this->connectionName = $connectionName; + $this->query = new Query($sql); + } + + public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool + { + $this->query->setParam($param, $variable, $type); + + return parent::bindParam($param, $variable, $type, ...\array_slice(\func_get_args(), 3)); + } + + public function bindValue($param, $value, $type = ParameterType::STRING): bool + { + $this->query->setValue($param, $value, $type); + + return parent::bindValue($param, $value, $type); + } + + public function execute($params = null): ResultInterface + { + if (null !== $params) { + $this->query->setValues($params); + } + + // clone to prevent variables by reference to change + $this->debugDataHolder->addQuery($this->connectionName, $query = clone $this->query); + + $query->start(); + + try { + $result = parent::execute($params); + } finally { + $query->stop(); + } + + return $result; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php b/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php index 35fc48ff1536f..25cc33fb4ae9f 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php @@ -12,11 +12,14 @@ namespace Symfony\Bridge\Doctrine\Tests\DataCollector; use Doctrine\DBAL\Connection; -use Doctrine\DBAL\Logging\DebugStack; +use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\Persistence\ManagerRegistry; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector; +use Symfony\Bridge\Doctrine\Middleware\Debug\DebugDataHolder; +use Symfony\Bridge\Doctrine\Middleware\Debug\Query; +use Symfony\Bridge\PhpUnit\ClockMock; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\VarDumper\Cloner\Data; @@ -27,66 +30,40 @@ class_exists(\Doctrine\DBAL\Platforms\MySqlPlatform::class); class DoctrineDataCollectorTest extends TestCase { - public function testCollectConnections() - { - $c = $this->createCollector([]); - $c->collect(new Request(), new Response()); - $this->assertEquals(['default' => 'doctrine.dbal.default_connection'], $c->getConnections()); - } - - public function testCollectManagers() - { - $c = $this->createCollector([]); - $c->collect(new Request(), new Response()); - $this->assertEquals(['default' => 'doctrine.orm.default_entity_manager'], $c->getManagers()); - } + use DoctrineDataCollectorTestTrait; - public function testCollectQueryCount() + protected function setUp(): void { - $c = $this->createCollector([]); - $c->collect(new Request(), new Response()); - $this->assertEquals(0, $c->getQueryCount()); - - $queries = [ - ['sql' => 'SELECT * FROM table1', 'params' => [], 'types' => [], 'executionMS' => 0], - ]; - $c = $this->createCollector($queries); - $c->collect(new Request(), new Response()); - $this->assertEquals(1, $c->getQueryCount()); + ClockMock::register(self::class); + ClockMock::withClockMock(1500000000); } - public function testCollectTime() + public function testReset() { - $c = $this->createCollector([]); - $c->collect(new Request(), new Response()); - $this->assertEquals(0, $c->getTime()); - $queries = [ ['sql' => 'SELECT * FROM table1', 'params' => [], 'types' => [], 'executionMS' => 1], ]; $c = $this->createCollector($queries); $c->collect(new Request(), new Response()); - $this->assertEquals(1, $c->getTime()); - $queries = [ - ['sql' => 'SELECT * FROM table1', 'params' => [], 'types' => [], 'executionMS' => 1], - ['sql' => 'SELECT * FROM table2', 'params' => [], 'types' => [], 'executionMS' => 2], - ]; - $c = $this->createCollector($queries); + $c->reset(); $c->collect(new Request(), new Response()); - $this->assertEquals(3, $c->getTime()); + $c = unserialize(serialize($c)); + + $this->assertEquals([], $c->getQueries()); } /** * @dataProvider paramProvider */ - public function testCollectQueries($param, $types, $expected, $explainable, bool $runnable = true) + public function testCollectQueries($param, $types, $expected) { $queries = [ ['sql' => 'SELECT * FROM table1 WHERE field1 = ?1', 'params' => [$param], 'types' => $types, 'executionMS' => 1], ]; $c = $this->createCollector($queries); $c->collect(new Request(), new Response()); + $c = unserialize(serialize($c)); $collectedQueries = $c->getQueries(); @@ -102,8 +79,8 @@ public function testCollectQueries($param, $types, $expected, $explainable, bool $this->assertEquals($expected, $collectedParam); } - $this->assertEquals($explainable, $collectedQueries['default'][0]['explainable']); - $this->assertSame($runnable, $collectedQueries['default'][0]['runnable']); + $this->assertTrue($collectedQueries['default'][0]['explainable']); + $this->assertTrue($collectedQueries['default'][0]['runnable']); } public function testCollectQueryWithNoParams() @@ -114,6 +91,7 @@ public function testCollectQueryWithNoParams() ]; $c = $this->createCollector($queries); $c->collect(new Request(), new Response()); + $c = unserialize(serialize($c)); $collectedQueries = $c->getQueries(); $this->assertInstanceOf(Data::class, $collectedQueries['default'][0]['params']); @@ -126,36 +104,10 @@ public function testCollectQueryWithNoParams() $this->assertTrue($collectedQueries['default'][1]['runnable']); } - public function testCollectQueryWithNoTypes() - { - $queries = [ - ['sql' => 'SET sql_mode=(SELECT REPLACE(@@sql_mode, \'ONLY_FULL_GROUP_BY\', \'\'))', 'params' => [], 'types' => null, 'executionMS' => 1], - ]; - $c = $this->createCollector($queries); - $c->collect(new Request(), new Response()); - - $collectedQueries = $c->getQueries(); - $this->assertSame([], $collectedQueries['default'][0]['types']); - } - - public function testReset() - { - $queries = [ - ['sql' => 'SELECT * FROM table1', 'params' => [], 'types' => [], 'executionMS' => 1], - ]; - $c = $this->createCollector($queries); - $c->collect(new Request(), new Response()); - - $c->reset(); - $c->collect(new Request(), new Response()); - - $this->assertEquals(['default' => []], $c->getQueries()); - } - /** * @dataProvider paramProvider */ - public function testSerialization($param, array $types, $expected, $explainable, bool $runnable = true) + public function testSerialization($param, array $types, $expected) { $queries = [ ['sql' => 'SELECT * FROM table1 WHERE field1 = ?1', 'params' => [$param], 'types' => $types, 'executionMS' => 1], @@ -178,55 +130,17 @@ public function testSerialization($param, array $types, $expected, $explainable, $this->assertEquals($expected, $collectedParam); } - $this->assertEquals($explainable, $collectedQueries['default'][0]['explainable']); - $this->assertSame($runnable, $collectedQueries['default'][0]['runnable']); + $this->assertTrue($collectedQueries['default'][0]['explainable']); + $this->assertTrue($collectedQueries['default'][0]['runnable']); } public function paramProvider(): array { return [ - ['some value', [], 'some value', true], - [1, [], 1, true], - [true, [], true, true], - [null, [], null, true], - [new \DateTime('2011-09-11'), ['date'], '2011-09-11', true], - [fopen(__FILE__, 'r'), [], '/* Resource(stream) */', false, false], - [ - new \stdClass(), - [], - <<method('getConnection') ->willReturn($connection); - $logger = $this->createMock(DebugStack::class); - $logger->queries = $queries; + $debugDataHolder = new DebugDataHolder(); + $collector = new DoctrineDataCollector($registry, $debugDataHolder); + foreach ($queries as $queryData) { + $query = new Query($queryData['sql'] ?? ''); + foreach (($queryData['params'] ?? []) as $key => $value) { + if (\is_int($key)) { + ++$key; + } - $collector = new DoctrineDataCollector($registry); - $collector->addLogger('default', $logger); + $query->setValue($key, $value, $queryData['type'][$key] ?? ParameterType::STRING); + } - return $collector; - } -} + $query->start(); -class StringRepresentableClass -{ - public function __toString(): string - { - return 'string representation'; + $debugDataHolder->addQuery('default', $query); + + if (isset($queryData['executionMS'])) { + sleep($queryData['executionMS']); + } + $query->stop(); + } + + return $collector; } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTestTrait.php b/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTestTrait.php new file mode 100644 index 0000000000000..23977a3be9881 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTestTrait.php @@ -0,0 +1,79 @@ +createCollector([]); + $c->collect(new Request(), new Response()); + $c = unserialize(serialize($c)); + $this->assertEquals(['default' => 'doctrine.dbal.default_connection'], $c->getConnections()); + } + + public function testCollectManagers() + { + $c = $this->createCollector([]); + $c->collect(new Request(), new Response()); + $c = unserialize(serialize($c)); + $this->assertEquals(['default' => 'doctrine.orm.default_entity_manager'], $c->getManagers()); + } + + public function testCollectQueryCount() + { + $c = $this->createCollector([]); + $c->collect(new Request(), new Response()); + $c = unserialize(serialize($c)); + $this->assertEquals(0, $c->getQueryCount()); + + $queries = [ + ['sql' => 'SELECT * FROM table1', 'params' => [], 'types' => [], 'executionMS' => 0], + ]; + $c = $this->createCollector($queries); + $c->collect(new Request(), new Response()); + $c = unserialize(serialize($c)); + $this->assertEquals(1, $c->getQueryCount()); + } + + public function testCollectTime() + { + $c = $this->createCollector([]); + $c->collect(new Request(), new Response()); + $c = unserialize(serialize($c)); + $this->assertEquals(0, $c->getTime()); + + $queries = [ + ['sql' => 'SELECT * FROM table1', 'params' => [], 'types' => [], 'executionMS' => 1], + ]; + $c = $this->createCollector($queries); + $c->collect(new Request(), new Response()); + $c = unserialize(serialize($c)); + $this->assertEquals(1, $c->getTime()); + + $queries = [ + ['sql' => 'SELECT * FROM table1', 'params' => [], 'types' => [], 'executionMS' => 1], + ['sql' => 'SELECT * FROM table2', 'params' => [], 'types' => [], 'executionMS' => 2], + ]; + $c = $this->createCollector($queries); + $c->collect(new Request(), new Response()); + $c = unserialize(serialize($c)); + $this->assertEquals(3, $c->getTime()); + } + + public function testCollectQueryWithNoTypes() + { + $queries = [ + ['sql' => 'SET sql_mode=(SELECT REPLACE(@@sql_mode, \'ONLY_FULL_GROUP_BY\', \'\'))', 'params' => [], 'types' => null, 'executionMS' => 1], + ]; + $c = $this->createCollector($queries); + $c->collect(new Request(), new Response()); + $c = unserialize(serialize($c)); + + $collectedQueries = $c->getQueries(); + $this->assertSame([], $collectedQueries['default'][0]['types']); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorWithDebugStackTest.php b/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorWithDebugStackTest.php new file mode 100644 index 0000000000000..f0962eff3132d --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorWithDebugStackTest.php @@ -0,0 +1,195 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\DataCollector; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Logging\DebugStack; +use Doctrine\DBAL\Platforms\MySQLPlatform; +use Doctrine\Persistence\ManagerRegistry; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\VarDumper\Cloner\Data; +use Symfony\Component\VarDumper\Dumper\CliDumper; + +// Doctrine DBAL 2 compatibility +class_exists(\Doctrine\DBAL\Platforms\MySqlPlatform::class); + +/** + * @group legacy + */ +class DoctrineDataCollectorWithDebugStackTest extends TestCase +{ + use DoctrineDataCollectorTestTrait; + + public function testReset() + { + $queries = [ + ['sql' => 'SELECT * FROM table1', 'params' => [], 'types' => [], 'executionMS' => 1], + ]; + $c = $this->createCollector($queries); + $c->collect(new Request(), new Response()); + + $c->reset(); + $c->collect(new Request(), new Response()); + $c = unserialize(serialize($c)); + + $this->assertEquals(['default' => []], $c->getQueries()); + } + + /** + * @dataProvider paramProvider + */ + public function testCollectQueries($param, $types, $expected, $explainable, bool $runnable = true) + { + $queries = [ + ['sql' => 'SELECT * FROM table1 WHERE field1 = ?1', 'params' => [$param], 'types' => $types, 'executionMS' => 1], + ]; + $c = $this->createCollector($queries); + $c->collect(new Request(), new Response()); + $c = unserialize(serialize($c)); + + $collectedQueries = $c->getQueries(); + + $collectedParam = $collectedQueries['default'][0]['params'][0]; + if ($collectedParam instanceof Data) { + $dumper = new CliDumper($out = fopen('php://memory', 'r+')); + $dumper->setColors(false); + $collectedParam->dump($dumper); + $this->assertStringMatchesFormat($expected, print_r(stream_get_contents($out, -1, 0), true)); + } elseif (\is_string($expected)) { + $this->assertStringMatchesFormat($expected, $collectedParam); + } else { + $this->assertEquals($expected, $collectedParam); + } + + $this->assertEquals($explainable, $collectedQueries['default'][0]['explainable']); + $this->assertSame($runnable, $collectedQueries['default'][0]['runnable']); + } + + /** + * @dataProvider paramProvider + */ + public function testSerialization($param, array $types, $expected, $explainable, bool $runnable = true) + { + $queries = [ + ['sql' => 'SELECT * FROM table1 WHERE field1 = ?1', 'params' => [$param], 'types' => $types, 'executionMS' => 1], + ]; + $c = $this->createCollector($queries); + $c->collect(new Request(), new Response()); + $c = unserialize(serialize($c)); + + $collectedQueries = $c->getQueries(); + + $collectedParam = $collectedQueries['default'][0]['params'][0]; + if ($collectedParam instanceof Data) { + $dumper = new CliDumper($out = fopen('php://memory', 'r+')); + $dumper->setColors(false); + $collectedParam->dump($dumper); + $this->assertStringMatchesFormat($expected, print_r(stream_get_contents($out, -1, 0), true)); + } elseif (\is_string($expected)) { + $this->assertStringMatchesFormat($expected, $collectedParam); + } else { + $this->assertEquals($expected, $collectedParam); + } + + $this->assertEquals($explainable, $collectedQueries['default'][0]['explainable']); + $this->assertSame($runnable, $collectedQueries['default'][0]['runnable']); + } + + public function paramProvider(): array + { + return [ + ['some value', [], 'some value', true], + [1, [], 1, true], + [true, [], true, true], + [null, [], null, true], + [new \DateTime('2011-09-11'), ['date'], '2011-09-11', true], + [fopen(__FILE__, 'r'), [], '/* Resource(stream) */', false, false], + [ + new \stdClass(), + [], + <<getMockBuilder(Connection::class) + ->disableOriginalConstructor() + ->getMock(); + $connection->expects($this->any()) + ->method('getDatabasePlatform') + ->willReturn(new MySqlPlatform()); + + $registry = $this->createMock(ManagerRegistry::class); + $registry + ->expects($this->any()) + ->method('getConnectionNames') + ->willReturn(['default' => 'doctrine.dbal.default_connection']); + $registry + ->expects($this->any()) + ->method('getManagerNames') + ->willReturn(['default' => 'doctrine.orm.default_entity_manager']); + $registry->expects($this->any()) + ->method('getConnection') + ->willReturn($connection); + + $collector = new DoctrineDataCollector($registry); + $logger = $this->createMock(DebugStack::class); + $logger->queries = $queries; + $collector->addLogger('default', $logger); + + return $collector; + } +} + +class StringRepresentableClass +{ + public function __toString(): string + { + return 'string representation'; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Middleware/Debug/MiddlewareTest.php b/src/Symfony/Bridge/Doctrine/Tests/Middleware/Debug/MiddlewareTest.php new file mode 100644 index 0000000000000..e43cec8b98650 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Middleware/Debug/MiddlewareTest.php @@ -0,0 +1,253 @@ +markTestSkipped(sprintf('%s needed to run this test', MiddlewareInterface::class)); + } + + ClockMock::withClockMock(false); + } + + private function init(bool $withStopwatch = true): void + { + $this->stopwatch = $withStopwatch ? new Stopwatch() : null; + + $configuration = new Configuration(); + $this->debugDataHolder = new DebugDataHolder(); + $configuration->setMiddlewares([new Middleware($this->debugDataHolder, $this->stopwatch)]); + + $this->conn = DriverManager::getConnection([ + 'driver' => 'pdo_sqlite', + 'memory' => true, + ], $configuration); + + $this->conn->executeQuery(<< [ + static function(object $target, ...$args) { + return $target->executeStatement(...$args); + }, + ], + 'executeQuery' => [ + static function(object $target, ...$args): Result { + return $target->executeQuery(...$args); + }, + ], + ]; + } + + /** + * @dataProvider provideExecuteMethod + */ + public function testWithoutBinding(callable $executeMethod) + { + $this->init(); + + $executeMethod($this->conn, 'INSERT INTO products(name, price, stock) VALUES ("product1", 12.5, 5)'); + + $debug = $this->debugDataHolder->getData()['default'] ?? []; + $this->assertCount(2, $debug); + $this->assertSame('INSERT INTO products(name, price, stock) VALUES ("product1", 12.5, 5)', $debug[1]['sql']); + $this->assertSame([], $debug[1]['params']); + $this->assertSame([], $debug[1]['types']); + $this->assertGreaterThan(0, $debug[1]['executionMS']); + } + + /** + * @dataProvider provideExecuteMethod + */ + public function testWithValueBound(callable $executeMethod) + { + $this->init(); + + $stmt = $this->conn->prepare('INSERT INTO products(name, price, stock) VALUES (?, ?, ?)'); + $stmt->bindValue(1, 'product1'); + $stmt->bindValue(2, 12.5); + $stmt->bindValue(3, 5, ParameterType::INTEGER); + + $executeMethod($stmt); + + $debug = $this->debugDataHolder->getData()['default'] ?? []; + $this->assertCount(2, $debug); + $this->assertSame('INSERT INTO products(name, price, stock) VALUES (?, ?, ?)', $debug[1]['sql']); + $this->assertSame(['product1', 12.5, 5], $debug[1]['params']); + $this->assertSame([ParameterType::STRING, ParameterType::STRING, ParameterType::INTEGER], $debug[1]['types']); + $this->assertGreaterThan(0, $debug[1]['executionMS']); + } + + /** + * @dataProvider provideExecuteMethod + */ + public function testWithParamBound(callable $executeMethod) + { + $this->init(); + + $product = 'product1'; + $price = 12.5; + $stock = 5; + + $stmt = $this->conn->prepare('INSERT INTO products(name, price, stock) VALUES (?, ?, ?)'); + $stmt->bindParam(1, $product); + $stmt->bindParam(2, $price); + $stmt->bindParam(3, $stock, ParameterType::INTEGER); + + $executeMethod($stmt); + + // Debug data should not be affected by these changes + $product = 'product2'; + $price = 13.5; + $stock = 4; + + $debug = $this->debugDataHolder->getData()['default'] ?? []; + $this->assertCount(2, $debug); + $this->assertSame('INSERT INTO products(name, price, stock) VALUES (?, ?, ?)', $debug[1]['sql']); + $this->assertSame(['product1', '12.5', 5], $debug[1]['params']); + $this->assertSame([ParameterType::STRING, ParameterType::STRING, ParameterType::INTEGER], $debug[1]['types']); + $this->assertGreaterThan(0, $debug[1]['executionMS']); + } + + public function provideEndTransactionMethod(): array + { + return [ + 'commit' => [ + static function(Connection $conn): bool { + return $conn->commit(); + }, + '"COMMIT"', + ], + 'rollback' => [ + static function(Connection $conn): bool { + return $conn->rollBack(); + }, + '"ROLLBACK"', + ], + ]; + } + + /** + * @dataProvider provideEndTransactionMethod + */ + public function testTransaction(callable $endTransactionMethod, string $expectedEndTransactionDebug) + { + $this->init(); + + $this->conn->beginTransaction(); + $this->conn->beginTransaction(); + $this->conn->executeStatement('INSERT INTO products(name, price, stock) VALUES ("product1", 12.5, 5)'); + $endTransactionMethod($this->conn); + $endTransactionMethod($this->conn); + $this->conn->beginTransaction(); + $this->conn->executeStatement('INSERT INTO products(name, price, stock) VALUES ("product2", 15.5, 12)'); + $endTransactionMethod($this->conn); + + $debug = $this->debugDataHolder->getData()['default'] ?? []; + $this->assertCount(7, $debug); + $this->assertSame('"START TRANSACTION"', $debug[1]['sql']); + $this->assertGreaterThan(0, $debug[1]['executionMS']); + $this->assertSame('INSERT INTO products(name, price, stock) VALUES ("product1", 12.5, 5)', $debug[2]['sql']); + $this->assertGreaterThan(0, $debug[2]['executionMS']); + $this->assertSame($expectedEndTransactionDebug, $debug[3]['sql']); + $this->assertGreaterThan(0, $debug[3]['executionMS']); + $this->assertSame('"START TRANSACTION"', $debug[4]['sql']); + $this->assertGreaterThan(0, $debug[4]['executionMS']); + $this->assertSame('INSERT INTO products(name, price, stock) VALUES ("product2", 15.5, 12)', $debug[5]['sql']); + $this->assertGreaterThan(0, $debug[5]['executionMS']); + $this->assertSame($expectedEndTransactionDebug, $debug[6]['sql']); + $this->assertGreaterThan(0, $debug[6]['executionMS']); + } + + public function provideExecuteAndEndTransactionMethods(): array + { + return [ + 'commit and exec' => [ + static function(Connection $conn, string $sql) { + return $conn->executeStatement($sql); + }, + static function(Connection $conn): bool { + return $conn->commit(); + }, + ], + 'rollback and query' => [ + static function(Connection $conn, string $sql): Result { + return $conn->executeQuery($sql); + }, + static function(Connection $conn): bool { + return $conn->rollBack(); + }, + ], + ]; + } + + /** + * @dataProvider provideExecuteAndEndTransactionMethods + */ + public function testGlobalDoctrineDuration(callable $sqlMethod, callable $endTransactionMethod) + { + $this->init(); + + $periods = $this->stopwatch->getEvent('doctrine')->getPeriods(); + $this->assertCount(1, $periods); + + $this->conn->beginTransaction(); + + $this->assertFalse($this->stopwatch->getEvent('doctrine')->isStarted()); + $this->assertCount(2, $this->stopwatch->getEvent('doctrine')->getPeriods()); + + $sqlMethod($this->conn, 'SELECT * FROM products'); + + $this->assertFalse($this->stopwatch->getEvent('doctrine')->isStarted()); + $this->assertCount(3, $this->stopwatch->getEvent('doctrine')->getPeriods()); + + $endTransactionMethod($this->conn); + + $this->assertFalse($this->stopwatch->getEvent('doctrine')->isStarted()); + $this->assertCount(4, $this->stopwatch->getEvent('doctrine')->getPeriods()); + } + + /** + * @dataProvider provideExecuteAndEndTransactionMethods + */ + public function testWithoutStopwatch(callable $sqlMethod, callable $endTransactionMethod) + { + $this->init(false); + + $this->conn->beginTransaction(); + $sqlMethod($this->conn, 'SELECT * FROM products'); + $endTransactionMethod($this->conn); + } +} diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index 5d8e8485c73e9..dadb456cc2029 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -34,6 +34,7 @@ "symfony/http-kernel": "^5.0|^6.0", "symfony/messenger": "^4.4|^5.0|^6.0", "symfony/doctrine-messenger": "^5.1|^6.0", + "symfony/phpunit-bridge": "^4.4|^5.4|^6.0", "symfony/property-access": "^4.4|^5.0|^6.0", "symfony/property-info": "^5.0|^6.0", "symfony/proxy-manager-bridge": "^4.4|^5.0|^6.0", diff --git a/src/Symfony/Bridge/Doctrine/phpunit.xml.dist b/src/Symfony/Bridge/Doctrine/phpunit.xml.dist index fa76fa9b500e7..31a2546b47ec4 100644 --- a/src/Symfony/Bridge/Doctrine/phpunit.xml.dist +++ b/src/Symfony/Bridge/Doctrine/phpunit.xml.dist @@ -28,4 +28,14 @@ + + + + + + Symfony\Bridge\Doctrine\Middleware\Debug + + + + pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy