1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* This file is part of the Kdyby (http://www.kdyby.org) |
5
|
|
|
* |
6
|
|
|
* Copyright (c) 2008 Filip Procházka ([email protected]) |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the file license.txt that was distributed with this source code. |
9
|
|
|
*/ |
10
|
|
|
|
11
|
|
|
namespace Kdyby\Doctrine\Diagnostics; |
12
|
|
|
|
13
|
|
|
use Doctrine; |
14
|
|
|
use Doctrine\Common\Collections\ArrayCollection; |
15
|
|
|
use Doctrine\Common\Persistence\Proxy; |
16
|
|
|
use Doctrine\Common\Annotations\AnnotationException; |
17
|
|
|
use Doctrine\DBAL\Platforms\AbstractPlatform; |
18
|
|
|
use Doctrine\DBAL\Types\Type; |
19
|
|
|
use Kdyby; |
20
|
|
|
use Nette; |
21
|
|
|
use Nette\Utils\Strings; |
22
|
|
|
use Tracy\Bar; |
23
|
|
|
use Tracy\BlueScreen; |
24
|
|
|
use Tracy\Debugger; |
25
|
|
|
use Tracy\Dumper; |
26
|
|
|
use Tracy\Helpers; |
27
|
|
|
use Tracy\IBarPanel; |
28
|
|
|
|
29
|
|
|
|
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Debug panel for Doctrine |
33
|
|
|
* |
34
|
|
|
* @author David Grudl |
35
|
|
|
* @author Patrik Votoček |
36
|
|
|
* @author Filip Procházka <[email protected]> |
37
|
|
|
*/ |
38
|
|
|
class Panel implements IBarPanel, Doctrine\DBAL\Logging\SQLLogger |
39
|
|
|
{ |
40
|
|
|
|
41
|
|
|
use \Kdyby\StrictObjects\Scream; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @var int logged time |
45
|
|
|
*/ |
46
|
|
|
public $totalTime = 0; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @var array |
50
|
|
|
*/ |
51
|
|
|
public $queries = []; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @var array |
55
|
|
|
*/ |
56
|
|
|
public $failed = []; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @var array |
60
|
|
|
*/ |
61
|
|
|
public $skipPaths = [ |
62
|
|
|
'vendor/nette/', 'src/Nette/', |
63
|
|
|
'vendor/doctrine/collections/', 'lib/Doctrine/Collections/', |
64
|
|
|
'vendor/doctrine/common/', 'lib/Doctrine/Common/', |
65
|
|
|
'vendor/doctrine/dbal/', 'lib/Doctrine/DBAL/', |
66
|
|
|
'vendor/doctrine/orm/', 'lib/Doctrine/ORM/', |
67
|
|
|
'vendor/kdyby/doctrine/', 'src/Kdyby/Doctrine/', |
68
|
|
|
'vendor/phpunit', |
69
|
|
|
]; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* @var \Doctrine\DBAL\Connection |
73
|
|
|
*/ |
74
|
|
|
private $connection; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* @var \Doctrine\ORM\EntityManager |
78
|
|
|
*/ |
79
|
|
|
private $em; |
80
|
|
|
|
81
|
|
|
|
82
|
|
|
|
83
|
|
|
/***************** Doctrine\DBAL\Logging\SQLLogger ********************/ |
84
|
|
|
|
85
|
|
|
|
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* @param string |
89
|
|
|
* @param array |
90
|
|
|
* @param array |
91
|
|
|
*/ |
92
|
|
|
public function startQuery($sql, array $params = NULL, array $types = NULL) |
93
|
|
|
{ |
94
|
|
|
Debugger::timer('doctrine'); |
95
|
|
|
|
96
|
|
|
$source = NULL; |
97
|
|
|
foreach (debug_backtrace(FALSE) as $row) { |
98
|
|
|
if (isset($row['file']) && $this->filterTracePaths(realpath($row['file']))) { |
99
|
|
|
if (isset($row['class']) && stripos($row['class'], '\\' . Proxy::MARKER) !== FALSE) { |
100
|
|
|
if (!in_array(Doctrine\Common\Persistence\Proxy::class, class_implements($row['class']))) { |
101
|
|
|
continue; |
102
|
|
|
|
103
|
|
|
} elseif (isset($row['function']) && $row['function'] === '__load') { |
104
|
|
|
continue; |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
} elseif (stripos($row['file'], DIRECTORY_SEPARATOR . Proxy::MARKER) !== FALSE) { |
108
|
|
|
continue; |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
$source = [$row['file'], (int) $row['line']]; |
112
|
|
|
break; |
113
|
|
|
} |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
$this->queries[] = [$sql, $params, NULL, $types, $source]; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
|
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* @param string $file |
123
|
|
|
* @return boolean |
124
|
|
|
*/ |
125
|
|
|
protected function filterTracePaths($file) |
126
|
|
|
{ |
127
|
|
|
$file = str_replace(DIRECTORY_SEPARATOR, '/', $file); |
128
|
|
|
$return = is_file($file); |
129
|
|
|
foreach ($this->skipPaths as $path) { |
130
|
|
|
if (!$return) { |
131
|
|
|
break; |
132
|
|
|
} |
133
|
|
|
$return = $return && strpos($file, '/' . trim($path, '/') . '/') === FALSE; |
134
|
|
|
} |
135
|
|
|
return $return; |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
|
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* @return array |
142
|
|
|
*/ |
143
|
|
|
public function stopQuery() |
144
|
|
|
{ |
145
|
|
|
$keys = array_keys($this->queries); |
146
|
|
|
$key = end($keys); |
147
|
|
|
$this->queries[$key][2] = $time = Debugger::timer('doctrine'); |
148
|
|
|
$this->totalTime += $time; |
149
|
|
|
|
150
|
|
|
return $this->queries[$key] + array_fill_keys(range(0, 4), NULL); |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
|
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* @param \Exception|\Throwable $exception |
157
|
|
|
*/ |
158
|
|
|
public function queryFailed($exception) |
159
|
|
|
{ |
160
|
|
|
$this->failed[spl_object_hash($exception)] = $this->stopQuery(); |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
|
164
|
|
|
|
165
|
|
|
/***************** Tracy\IBarPanel ********************/ |
166
|
|
|
|
167
|
|
|
|
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* @return string |
171
|
|
|
*/ |
172
|
|
|
public function getTab() |
173
|
|
|
{ |
174
|
|
|
return '<span title="Doctrine 2">' |
175
|
|
|
. '<svg viewBox="0 0 2048 2048"><path fill="#aaa" d="M1024 896q237 0 443-43t325-127v170q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-170q119 84 325 127t443 43zm0 768q237 0 443-43t325-127v170q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-170q119 84 325 127t443 43zm0-384q237 0 443-43t325-127v170q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-170q119 84 325 127t443 43zm0-1152q208 0 385 34.5t280 93.5 103 128v128q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-128q0-69 103-128t280-93.5 385-34.5z"></path></svg>' |
176
|
|
|
. '<span class="tracy-label">' |
177
|
|
|
. count($this->queries) . ' queries' |
178
|
|
|
. ($this->totalTime ? ' / ' . sprintf('%0.1f', $this->totalTime * 1000) . ' ms' : '') |
179
|
|
|
. '</span>' |
180
|
|
|
. '</span>'; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
|
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* @return string |
187
|
|
|
*/ |
188
|
|
|
public function getPanel() |
189
|
|
|
{ |
190
|
|
|
if (empty($this->queries)) { |
191
|
|
|
return ''; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
$connParams = $this->connection->getParams(); |
195
|
|
|
if ($connParams['driver'] === 'pdo_sqlite' && isset($connParams['path'])) { |
196
|
|
|
$host = 'path: ' . basename($connParams['path']); |
197
|
|
|
|
198
|
|
|
} else { |
199
|
|
|
$host = sprintf('host: %s%s/%s', |
200
|
|
|
$this->connection->getHost(), |
201
|
|
|
(($p = $this->connection->getPort()) ? ':' . $p : ''), |
202
|
|
|
$this->connection->getDatabase() |
203
|
|
|
); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
return |
207
|
|
|
$this->renderStyles() . |
208
|
|
|
sprintf('<h1>Queries: %s %s, %s</h1>', |
209
|
|
|
count($this->queries), |
210
|
|
|
($this->totalTime ? ', time: ' . sprintf('%0.3f', $this->totalTime * 1000) . ' ms' : ''), |
211
|
|
|
$host |
212
|
|
|
) . |
213
|
|
|
'<div class="nette-inner tracy-inner nette-Doctrine2Panel">' . |
214
|
|
|
implode('<br>', array_filter([ |
215
|
|
|
$this->renderPanelCacheStatistics(), |
216
|
|
|
$this->renderPanelQueries() |
217
|
|
|
])) . |
218
|
|
|
'</div>'; |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
|
222
|
|
|
|
223
|
|
|
private function renderPanelCacheStatistics() |
224
|
|
|
{ |
225
|
|
|
if (empty($this->em)) { |
226
|
|
|
return ''; |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
$config = $this->em->getConfiguration(); |
230
|
|
|
if (!$config->isSecondLevelCacheEnabled()) { |
231
|
|
|
return ''; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
$loggerChain = $config->getSecondLevelCacheConfiguration() |
235
|
|
|
->getCacheLogger(); |
236
|
|
|
|
237
|
|
|
if (!$loggerChain instanceof Doctrine\ORM\Cache\Logging\CacheLoggerChain) { |
238
|
|
|
return ''; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
if (!$statistics = $loggerChain->getLogger('statistics')) { |
242
|
|
|
return ''; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
return Dumper::toHtml($statistics, [Dumper::DEPTH => 5]); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
|
249
|
|
|
|
250
|
|
|
private function renderPanelQueries() |
251
|
|
|
{ |
252
|
|
|
if (empty($this->queries)) { |
253
|
|
|
return ""; |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
$s = ""; |
257
|
|
|
foreach ($this->queries as $query) { |
258
|
|
|
$s .= $this->processQuery($query); |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
return '<table><tr><th>ms</th><th>SQL Statement</th></tr>' . $s . '</table>'; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
|
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* @return string |
268
|
|
|
*/ |
269
|
|
|
protected function renderStyles() |
270
|
|
|
{ |
271
|
|
|
return '<style> |
272
|
|
|
#nette-debug td.nette-Doctrine2Panel-sql { background: white !important} |
273
|
|
|
#nette-debug .nette-Doctrine2Panel-source { color: #BBB !important } |
274
|
|
|
#nette-debug nette-Doctrine2Panel tr table { margin: 8px 0; max-height: 150px; overflow:auto } |
275
|
|
|
#tracy-debug td.nette-Doctrine2Panel-sql { background: white !important} |
276
|
|
|
#tracy-debug .nette-Doctrine2Panel-source { color: #BBB !important } |
277
|
|
|
#tracy-debug nette-Doctrine2Panel tr table { margin: 8px 0; max-height: 150px; overflow:auto } |
278
|
|
|
</style>'; |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
|
282
|
|
|
|
283
|
|
|
/** |
284
|
|
|
* @param array |
285
|
|
|
* @return string |
286
|
|
|
*/ |
287
|
|
|
protected function processQuery(array $query) |
288
|
|
|
{ |
289
|
|
|
$h = 'htmlspecialchars'; |
290
|
|
|
list($sql, $params, $time, $types, $source) = $query; |
291
|
|
|
|
292
|
|
|
$s = self::highlightQuery(static::formatQuery($sql, (array) $params, (array) $types, $this->connection ? $this->connection->getDatabasePlatform() : NULL)); |
293
|
|
|
if ($source) { |
294
|
|
|
$s .= self::editorLink($source[0], $source[1], $h('.../' . basename(dirname($source[0]))) . '/<b>' . $h(basename($source[0])) . '</b>'); |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
return '<tr><td>' . sprintf('%0.3f', $time * 1000) . '</td>' . |
298
|
|
|
'<td class = "nette-Doctrine2Panel-sql">' . $s . '</td></tr>'; |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
|
302
|
|
|
|
303
|
|
|
/****************** Exceptions handling *********************/ |
304
|
|
|
|
305
|
|
|
|
306
|
|
|
|
307
|
|
|
/** |
308
|
|
|
* @param \Exception|\Throwable $e |
309
|
|
|
* @return array|NULL |
310
|
|
|
*/ |
311
|
|
|
public function renderQueryException($e) |
312
|
|
|
{ |
313
|
|
|
if ($e instanceof \PDOException && count($this->queries)) { |
314
|
|
|
$types = $params = []; |
315
|
|
|
|
316
|
|
|
if ($this->connection !== NULL) { |
317
|
|
|
if (!$e instanceof Kdyby\Doctrine\DBALException || $e->connection !== $this->connection) { |
318
|
|
|
return NULL; |
319
|
|
|
|
320
|
|
|
} elseif (!isset($this->failed[spl_object_hash($e)])) { |
321
|
|
|
return NULL; |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
list($sql, $params, , , $source) = $this->failed[spl_object_hash($e)]; |
325
|
|
|
|
326
|
|
|
} else { |
327
|
|
|
list($sql, $params, , $types, $source) = end($this->queries) + range(1, 5); |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
if (!$sql) { |
331
|
|
|
return NULL; |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
return [ |
335
|
|
|
'tab' => 'SQL', |
336
|
|
|
'panel' => $this->dumpQuery($sql, $params, $types, $source), |
337
|
|
|
]; |
338
|
|
|
|
339
|
|
|
} elseif ($e instanceof Kdyby\Doctrine\QueryException && $e->query !== NULL) { |
340
|
|
|
if ($e->query instanceof Doctrine\ORM\Query) { |
341
|
|
|
return [ |
342
|
|
|
'tab' => 'DQL', |
343
|
|
|
'panel' => $this->dumpQuery($e->query->getDQL(), $e->query->getParameters()), |
344
|
|
|
]; |
345
|
|
|
|
346
|
|
|
} elseif ($e->query instanceof Kdyby\Doctrine\NativeQueryWrapper) { |
347
|
|
|
return [ |
348
|
|
|
'tab' => 'Native SQL', |
349
|
|
|
'panel' => $this->dumpQuery($e->query->getSQL(), $e->query->getParameters()), |
350
|
|
|
]; |
351
|
|
|
} |
352
|
|
|
} |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
|
356
|
|
|
|
357
|
|
|
/** |
358
|
|
|
* @param \Exception|\Throwable $e |
359
|
|
|
* @param \Nette\DI\Container $dic |
360
|
|
|
* @return array|NULL |
361
|
|
|
*/ |
362
|
|
|
public static function renderException($e, Nette\DI\Container $dic) |
363
|
|
|
{ |
364
|
|
|
if ($e instanceof AnnotationException) { |
365
|
|
|
if ($dump = self::highlightAnnotationLine($e)) { |
366
|
|
|
return [ |
367
|
|
|
'tab' => 'Annotation', |
368
|
|
|
'panel' => $dump, |
369
|
|
|
]; |
370
|
|
|
} |
371
|
|
|
|
372
|
|
|
} elseif ($e instanceof Doctrine\ORM\Mapping\MappingException) { |
373
|
|
|
if ($invalidEntity = Strings::match($e->getMessage(), '~^Class "([\\S]+)" .*? is not .*? valid~i')) { |
374
|
|
|
$refl = Nette\Reflection\ClassType::from($invalidEntity[1]); |
375
|
|
|
$file = $refl->getFileName(); |
376
|
|
|
$errorLine = $refl->getStartLine(); |
377
|
|
|
|
378
|
|
|
return [ |
379
|
|
|
'tab' => 'Invalid entity', |
380
|
|
|
'panel' => '<p><b>File:</b> ' . self::editorLink($file, $errorLine) . '</p>' . |
381
|
|
|
BlueScreen::highlightFile($file, $errorLine), |
382
|
|
|
]; |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
} elseif ($e instanceof Doctrine\DBAL\Schema\SchemaException && $dic && ($em = $dic->getByType(Kdyby\Doctrine\EntityManager::class, FALSE))) { |
386
|
|
|
/** @var Kdyby\Doctrine\EntityManager $em */ |
387
|
|
|
|
388
|
|
|
if ($invalidTable = Strings::match($e->getMessage(), '~table \'(.*?)\'~i')) { |
389
|
|
|
foreach ($em->getMetadataFactory()->getAllMetadata() as $class) { |
390
|
|
|
/** @var Kdyby\Doctrine\Mapping\ClassMetadata $class */ |
391
|
|
|
if ($class->getTableName() === $invalidTable[1]) { |
392
|
|
|
$refl = $class->getReflectionClass(); |
393
|
|
|
break; |
394
|
|
|
} |
395
|
|
|
} |
396
|
|
|
|
397
|
|
|
if (!isset($refl)) { |
398
|
|
|
return NULL; |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
$file = $refl->getFileName(); |
402
|
|
|
$errorLine = $refl->getStartLine(); |
403
|
|
|
|
404
|
|
|
return [ |
405
|
|
|
'tab' => 'Invalid schema', |
406
|
|
|
'panel' => '<p><b>File:</b> ' . self::editorLink($file, $errorLine) . '</p>' . |
407
|
|
|
BlueScreen::highlightFile($file, $errorLine), |
408
|
|
|
]; |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
} elseif ($e instanceof Kdyby\Doctrine\DBALException && $e->query !== NULL) { |
412
|
|
|
return [ |
413
|
|
|
'tab' => 'SQL', |
414
|
|
|
'panel' => self::highlightQuery(static::formatQuery($e->query, $e->params, [])), |
415
|
|
|
]; |
416
|
|
|
|
417
|
|
|
} elseif ($e instanceof Doctrine\DBAL\Exception\DriverException) { |
418
|
|
|
if (($prev = $e->getPrevious()) && ($item = Helpers::findTrace($e->getTrace(), Doctrine\DBAL\DBALException::class . '::driverExceptionDuringQuery'))) { |
419
|
|
|
/** @var \Doctrine\DBAL\Driver $driver */ |
420
|
|
|
$driver = $item['args'][0]; |
421
|
|
|
$params = isset($item['args'][3]) ? $item['args'][3] : []; |
422
|
|
|
|
423
|
|
|
return [ |
424
|
|
|
'tab' => 'SQL', |
425
|
|
|
'panel' => self::highlightQuery(static::formatQuery($item['args'][2], $params, [], $driver->getDatabasePlatform())), |
426
|
|
|
]; |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
} elseif ($e instanceof Doctrine\ORM\Query\QueryException) { |
430
|
|
|
if (($prev = $e->getPrevious()) && preg_match('~^(SELECT|INSERT|UPDATE|DELETE)\s+.*~i', $prev->getMessage())) { |
431
|
|
|
return [ |
432
|
|
|
'tab' => 'DQL', |
433
|
|
|
'panel' => self::highlightQuery(static::formatQuery($prev->getMessage(), [], [])), |
434
|
|
|
]; |
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
} elseif ($e instanceof \PDOException) { |
438
|
|
|
$params = []; |
439
|
|
|
|
440
|
|
|
if (isset($e->queryString)) { |
441
|
|
|
$sql = $e->queryString; |
|
|
|
|
442
|
|
|
|
443
|
|
|
} elseif ($item = Helpers::findTrace($e->getTrace(), Doctrine\DBAL\Connection::class . '::executeQuery')) { |
444
|
|
|
$sql = $item['args'][0]; |
445
|
|
|
$params = $item['args'][1]; |
446
|
|
|
|
447
|
|
|
} elseif ($item = Helpers::findTrace($e->getTrace(), \PDO::class . '::query')) { |
448
|
|
|
$sql = $item['args'][0]; |
449
|
|
|
|
450
|
|
|
} elseif ($item = Helpers::findTrace($e->getTrace(), \PDO::class . '::prepare')) { |
451
|
|
|
$sql = $item['args'][0]; |
452
|
|
|
} |
453
|
|
|
|
454
|
|
|
return isset($sql) ? [ |
455
|
|
|
'tab' => 'SQL', |
456
|
|
|
'panel' => self::highlightQuery(static::formatQuery($sql, $params, [])), |
457
|
|
|
] : NULL; |
458
|
|
|
} |
459
|
|
|
|
460
|
|
|
return NULL; |
461
|
|
|
} |
462
|
|
|
|
463
|
|
|
|
464
|
|
|
|
465
|
|
|
/** |
466
|
|
|
* @param string $query |
467
|
|
|
* @param array|Doctrine\Common\Collections\ArrayCollection $params |
468
|
|
|
* @param array $types |
469
|
|
|
* @param string $source |
470
|
|
|
* @return string |
471
|
|
|
*/ |
472
|
|
|
protected function dumpQuery($query, $params, array $types = [], $source = NULL) |
473
|
|
|
{ |
474
|
|
|
if ($params instanceof ArrayCollection) { |
475
|
|
|
$tmp = []; |
476
|
|
|
$tmpTypes = []; |
477
|
|
|
foreach ($params as $key => $param) { |
478
|
|
|
if ($param instanceof Doctrine\ORM\Query\Parameter) { |
479
|
|
|
$tmpTypes[$param->getName()] = $param->getType(); |
480
|
|
|
$tmp[$param->getName()] = $param->getValue(); |
481
|
|
|
continue; |
482
|
|
|
} |
483
|
|
|
$tmp[$key] = $param; |
484
|
|
|
} |
485
|
|
|
$params = $tmp; |
486
|
|
|
$types = $tmpTypes; |
487
|
|
|
} |
488
|
|
|
|
489
|
|
|
// query |
490
|
|
|
$s = '<p><b>Query</b></p><table><tr><td class="nette-Doctrine2Panel-sql">'; |
491
|
|
|
$s .= self::highlightQuery(static::formatQuery($query, $params, $types, $this->connection ? $this->connection->getDatabasePlatform() : NULL)); |
492
|
|
|
$s .= '</td></tr></table>'; |
493
|
|
|
|
494
|
|
|
$e = NULL; |
495
|
|
|
if ($source && is_array($source)) { |
|
|
|
|
496
|
|
|
list($file, $line) = $source; |
497
|
|
|
$e = '<p><b>File:</b> ' . self::editorLink($file, $line) . '</p>'; |
498
|
|
|
} |
499
|
|
|
|
500
|
|
|
// styles and dump |
501
|
|
|
return $this->renderStyles() . '<div class="nette-inner tracy-inner nette-Doctrine2Panel">' . $e . $s . '</div>'; |
502
|
|
|
} |
503
|
|
|
|
504
|
|
|
|
505
|
|
|
|
506
|
|
|
/** |
507
|
|
|
* Returns syntax highlighted SQL command. |
508
|
|
|
* This method is same as Nette\Database\Helpers::dumpSql except for parameters handling. |
509
|
|
|
* @link https://github.com/nette/database/blob/667143b2d5b940f78c8dc9212f95b1bbc033c6a3/src/Database/Helpers.php#L75-L138 |
510
|
|
|
* @author David Grudl |
511
|
|
|
* @param string $sql |
512
|
|
|
* @return string |
513
|
|
|
*/ |
514
|
|
|
public static function highlightQuery($sql) |
515
|
|
|
{ |
516
|
|
|
static $keywords1 = 'SELECT|(?:ON\s+DUPLICATE\s+KEY)?UPDATE|INSERT(?:\s+INTO)?|REPLACE(?:\s+INTO)?|DELETE|CALL|UNION|FROM|WHERE|HAVING|GROUP\s+BY|ORDER\s+BY|LIMIT|OFFSET|SET|VALUES|LEFT\s+JOIN|INNER\s+JOIN|TRUNCATE'; |
517
|
|
|
static $keywords2 = 'ALL|DISTINCT|DISTINCTROW|IGNORE|AS|USING|ON|AND|OR|IN|IS|NOT|NULL|[RI]?LIKE|REGEXP|TRUE|FALSE|WITH|INSTANCE\s+OF'; |
518
|
|
|
|
519
|
|
|
// insert new lines |
520
|
|
|
$sql = " $sql "; |
521
|
|
|
$sql = preg_replace("#(?<=[\\s,(])($keywords1)(?=[\\s,)])#i", "\n\$1", $sql); |
522
|
|
|
|
523
|
|
|
// reduce spaces |
524
|
|
|
$sql = preg_replace('#[ \t]{2,}#', ' ', $sql); |
525
|
|
|
|
526
|
|
|
$sql = wordwrap($sql, 100); |
527
|
|
|
$sql = preg_replace('#([ \t]*\r?\n){2,}#', "\n", $sql); |
528
|
|
|
|
529
|
|
|
// syntax highlight |
530
|
|
|
$sql = htmlspecialchars($sql, ENT_IGNORE, 'UTF-8'); |
531
|
|
|
$sql = preg_replace_callback("#(/\\*.+?\\*/)|(\\*\\*.+?\\*\\*)|(?<=[\\s,(])($keywords1)(?=[\\s,)])|(?<=[\\s,(=])($keywords2)(?=[\\s,)=])#is", function ($matches) { |
532
|
|
|
if (!empty($matches[1])) { // comment |
533
|
|
|
return '<em style="color:gray">' . $matches[1] . '</em>'; |
534
|
|
|
|
535
|
|
|
} elseif (!empty($matches[2])) { // error |
536
|
|
|
return '<strong style="color:red">' . $matches[2] . '</strong>'; |
537
|
|
|
|
538
|
|
|
} elseif (!empty($matches[3])) { // most important keywords |
539
|
|
|
return '<strong style="color:blue">' . $matches[3] . '</strong>'; |
540
|
|
|
|
541
|
|
|
} elseif (!empty($matches[4])) { // other keywords |
542
|
|
|
return '<strong style="color:green">' . $matches[4] . '</strong>'; |
543
|
|
|
} |
544
|
|
|
}, $sql); |
545
|
|
|
|
546
|
|
|
return '<pre class="dump">' . trim($sql) . "</pre>\n"; |
547
|
|
|
} |
548
|
|
|
|
549
|
|
|
|
550
|
|
|
|
551
|
|
|
/** |
552
|
|
|
* @param string $query |
553
|
|
|
* @param array $params |
554
|
|
|
* @param array $types |
555
|
|
|
* @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform |
556
|
|
|
* @throws \Doctrine\DBAL\DBALException |
557
|
|
|
* @throws \Nette\Utils\RegexpException |
558
|
|
|
* @return string |
559
|
|
|
*/ |
560
|
|
|
public static function formatQuery($query, $params, array $types = [], AbstractPlatform $platform = NULL) |
561
|
|
|
{ |
562
|
|
|
if ($platform === NULL) { |
563
|
|
|
$platform = new Doctrine\DBAL\Platforms\MySqlPlatform(); |
564
|
|
|
} |
565
|
|
|
|
566
|
|
|
if (!$types) { |
|
|
|
|
567
|
|
|
foreach ($params as $key => $param) { |
568
|
|
|
if (is_array($param)) { |
569
|
|
|
$types[$key] = Doctrine\DBAL\Connection::PARAM_STR_ARRAY; |
570
|
|
|
|
571
|
|
|
} else { |
572
|
|
|
$types[$key] = 'string'; |
573
|
|
|
} |
574
|
|
|
} |
575
|
|
|
} |
576
|
|
|
|
577
|
|
|
try { |
578
|
|
|
list($query, $params, $types) = \Doctrine\DBAL\SQLParserUtils::expandListParameters($query, $params, $types); |
579
|
|
|
} catch (Doctrine\DBAL\SQLParserUtilsException $e) { |
|
|
|
|
580
|
|
|
} |
581
|
|
|
|
582
|
|
|
$formattedParams = []; |
583
|
|
|
foreach ($params as $key => $param) { |
584
|
|
|
if (isset($types[$key])) { |
585
|
|
|
if (is_scalar($types[$key]) && array_key_exists($types[$key], Type::getTypesMap())) { |
586
|
|
|
$types[$key] = Type::getType($types[$key]); |
587
|
|
|
} |
588
|
|
|
|
589
|
|
|
/** @var Type[] $types */ |
590
|
|
|
if ($types[$key] instanceof Type) { |
591
|
|
|
$param = $types[$key]->convertToDatabaseValue($param, $platform); |
592
|
|
|
} |
593
|
|
|
} |
594
|
|
|
|
595
|
|
|
$formattedParams[] = SimpleParameterFormatter::format($param); |
596
|
|
|
} |
597
|
|
|
$params = $formattedParams; |
598
|
|
|
|
599
|
|
|
if (Nette\Utils\Validators::isList($params)) { |
600
|
|
|
$parts = explode('?', $query); |
601
|
|
|
if (count($params) > $parts) { |
602
|
|
|
throw new Kdyby\Doctrine\InvalidStateException("Too mny parameters passed to query."); |
603
|
|
|
} |
604
|
|
|
|
605
|
|
|
return implode('', Kdyby\Doctrine\Helpers::zipper($parts, $params)); |
606
|
|
|
} |
607
|
|
|
|
608
|
|
|
return Strings::replace($query, '~(\\:[a-z][a-z0-9]*|\\?[0-9]*)~i', function ($m) use (&$params) { |
609
|
|
|
if (substr($m[0], 0, 1) === '?') { |
610
|
|
|
if (strlen($m[0]) > 1) { |
611
|
|
|
if (isset($params[$k = substr($m[0], 1)])) { |
612
|
|
|
return $params[$k]; |
613
|
|
|
} |
614
|
|
|
|
615
|
|
|
} else { |
616
|
|
|
return array_shift($params); |
617
|
|
|
} |
618
|
|
|
|
619
|
|
|
} else { |
620
|
|
|
if (isset($params[$k = substr($m[0], 1)])) { |
621
|
|
|
return $params[$k]; |
622
|
|
|
} |
623
|
|
|
} |
624
|
|
|
|
625
|
|
|
return $m[0]; |
626
|
|
|
}); |
627
|
|
|
} |
628
|
|
|
|
629
|
|
|
|
630
|
|
|
|
631
|
|
|
/** |
632
|
|
|
* @param \Doctrine\Common\Annotations\AnnotationException $e |
633
|
|
|
* @return string|bool |
634
|
|
|
*/ |
635
|
|
|
public static function highlightAnnotationLine(AnnotationException $e) |
636
|
|
|
{ |
637
|
|
|
foreach ($e->getTrace() as $step) { |
638
|
|
|
if (@$step['class'] . @$step['type'] . @$step['function'] !== Doctrine\Common\Annotations\DocParser::class . '->parse') { |
639
|
|
|
continue; |
640
|
|
|
} |
641
|
|
|
|
642
|
|
|
$context = Strings::match($step['args'][1], '~^(?P<type>[^\s]+)\s*(?P<class>[^:]+)(?:::\$?(?P<property>[^\\(]+))?$~i'); |
643
|
|
|
break; |
644
|
|
|
} |
645
|
|
|
|
646
|
|
|
if (!isset($context)) { |
647
|
|
|
return FALSE; |
648
|
|
|
} |
649
|
|
|
|
650
|
|
|
$refl = Nette\Reflection\ClassType::from($context['class']); |
651
|
|
|
$file = $refl->getFileName(); |
652
|
|
|
$line = NULL; |
653
|
|
|
|
654
|
|
|
if ($context['type'] === 'property') { |
655
|
|
|
$refl = $refl->getProperty($context['property']); |
656
|
|
|
$line = Kdyby\Doctrine\Helpers::getPropertyLine($refl); |
657
|
|
|
|
658
|
|
|
} elseif ($context['type'] === 'method') { |
659
|
|
|
$refl = $refl->getProperty($context['method']); |
660
|
|
|
} |
661
|
|
|
|
662
|
|
|
$errorLine = self::calculateErrorLine($refl, $e, $line); |
663
|
|
|
if ($errorLine === NULL) { |
664
|
|
|
return FALSE; |
665
|
|
|
} |
666
|
|
|
|
667
|
|
|
$dump = BlueScreen::highlightFile($file, $errorLine); |
668
|
|
|
|
669
|
|
|
return '<p><b>File:</b> ' . self::editorLink($file, $errorLine) . '</p>' . $dump; |
670
|
|
|
} |
671
|
|
|
|
672
|
|
|
|
673
|
|
|
|
674
|
|
|
/** |
675
|
|
|
* @param \Reflector|\Nette\Reflection\ClassType|\Nette\Reflection\Method|\Nette\Reflection\Property $refl |
676
|
|
|
* @param \Exception|\Throwable $e |
677
|
|
|
* @param int|NULL $startLine |
678
|
|
|
* @return int|NULL |
679
|
|
|
*/ |
680
|
|
|
public static function calculateErrorLine(\Reflector $refl, $e, $startLine = NULL) |
681
|
|
|
{ |
682
|
|
|
if ($startLine === NULL && method_exists($refl, 'getStartLine')) { |
683
|
|
|
$startLine = $refl->getStartLine(); |
|
|
|
|
684
|
|
|
} |
685
|
|
|
if ($startLine === NULL) { |
686
|
|
|
return NULL; |
687
|
|
|
} |
688
|
|
|
|
689
|
|
|
if ($pos = Strings::match($e->getMessage(), '~position\s*(\d+)~')) { |
690
|
|
|
$targetLine = self::calculateAffectedLine($refl, $pos[1]); |
691
|
|
|
|
692
|
|
|
} elseif ($notImported = Strings::match($e->getMessage(), '~^\[Semantical Error\]\s+The annotation "([^"]*?)"~i')) { |
693
|
|
|
$parts = explode(self::findRenamed($refl, $notImported[1]), self::cleanedPhpDoc($refl), 2); |
694
|
|
|
$targetLine = self::calculateAffectedLine($refl, strlen($parts[0])); |
695
|
|
|
|
696
|
|
|
} elseif ($notFound = Strings::match($e->getMessage(), '~^\[Semantical Error\]\s+Couldn\'t find\s+(.*?)\s+(.*?),\s+~')) { |
697
|
|
|
// this is just a guess |
698
|
|
|
$parts = explode(self::findRenamed($refl, $notFound[2]), self::cleanedPhpDoc($refl), 2); |
699
|
|
|
$targetLine = self::calculateAffectedLine($refl, strlen($parts[0])); |
700
|
|
|
|
701
|
|
|
} else { |
702
|
|
|
$targetLine = self::calculateAffectedLine($refl, 1); |
703
|
|
|
} |
704
|
|
|
|
705
|
|
|
$phpDocLines = count(Strings::split($refl->getDocComment(), '~[\n\r]+~')); |
|
|
|
|
706
|
|
|
|
707
|
|
|
return $startLine - ($phpDocLines - ($targetLine - 1)); |
708
|
|
|
} |
709
|
|
|
|
710
|
|
|
|
711
|
|
|
|
712
|
|
|
/** |
713
|
|
|
* @param \Reflector|\Nette\Reflection\ClassType|\Nette\Reflection\Method $refl |
714
|
|
|
* @param int $symbolPos |
715
|
|
|
* @return int |
716
|
|
|
*/ |
717
|
|
|
protected static function calculateAffectedLine(\Reflector $refl, $symbolPos) |
718
|
|
|
{ |
719
|
|
|
$doc = $refl->getDocComment(); |
|
|
|
|
720
|
|
|
/** @var int|NULL $atPos */ |
721
|
|
|
$atPos = NULL; |
722
|
|
|
$cleanedDoc = self::cleanedPhpDoc($refl, $atPos); |
723
|
|
|
$beforeCleanLines = count(Strings::split(substr($doc, 0, $atPos), '~[\n\r]+~')); |
724
|
|
|
$parsedDoc = substr($cleanedDoc, 0, $symbolPos + 1); |
725
|
|
|
$parsedLines = count(Strings::split($parsedDoc, '~[\n\r]+~')); |
726
|
|
|
|
727
|
|
|
return $parsedLines + max($beforeCleanLines - 1, 0); |
728
|
|
|
} |
729
|
|
|
|
730
|
|
|
|
731
|
|
|
|
732
|
|
|
/** |
733
|
|
|
* @param \Reflector|Nette\Reflection\ClassType|Nette\Reflection\Method $refl |
734
|
|
|
* @param $annotation |
735
|
|
|
*/ |
736
|
|
|
private static function findRenamed(\Reflector $refl, $annotation) |
737
|
|
|
{ |
738
|
|
|
$parser = new Doctrine\Common\Annotations\PhpParser(); |
739
|
|
|
$imports = $parser->parseClass($refl instanceof \ReflectionClass ? $refl : $refl->getDeclaringClass()); |
|
|
|
|
740
|
|
|
|
741
|
|
|
$annotationClass = ltrim($annotation, '@'); |
742
|
|
|
foreach ($imports as $alias => $import) { |
743
|
|
|
if (!Strings::startsWith($annotationClass, $import)) { |
744
|
|
|
continue; |
745
|
|
|
} |
746
|
|
|
|
747
|
|
|
$aliased = str_replace(Strings::lower($import), $alias, Strings::lower($annotationClass)); |
748
|
|
|
$searchFor = preg_quote(Strings::lower($aliased)); |
749
|
|
|
|
750
|
|
|
if (!$m = Strings::match($refl->getDocComment(), "~(?P<usage>@?$searchFor)~i")) { |
|
|
|
|
751
|
|
|
continue; |
752
|
|
|
} |
753
|
|
|
|
754
|
|
|
return $m['usage']; |
755
|
|
|
} |
756
|
|
|
|
757
|
|
|
return $annotation; |
758
|
|
|
} |
759
|
|
|
|
760
|
|
|
|
761
|
|
|
|
762
|
|
|
/** |
763
|
|
|
* @param \Nette\Reflection\ClassType|\Nette\Reflection\Method|\Reflector $refl |
764
|
|
|
* @param int|null $atPos |
765
|
|
|
* @return string |
766
|
|
|
*/ |
767
|
|
|
private static function cleanedPhpDoc(\Reflector $refl, &$atPos = NULL) |
768
|
|
|
{ |
769
|
|
|
return trim(substr($doc = $refl->getDocComment(), $atPos = strpos($doc, '@') - 1), '* /'); |
|
|
|
|
770
|
|
|
} |
771
|
|
|
|
772
|
|
|
|
773
|
|
|
|
774
|
|
|
/** |
775
|
|
|
* Returns link to editor. |
776
|
|
|
* @author David Grudl |
777
|
|
|
* @param string $file |
778
|
|
|
* @param string|int $line |
779
|
|
|
* @param string $text |
780
|
|
|
* @return Nette\Utils\Html |
781
|
|
|
*/ |
782
|
|
|
private static function editorLink($file, $line, $text = NULL) |
783
|
|
|
{ |
784
|
|
|
if (Debugger::$editor && is_file($file) && $text !== NULL) { |
785
|
|
|
return Nette\Utils\Html::el('a') |
786
|
|
|
->href(strtr(Debugger::$editor, ['%file' => rawurlencode($file), '%line' => $line])) |
787
|
|
|
->setAttribute('title', "$file:$line") |
788
|
|
|
->setHtml($text); |
789
|
|
|
|
790
|
|
|
} else { |
791
|
|
|
return Nette\Utils\Html::el()->setHtml(Helpers::editorLink($file, $line)); |
792
|
|
|
} |
793
|
|
|
} |
794
|
|
|
|
795
|
|
|
|
796
|
|
|
|
797
|
|
|
/****************** Registration *********************/ |
798
|
|
|
|
799
|
|
|
|
800
|
|
|
|
801
|
|
|
public function enableLogging() |
802
|
|
|
{ |
803
|
|
|
if ($this->connection === NULL) { |
804
|
|
|
throw new Kdyby\Doctrine\InvalidStateException("Doctrine Panel is not bound to connection."); |
805
|
|
|
} |
806
|
|
|
|
807
|
|
|
$config = $this->connection->getConfiguration(); |
808
|
|
|
$logger = $config->getSQLLogger(); |
809
|
|
|
|
810
|
|
|
if ($logger instanceof Doctrine\DBAL\Logging\LoggerChain) { |
811
|
|
|
$logger->addLogger($this); |
812
|
|
|
|
813
|
|
|
} else { |
814
|
|
|
$config->setSQLLogger($this); |
815
|
|
|
} |
816
|
|
|
} |
817
|
|
|
|
818
|
|
|
|
819
|
|
|
|
820
|
|
|
/** |
821
|
|
|
* @param \Doctrine\DBAL\Connection $connection |
822
|
|
|
* @return Panel |
823
|
|
|
*/ |
824
|
|
|
public function bindConnection(Doctrine\DBAL\Connection $connection) |
825
|
|
|
{ |
826
|
|
|
if ($this->connection !== NULL) { |
827
|
|
|
throw new Kdyby\Doctrine\InvalidStateException("Doctrine Panel is already bound to connection."); |
828
|
|
|
} |
829
|
|
|
|
830
|
|
|
$this->connection = $connection; |
831
|
|
|
|
832
|
|
|
// Tracy |
833
|
|
|
$this->registerBarPanel(Debugger::getBar()); |
834
|
|
|
Debugger::getBlueScreen()->addPanel([$this, 'renderQueryException']); |
835
|
|
|
|
836
|
|
|
return $this; |
837
|
|
|
} |
838
|
|
|
|
839
|
|
|
|
840
|
|
|
|
841
|
|
|
/** |
842
|
|
|
* @param Doctrine\ORM\EntityManager $em |
843
|
|
|
* @return Panel |
844
|
|
|
*/ |
845
|
|
|
public function bindEntityManager(Doctrine\ORM\EntityManager $em) |
|
|
|
|
846
|
|
|
{ |
847
|
|
|
$this->em = $em; |
848
|
|
|
|
849
|
|
|
if ($this->em instanceof Kdyby\Doctrine\EntityManager) { |
850
|
|
|
$uowPanel = new EntityManagerUnitOfWorkSnapshotPanel(); |
851
|
|
|
$uowPanel->bindEntityManager($em); |
852
|
|
|
} |
853
|
|
|
|
854
|
|
|
if ($this->connection === NULL) { |
855
|
|
|
$this->bindConnection($em->getConnection()); |
856
|
|
|
} |
857
|
|
|
|
858
|
|
|
return $this; |
859
|
|
|
} |
860
|
|
|
|
861
|
|
|
|
862
|
|
|
|
863
|
|
|
/** |
864
|
|
|
* Registers panel to debugger |
865
|
|
|
* |
866
|
|
|
* @param \Tracy\Bar $bar |
867
|
|
|
*/ |
868
|
|
|
public function registerBarPanel(Bar $bar) |
869
|
|
|
{ |
870
|
|
|
$bar->addPanel($this); |
871
|
|
|
} |
872
|
|
|
|
873
|
|
|
|
874
|
|
|
|
875
|
|
|
/** |
876
|
|
|
* Registers generic exception renderer |
877
|
|
|
*/ |
878
|
|
|
public static function registerBluescreen(Nette\DI\Container $dic) |
879
|
|
|
{ |
880
|
|
|
Debugger::getBlueScreen()->addPanel(function ($e) use ($dic) { |
881
|
|
|
return Panel::renderException($e, $dic); |
882
|
|
|
}); |
883
|
|
|
} |
884
|
|
|
|
885
|
|
|
} |
886
|
|
|
|
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.
If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.