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
+
+
+
+