Skip to content

Commit c81ca71

Browse files
committed
[DoctrineBridge] Improve queries parameters display in Profiler
1 parent 43b753d commit c81ca71

File tree

3 files changed

+168
-26
lines changed

3 files changed

+168
-26
lines changed

src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
use Symfony\Component\HttpFoundation\Request;
1919
use Symfony\Component\HttpFoundation\Response;
2020
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
21+
use Symfony\Component\VarDumper\Caster\Caster;
22+
use Symfony\Component\VarDumper\Cloner\Stub;
2123

2224
/**
2325
* DoctrineDataCollector.
@@ -121,6 +123,38 @@ public function getName()
121123
return 'db';
122124
}
123125

126+
/**
127+
* {@inheritdoc}
128+
*/
129+
protected function getCasters()
130+
{
131+
return parent::getCasters() + [
132+
ObjectParameter::class => static function (ObjectParameter $o, array $a, Stub $s): array {
133+
$s->class = $o->getClass();
134+
$s->value = $o->getObject();
135+
136+
$r = new \ReflectionClass($o->getClass());
137+
if ($f = $r->getFileName()) {
138+
$s->attr['file'] = $f;
139+
$s->attr['line'] = $r->getStartLine();
140+
} else {
141+
unset($s->attr['file']);
142+
unset($s->attr['line']);
143+
}
144+
145+
if ($error = $o->getError()) {
146+
return [Caster::PREFIX_VIRTUAL.'' => $error->getMessage()];
147+
}
148+
149+
if ($o->isStringable()) {
150+
return [Caster::PREFIX_VIRTUAL.'__toString()' => (string) $o->getObject()];
151+
}
152+
153+
return [Caster::PREFIX_VIRTUAL.'' => sprintf('Object of class "%s" could not be converted to string.', $o->getClass())];
154+
},
155+
];
156+
}
157+
124158
private function sanitizeQueries(string $connectionName, array $queries): array
125159
{
126160
foreach ($queries as $i => $query) {
@@ -133,6 +167,7 @@ private function sanitizeQueries(string $connectionName, array $queries): array
133167
private function sanitizeQuery(string $connectionName, array $query): array
134168
{
135169
$query['explainable'] = true;
170+
$query['runnable'] = true;
136171
if (null === $query['params']) {
137172
$query['params'] = [];
138173
}
@@ -143,6 +178,7 @@ private function sanitizeQuery(string $connectionName, array $query): array
143178
$query['types'] = [];
144179
}
145180
foreach ($query['params'] as $j => $param) {
181+
$e = null;
146182
if (isset($query['types'][$j])) {
147183
// Transform the param according to the type
148184
$type = $query['types'][$j];
@@ -162,10 +198,14 @@ private function sanitizeQuery(string $connectionName, array $query): array
162198
}
163199
}
164200

165-
list($query['params'][$j], $explainable) = $this->sanitizeParam($param);
201+
list($query['params'][$j], $explainable, $runnable) = $this->sanitizeParam($param, $e);
166202
if (!$explainable) {
167203
$query['explainable'] = false;
168204
}
205+
206+
if (!$runnable) {
207+
$query['runnable'] = false;
208+
}
169209
}
170210

171211
$query['params'] = $this->cloneVar($query['params']);
@@ -180,32 +220,33 @@ private function sanitizeQuery(string $connectionName, array $query): array
180220
* indicating if the original value was kept (allowing to use the sanitized
181221
* value to explain the query).
182222
*/
183-
private function sanitizeParam($var): array
223+
private function sanitizeParam($var, ?\Throwable $error): array
184224
{
185225
if (\is_object($var)) {
186-
$className = \get_class($var);
226+
return [new ObjectParameter($var, $stringable = method_exists($var, '__toString'), $error), false, $stringable && !$error];
227+
}
187228

188-
return method_exists($var, '__toString') ?
189-
[sprintf('/* Object(%s): */"%s"', $className, $var->__toString()), false] :
190-
[sprintf('/* Object(%s) */', $className), false];
229+
if ($error) {
230+
return [''.$error->getMessage(), false, false];
191231
}
192232

193233
if (\is_array($var)) {
194234
$a = [];
195-
$original = true;
235+
$explainable = $runnable = true;
196236
foreach ($var as $k => $v) {
197-
list($value, $orig) = $this->sanitizeParam($v);
198-
$original = $original && $orig;
237+
list($value, $e, $r) = $this->sanitizeParam($v, null);
238+
$explainable = $explainable && $e;
239+
$runnable = $runnable && $r;
199240
$a[$k] = $value;
200241
}
201242

202-
return [$a, $original];
243+
return [$a, $explainable, $runnable];
203244
}
204245

205246
if (\is_resource($var)) {
206-
return [sprintf('/* Resource(%s) */', get_resource_type($var)), false];
247+
return [sprintf('/* Resource(%s) */', get_resource_type($var)), false, false];
207248
}
208249

209-
return [$var, true];
250+
return [$var, true, true];
210251
}
211252
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Doctrine\DataCollector;
13+
14+
final class ObjectParameter
15+
{
16+
private $object;
17+
private $stringable;
18+
private $error;
19+
private $class;
20+
21+
/**
22+
* @param object $object
23+
*/
24+
public function __construct($object, bool $stringable, ?\Throwable $error)
25+
{
26+
$this->object = $object;
27+
$this->stringable = $stringable;
28+
$this->error = $error;
29+
$this->class = \get_class($object);
30+
}
31+
32+
/**
33+
* @return object
34+
*/
35+
public function getObject()
36+
{
37+
return $this->object;
38+
}
39+
40+
public function isStringable(): bool
41+
{
42+
return $this->stringable;
43+
}
44+
45+
public function getError(): ?\Throwable
46+
{
47+
return $this->error;
48+
}
49+
50+
public function getClass(): string
51+
{
52+
return $this->class;
53+
}
54+
}

src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
use Symfony\Component\HttpFoundation\Request;
1919
use Symfony\Component\HttpFoundation\Response;
2020
use Symfony\Component\VarDumper\Cloner\Data;
21+
use Symfony\Component\VarDumper\Dumper\CliDumper;
22+
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
2123

2224
class DoctrineDataCollectorTest extends TestCase
2325
{
@@ -74,7 +76,7 @@ public function testCollectTime()
7476
/**
7577
* @dataProvider paramProvider
7678
*/
77-
public function testCollectQueries($param, $types, $expected, $explainable)
79+
public function testCollectQueries($param, $types, $expected, $explainable, bool $runnable)
7880
{
7981
$queries = [
8082
['sql' => 'SELECT * FROM table1 WHERE field1 = ?1', 'params' => [$param], 'types' => $types, 'executionMS' => 1],
@@ -83,8 +85,17 @@ public function testCollectQueries($param, $types, $expected, $explainable)
8385
$c->collect(new Request(), new Response());
8486

8587
$collectedQueries = $c->getQueries();
86-
$this->assertEquals($expected, $collectedQueries['default'][0]['params'][0]);
88+
89+
$collectedParam = $collectedQueries['default'][0]['params'][0];
90+
if ($collectedParam instanceof Data) {
91+
$collectedParam->dump(new CliDumper($out = fopen('php://memory', 'r+b')));
92+
$this->assertStringMatchesFormat($expected, print_r(stream_get_contents($out, -1, 0), true));
93+
} else {
94+
$this->assertEquals($expected, $collectedParam);
95+
}
96+
8797
$this->assertEquals($explainable, $collectedQueries['default'][0]['explainable']);
98+
$this->assertSame($runnable, $collectedQueries['default'][0]['runnable']);
8899
}
89100

90101
public function testCollectQueryWithNoParams()
@@ -100,9 +111,11 @@ public function testCollectQueryWithNoParams()
100111
$this->assertInstanceOf(Data::class, $collectedQueries['default'][0]['params']);
101112
$this->assertEquals([], $collectedQueries['default'][0]['params']->getValue());
102113
$this->assertTrue($collectedQueries['default'][0]['explainable']);
114+
$this->assertTrue($collectedQueries['default'][0]['runnable']);
103115
$this->assertInstanceOf(Data::class, $collectedQueries['default'][1]['params']);
104116
$this->assertEquals([], $collectedQueries['default'][1]['params']->getValue());
105117
$this->assertTrue($collectedQueries['default'][1]['explainable']);
118+
$this->assertTrue($collectedQueries['default'][1]['runnable']);
106119
}
107120

108121
public function testCollectQueryWithNoTypes()
@@ -134,7 +147,7 @@ public function testReset()
134147
/**
135148
* @dataProvider paramProvider
136149
*/
137-
public function testSerialization($param, $types, $expected, $explainable)
150+
public function testSerialization($param, $types, $expected, $explainable, bool $runnable)
138151
{
139152
$queries = [
140153
['sql' => 'SELECT * FROM table1 WHERE field1 = ?1', 'params' => [$param], 'types' => $types, 'executionMS' => 1],
@@ -144,31 +157,65 @@ public function testSerialization($param, $types, $expected, $explainable)
144157
$c = unserialize(serialize($c));
145158

146159
$collectedQueries = $c->getQueries();
147-
$this->assertEquals($expected, $collectedQueries['default'][0]['params'][0]);
160+
161+
$collectedParam = $collectedQueries['default'][0]['params'][0];
162+
if ($collectedParam instanceof Data) {
163+
$collectedParam->dump(new CliDumper($out = fopen('php://memory', 'r+b')));
164+
$this->assertStringMatchesFormat($expected, print_r(stream_get_contents($out, -1, 0), true));
165+
} else {
166+
$this->assertEquals($expected, $collectedParam);
167+
}
168+
148169
$this->assertEquals($explainable, $collectedQueries['default'][0]['explainable']);
170+
$this->assertSame($runnable, $collectedQueries['default'][0]['runnable']);
149171
}
150172

151173
public function paramProvider()
152174
{
153175
$tests = [
154-
['some value', [], 'some value', true],
155-
[1, [], 1, true],
156-
[true, [], true, true],
157-
[null, [], null, true],
158-
[new \DateTime('2011-09-11'), ['date'], '2011-09-11', true],
159-
[fopen(__FILE__, 'r'), [], '/* Resource(stream) */', false],
160-
[new \stdClass(), [], '/* Object(stdClass) */', false],
176+
['some value', [], 'some value', true, true],
177+
[1, [], 1, true, true],
178+
[true, [], true, true, true],
179+
[null, [], null, true, true],
180+
[new \DateTime('2011-09-11'), ['date'], '2011-09-11', true, true],
181+
[fopen(__FILE__, 'r'), [], '/* Resource(stream) */', false, false],
182+
[
183+
new \stdClass(),
184+
[],
185+
<<<EOTXT
186+
{#%d
187+
⚠: "Object of class "stdClass" could not be converted to string."
188+
}
189+
EOTXT
190+
, false, false],
161191
[
162192
new StringRepresentableClass(),
163193
[],
164-
'/* Object(Symfony\Bridge\Doctrine\Tests\DataCollector\StringRepresentableClass): */"string representation"',
194+
<<<EOTXT
195+
Symfony\Bridge\Doctrine\Tests\DataCollector\StringRepresentableClass {#%d
196+
__toString(): "string representation"
197+
}
198+
EOTXT
199+
,
165200
false,
201+
true,
166202
],
167203
];
168204

169205
if (version_compare(Version::VERSION, '2.6', '>=')) {
170-
$tests[] = ['this is not a date', ['date'], 'this is not a date', false];
171-
$tests[] = [new \stdClass(), ['date'], '/* Object(stdClass) */', false];
206+
$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];
207+
$tests[] = [
208+
new \stdClass(),
209+
['date'],
210+
<<<EOTXT
211+
{#%d
212+
⚠: "Could not convert PHP value of type 'stdClass' to type 'date'. Expected one of the following types: null, DateTime"
213+
}
214+
EOTXT
215+
,
216+
false,
217+
false
218+
];
172219
}
173220

174221
return $tests;

0 commit comments

Comments
 (0)