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