diff --git a/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php b/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php index 871eeee97d..b184a7163d 100644 --- a/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php +++ b/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php @@ -18,6 +18,8 @@ use Doctrine\DBAL\Types\Type; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; +use Symfony\Component\VarDumper\Caster\Caster; +use Symfony\Component\VarDumper\Cloner\Stub; /** * DoctrineDataCollector. @@ -121,6 +123,38 @@ class DoctrineDataCollector extends DataCollector return 'db'; } + /** + * {@inheritdoc} + */ + protected function getCasters() + { + return parent::getCasters() + [ + ObjectParameter::class => static function (ObjectParameter $o, array $a, Stub $s): array { + $s->class = $o->getClass(); + $s->value = $o->getObject(); + + $r = new \ReflectionClass($o->getClass()); + if ($f = $r->getFileName()) { + $s->attr['file'] = $f; + $s->attr['line'] = $r->getStartLine(); + } else { + unset($s->attr['file']); + unset($s->attr['line']); + } + + if ($error = $o->getError()) { + return [Caster::PREFIX_VIRTUAL.'⚠' => $error->getMessage()]; + } + + if ($o->isStringable()) { + return [Caster::PREFIX_VIRTUAL.'__toString()' => (string) $o->getObject()]; + } + + return [Caster::PREFIX_VIRTUAL.'⚠' => sprintf('Object of class "%s" could not be converted to string.', $o->getClass())]; + }, + ]; + } + private function sanitizeQueries(string $connectionName, array $queries): array { foreach ($queries as $i => $query) { @@ -133,6 +167,7 @@ class DoctrineDataCollector extends DataCollector private function sanitizeQuery(string $connectionName, array $query): array { $query['explainable'] = true; + $query['runnable'] = true; if (null === $query['params']) { $query['params'] = []; } @@ -143,6 +178,7 @@ class DoctrineDataCollector extends DataCollector $query['types'] = []; } foreach ($query['params'] as $j => $param) { + $e = null; if (isset($query['types'][$j])) { // Transform the param according to the type $type = $query['types'][$j]; @@ -154,18 +190,19 @@ class DoctrineDataCollector extends DataCollector try { $param = $type->convertToDatabaseValue($param, $this->registry->getConnection($connectionName)->getDatabasePlatform()); } catch (\TypeError $e) { - // Error thrown while processing params, query is not explainable. - $query['explainable'] = false; } catch (ConversionException $e) { - $query['explainable'] = false; } } } - list($query['params'][$j], $explainable) = $this->sanitizeParam($param); + list($query['params'][$j], $explainable, $runnable) = $this->sanitizeParam($param, $e); if (!$explainable) { $query['explainable'] = false; } + + if (!$runnable) { + $query['runnable'] = false; + } } $query['params'] = $this->cloneVar($query['params']); @@ -180,32 +217,33 @@ class DoctrineDataCollector extends DataCollector * indicating if the original value was kept (allowing to use the sanitized * value to explain the query). */ - private function sanitizeParam($var): array + private function sanitizeParam($var, ?\Throwable $error): array { if (\is_object($var)) { - $className = \get_class($var); + return [$o = new ObjectParameter($var, $error), false, $o->isStringable() && !$error]; + } - return method_exists($var, '__toString') ? - [sprintf('/* Object(%s): */"%s"', $className, $var->__toString()), false] : - [sprintf('/* Object(%s) */', $className), false]; + if ($error) { + return ['⚠ '.$error->getMessage(), false, false]; } if (\is_array($var)) { $a = []; - $original = true; + $explainable = $runnable = true; foreach ($var as $k => $v) { - list($value, $orig) = $this->sanitizeParam($v); - $original = $original && $orig; + list($value, $e, $r) = $this->sanitizeParam($v, null); + $explainable = $explainable && $e; + $runnable = $runnable && $r; $a[$k] = $value; } - return [$a, $original]; + return [$a, $explainable, $runnable]; } if (\is_resource($var)) { - return [sprintf('/* Resource(%s) */', get_resource_type($var)), false]; + return [sprintf('/* Resource(%s) */', get_resource_type($var)), false, false]; } - return [$var, true]; + return [$var, true, true]; } } diff --git a/src/Symfony/Bridge/Doctrine/DataCollector/ObjectParameter.php b/src/Symfony/Bridge/Doctrine/DataCollector/ObjectParameter.php new file mode 100644 index 0000000000..26bdb7ff26 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/DataCollector/ObjectParameter.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\DataCollector; + +final class ObjectParameter +{ + private $object; + private $error; + private $stringable; + private $class; + + /** + * @param object $object + */ + public function __construct($object, ?\Throwable $error) + { + $this->object = $object; + $this->error = $error; + $this->stringable = \is_callable([$object, '__toString']); + $this->class = \get_class($object); + } + + /** + * @return object + */ + public function getObject() + { + return $this->object; + } + + public function getError(): ?\Throwable + { + return $this->error; + } + + public function isStringable(): bool + { + return $this->stringable; + } + + public function getClass(): string + { + return $this->class; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php b/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php index e6547f3844..13a6c70669 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php @@ -18,6 +18,7 @@ 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; class DoctrineDataCollectorTest extends TestCase { @@ -74,7 +75,7 @@ class DoctrineDataCollectorTest extends TestCase /** * @dataProvider paramProvider */ - public function testCollectQueries($param, $types, $expected, $explainable) + public function testCollectQueries($param, $types, $expected, $explainable, bool $runnable = true) { $queries = [ ['sql' => 'SELECT * FROM table1 WHERE field1 = ?1', 'params' => [$param], 'types' => $types, 'executionMS' => 1], @@ -83,8 +84,19 @@ class DoctrineDataCollectorTest extends TestCase $c->collect(new Request(), new Response()); $collectedQueries = $c->getQueries(); - $this->assertEquals($expected, $collectedQueries['default'][0]['params'][0]); + + $collectedParam = $collectedQueries['default'][0]['params'][0]; + if ($collectedParam instanceof Data) { + $dumper = new CliDumper($out = fopen('php://memory', 'r+b')); + $dumper->setColors(false); + $collectedParam->dump($dumper); + $this->assertStringMatchesFormat($expected, print_r(stream_get_contents($out, -1, 0), true)); + } else { + $this->assertEquals($expected, $collectedParam); + } + $this->assertEquals($explainable, $collectedQueries['default'][0]['explainable']); + $this->assertSame($runnable, $collectedQueries['default'][0]['runnable']); } public function testCollectQueryWithNoParams() @@ -100,9 +112,11 @@ class DoctrineDataCollectorTest extends TestCase $this->assertInstanceOf(Data::class, $collectedQueries['default'][0]['params']); $this->assertEquals([], $collectedQueries['default'][0]['params']->getValue()); $this->assertTrue($collectedQueries['default'][0]['explainable']); + $this->assertTrue($collectedQueries['default'][0]['runnable']); $this->assertInstanceOf(Data::class, $collectedQueries['default'][1]['params']); $this->assertEquals([], $collectedQueries['default'][1]['params']->getValue()); $this->assertTrue($collectedQueries['default'][1]['explainable']); + $this->assertTrue($collectedQueries['default'][1]['runnable']); } public function testCollectQueryWithNoTypes() @@ -134,7 +148,7 @@ class DoctrineDataCollectorTest extends TestCase /** * @dataProvider paramProvider */ - public function testSerialization($param, $types, $expected, $explainable) + public function testSerialization($param, $types, $expected, $explainable, bool $runnable = true) { $queries = [ ['sql' => 'SELECT * FROM table1 WHERE field1 = ?1', 'params' => [$param], 'types' => $types, 'executionMS' => 1], @@ -144,8 +158,19 @@ class DoctrineDataCollectorTest extends TestCase $c = unserialize(serialize($c)); $collectedQueries = $c->getQueries(); - $this->assertEquals($expected, $collectedQueries['default'][0]['params'][0]); + + $collectedParam = $collectedQueries['default'][0]['params'][0]; + if ($collectedParam instanceof Data) { + $dumper = new CliDumper($out = fopen('php://memory', 'r+b')); + $dumper->setColors(false); + $collectedParam->dump($dumper); + $this->assertStringMatchesFormat($expected, print_r(stream_get_contents($out, -1, 0), true)); + } else { + $this->assertEquals($expected, $collectedParam); + } + $this->assertEquals($explainable, $collectedQueries['default'][0]['explainable']); + $this->assertSame($runnable, $collectedQueries['default'][0]['runnable']); } public function paramProvider() @@ -156,19 +181,46 @@ class DoctrineDataCollectorTest extends TestCase [true, [], true, true], [null, [], null, true], [new \DateTime('2011-09-11'), ['date'], '2011-09-11', true], - [fopen(__FILE__, 'r'), [], '/* Resource(stream) */', false], - [new \stdClass(), [], '/* Object(stdClass) */', false], + [fopen(__FILE__, 'r'), [], '/* Resource(stream) */', false, false], + [ + new \stdClass(), + [], + <<=')) { - $tests[] = ['this is not a date', ['date'], 'this is not a date', false]; - $tests[] = [new \stdClass(), ['date'], '/* Object(stdClass) */', false]; + $tests[] = ['this is not a date', ['date'], "⚠ Could not convert PHP value 'this is not a date' of type 'string' to type 'date'. Expected one of the following types: null, DateTime", false, false]; + $tests[] = [ + new \stdClass(), + ['date'], + <<