Panel::highlightQuery()   A
last analyzed

Complexity

Conditions 5
Paths 1

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 34
rs 9.0648
c 0
b 0
f 0
cc 5
nc 1
nop 1
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 float 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 $sql
89
	 * @param array|null $params
90
	 * @param array|null $types
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(): ?string
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(): ?string
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(),
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\DBAL\Connection::getHost() has been deprecated.

This method has been deprecated.

Loading history...
201
				(($p = $this->connection->getPort()) ? ':' . $p : ''),
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\DBAL\Connection::getPort() has been deprecated.

This method has been deprecated.

Loading history...
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 $query
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 (!isset($this->failed[spl_object_hash($e)])) {
318
					return NULL;
319
				}
320
321
				list($sql, $params, , , $source) = $this->failed[spl_object_hash($e)];
322
323
			} else {
324
				list($sql, $params, , $types, $source) = end($this->queries) + range(1, 5);
325
			}
326
327
			if (!$sql) {
328
				return NULL;
329
			}
330
331
			return [
332
				'tab' => 'SQL',
333
				'panel' => $this->dumpQuery($sql, $params, $types, $source),
334
			];
335
336
		} elseif ($e instanceof Kdyby\Doctrine\QueryException && $e->query !== NULL) {
337
			if ($e->query instanceof Doctrine\ORM\Query) {
338
				return [
339
					'tab' => 'DQL',
340
					'panel' => $this->dumpQuery((string) $e->query->getDQL(), $e->query->getParameters()),
341
				];
342
343
			} elseif ($e->query instanceof Kdyby\Doctrine\NativeQueryWrapper) {
344
				return [
345
					'tab' => 'Native SQL',
346
					'panel' => $this->dumpQuery($e->query->getSQL(), $e->query->getParameters()),
347
				];
348
			}
349
		}
350
351
		return NULL;
352
	}
353
354
355
356
	/**
357
	 * @param \Exception|\Throwable $e
358
	 * @param \Nette\DI\Container $dic
359
	 * @return array|NULL
360
	 */
361
	public static function renderException($e, Nette\DI\Container $dic)
362
	{
363
		if ($e instanceof AnnotationException) {
364
			if ($dump = self::highlightAnnotationLine($e)) {
365
				return [
366
					'tab' => 'Annotation',
367
					'panel' => $dump,
368
				];
369
			}
370
371
		} elseif ($e instanceof Doctrine\ORM\Mapping\MappingException) {
372
			if ($invalidEntity = Strings::match($e->getMessage(), '~^Class "([\\S]+)" .*? is not .*? valid~i')) {
373
				$refl = Nette\Reflection\ClassType::from($invalidEntity[1]);
374
				$file = $refl->getFileName();
375
				$errorLine = $refl->getStartLine();
376
377
				return [
378
					'tab' => 'Invalid entity',
379
					'panel' => '<p><b>File:</b> ' . self::editorLink($file, $errorLine) . '</p>' .
380
						BlueScreen::highlightFile($file, $errorLine),
381
				];
382
			}
383
384
		} elseif ($e instanceof Doctrine\DBAL\Schema\SchemaException && $dic && ($em = $dic->getByType(Kdyby\Doctrine\EntityManager::class, FALSE))) {
385
			if (!$em instanceof Kdyby\Doctrine\EntityManager) {
386
				return null;
387
			}
388
389
			if ($invalidTable = Strings::match($e->getMessage(), '~table \'(.*?)\'~i')) {
390
				/** @var Kdyby\Doctrine\Mapping\ClassMetadata $class */
391
				foreach ($em->getMetadataFactory()->getAllMetadata() as $class) {
392
					if ($class->getTableName() === $invalidTable[1]) {
393
						$refl = $class->getReflectionClass();
394
						break;
395
					}
396
				}
397
398
				if (!isset($refl)) {
399
					return NULL;
400
				}
401
402
				$file = $refl->getFileName();
403
				$errorLine = $refl->getStartLine();
404
405
				return [
406
					'tab' => 'Invalid schema',
407
					'panel' => '<p><b>File:</b> ' . self::editorLink($file, $errorLine) . '</p>' .
408
						BlueScreen::highlightFile($file, $errorLine),
409
				];
410
			}
411
412
		} elseif ($e instanceof Kdyby\Doctrine\DBALException && $e->query !== NULL) {
413
			return [
414
				'tab' => 'SQL',
415
				'panel' => self::highlightQuery(static::formatQuery($e->query, $e->params, [])),
416
			];
417
418
		} elseif ($e instanceof Doctrine\DBAL\Exception\DriverException) {
419
			if (($prev = $e->getPrevious()) && ($item = Helpers::findTrace($e->getTrace(), Doctrine\DBAL\DBALException::class . '::driverExceptionDuringQuery'))) {
420
				/** @var \Doctrine\DBAL\Driver $driver */
421
				$driver = $item['args'][0];
422
				$params = isset($item['args'][3]) ? $item['args'][3] : [];
423
424
				return [
425
					'tab' => 'SQL',
426
					'panel' => self::highlightQuery(static::formatQuery($item['args'][2], $params, [], $driver->getDatabasePlatform())),
427
				];
428
			}
429
430
		} elseif ($e instanceof Doctrine\ORM\Query\QueryException) {
431
			if (($prev = $e->getPrevious()) && preg_match('~^(SELECT|INSERT|UPDATE|DELETE)\s+.*~i', $prev->getMessage())) {
432
				return [
433
					'tab' => 'DQL',
434
					'panel' => self::highlightQuery(static::formatQuery($prev->getMessage(), [], [])),
435
				];
436
			}
437
438
		} elseif ($e instanceof \PDOException) {
439
			$params = [];
440
441
			if (isset($e->queryString)) {
442
				$sql = $e->queryString;
0 ignored issues
show
Bug introduced by
The property queryString does not seem to exist. Did you mean string?

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.

Loading history...
443
444
			} elseif ($item = Helpers::findTrace($e->getTrace(), Doctrine\DBAL\Connection::class . '::executeQuery')) {
445
				$sql = $item['args'][0];
446
				$params = $item['args'][1];
447
448
			} elseif ($item = Helpers::findTrace($e->getTrace(), \PDO::class . '::query')) {
449
				$sql = $item['args'][0];
450
451
			} elseif ($item = Helpers::findTrace($e->getTrace(), \PDO::class . '::prepare')) {
452
				$sql = $item['args'][0];
453
			}
454
455
			return isset($sql) ? [
456
				'tab' => 'SQL',
457
				'panel' => self::highlightQuery(static::formatQuery($sql, $params, [])),
458
			] : NULL;
459
		}
460
461
		return NULL;
462
	}
463
464
465
466
	/**
467
	 * @param string $query
468
	 * @param array|Doctrine\Common\Collections\ArrayCollection $params
469
	 * @param array $types
470
	 * @param array|string $source
471
	 * @return string
472
	 */
473
	protected function dumpQuery($query, $params, array $types = [], $source = NULL)
474
	{
475
		if ($params instanceof ArrayCollection) {
476
			$tmp = [];
477
			$tmpTypes = [];
478
			foreach ($params as $key => $param) {
479
				if ($param instanceof Doctrine\ORM\Query\Parameter) {
480
					$tmpTypes[$param->getName()] = $param->getType();
481
					$tmp[$param->getName()] = $param->getValue();
482
					continue;
483
				}
484
				$tmp[$key] = $param;
485
			}
486
			$params = $tmp;
487
			$types = $tmpTypes;
488
		}
489
490
		// query
491
		$s = '<p><b>Query</b></p><table><tr><td class="nette-Doctrine2Panel-sql">';
492
		$s .= self::highlightQuery(static::formatQuery($query, $params, $types, $this->connection ? $this->connection->getDatabasePlatform() : NULL));
493
		$s .= '</td></tr></table>';
494
495
		$e = NULL;
496
		if ($source && is_array($source)) {
497
			list($file, $line) = $source;
498
			$e = '<p><b>File:</b> ' . self::editorLink($file, $line) . '</p>';
499
		}
500
501
		// styles and dump
502
		return $this->renderStyles() . '<div class="nette-inner tracy-inner nette-Doctrine2Panel">' . $e . $s . '</div>';
503
	}
504
505
506
507
	/**
508
	 * Returns syntax highlighted SQL command.
509
	 * This method is same as Nette\Database\Helpers::dumpSql except for parameters handling.
510
	 * @link https://github.com/nette/database/blob/667143b2d5b940f78c8dc9212f95b1bbc033c6a3/src/Database/Helpers.php#L75-L138
511
	 * @author David Grudl
512
	 * @param string $sql
513
	 * @return string
514
	 */
515
	public static function highlightQuery($sql)
516
	{
517
		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';
518
		static $keywords2 = 'ALL|DISTINCT|DISTINCTROW|IGNORE|AS|USING|ON|AND|OR|IN|IS|NOT|NULL|[RI]?LIKE|REGEXP|TRUE|FALSE|WITH|INSTANCE\s+OF';
519
520
		// insert new lines
521
		$sql = " $sql ";
522
		$sql = preg_replace("#(?<=[\\s,(])($keywords1)(?=[\\s,)])#i", "\n\$1", $sql);
523
524
		// reduce spaces
525
		$sql = preg_replace('#[ \t]{2,}#', ' ', $sql);
526
527
		$sql = wordwrap($sql, 100);
528
		$sql = preg_replace('#([ \t]*\r?\n){2,}#', "\n", $sql);
529
530
		// syntax highlight
531
		$sql = htmlspecialchars($sql, ENT_IGNORE, 'UTF-8');
532
		$sql = preg_replace_callback("#(/\\*.+?\\*/)|(\\*\\*.+?\\*\\*)|(?<=[\\s,(])($keywords1)(?=[\\s,)])|(?<=[\\s,(=])($keywords2)(?=[\\s,)=])#is", function ($matches) {
533
			if (!empty($matches[1])) { // comment
534
				return '<em style="color:gray">' . $matches[1] . '</em>';
535
536
			} elseif (!empty($matches[2])) { // error
537
				return '<strong style="color:red">' . $matches[2] . '</strong>';
538
539
			} elseif (!empty($matches[3])) { // most important keywords
540
				return '<strong style="color:blue">' . $matches[3] . '</strong>';
541
542
			} elseif (!empty($matches[4])) { // other keywords
543
				return '<strong style="color:green">' . $matches[4] . '</strong>';
544
			}
545
		}, $sql);
546
547
		return '<pre class="dump">' . trim($sql) . "</pre>\n";
548
	}
549
550
551
552
	/**
553
	 * @param string $query
554
	 * @param array $params
555
	 * @param array $types
556
	 * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform
557
	 * @throws \Doctrine\DBAL\DBALException
558
	 * @throws \Nette\Utils\RegexpException
559
	 * @return string
560
	 */
561
	public static function formatQuery($query, $params, array $types = [], AbstractPlatform $platform = NULL)
562
	{
563
		if ($platform === NULL) {
564
			$platform = new Doctrine\DBAL\Platforms\MySqlPlatform();
565
		}
566
567
		if (!$types) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $types of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
568
			foreach ($params as $key => $param) {
569
				if (is_array($param)) {
570
					$types[$key] = Doctrine\DBAL\Connection::PARAM_STR_ARRAY;
571
572
				} else {
573
					$types[$key] = 'string';
574
				}
575
			}
576
		}
577
578
		try {
579
			list($query, $params, $types) = \Doctrine\DBAL\SQLParserUtils::expandListParameters($query, $params, $types);
580
		} catch (Doctrine\DBAL\SQLParserUtilsException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
581
		}
582
583
		$formattedParams = [];
584
		foreach ($params as $key => $param) {
585
			if (isset($types[$key])) {
586
				if (is_scalar($types[$key]) && array_key_exists($types[$key], Type::getTypesMap())) {
587
					$types[$key] = Type::getType($types[$key]);
588
				}
589
590
				/** @var Type[] $types */
591
				if ($types[$key] instanceof Type) {
592
					$param = $types[$key]->convertToDatabaseValue($param, $platform);
593
				}
594
			}
595
596
			$formattedParams[] = SimpleParameterFormatter::format($param);
597
		}
598
		$params = $formattedParams;
599
600
		if (Nette\Utils\Validators::isList($params)) {
601
			$parts = explode('?', $query);
602
			if (count($params) > $parts) {
603
				throw new Kdyby\Doctrine\InvalidStateException("Too mny parameters passed to query.");
604
			}
605
606
			return implode('', Kdyby\Doctrine\Helpers::zipper($parts, $params));
607
		}
608
609
		return Strings::replace($query, '~(\\:[a-z][a-z0-9]*|\\?[0-9]*)~i', function ($m) use (&$params) {
610
			if (substr($m[0], 0, 1) === '?') {
611
				if (strlen($m[0]) > 1) {
612
					if (isset($params[$k = substr($m[0], 1)])) {
613
						return $params[$k];
614
					}
615
616
				} else {
617
					return array_shift($params);
618
				}
619
620
			} else {
621
				if (isset($params[$k = substr($m[0], 1)])) {
622
					return $params[$k];
623
				}
624
			}
625
626
			return $m[0];
627
		});
628
	}
629
630
631
632
	/**
633
	 * @param \Doctrine\Common\Annotations\AnnotationException $e
634
	 * @return string|bool
635
	 */
636
	public static function highlightAnnotationLine(AnnotationException $e)
637
	{
638
		foreach ($e->getTrace() as $step) {
639
			if (@$step['class'] . @$step['type'] . @$step['function'] !== Doctrine\Common\Annotations\DocParser::class . '->parse') {
640
				continue;
641
			}
642
643
			$context = Strings::match($step['args'][1], '~^(?P<type>[^\s]+)\s*(?P<class>[^:]+)(?:::\$?(?P<property>[^\\(]+))?$~i');
644
			break;
645
		}
646
647
		if (!isset($context)) {
648
			return FALSE;
649
		}
650
651
		$refl = Nette\Reflection\ClassType::from($context['class']);
652
		$file = $refl->getFileName();
653
		$line = NULL;
654
655
		if ($context['type'] === 'property') {
656
			$refl = $refl->getProperty($context['property']);
657
			$line = Kdyby\Doctrine\Helpers::getPropertyLine($refl);
658
659
		} elseif ($context['type'] === 'method') {
660
			$refl = $refl->getProperty($context['method']);
661
		}
662
663
		$errorLine = self::calculateErrorLine($refl, $e, $line);
664
		if ($errorLine === NULL) {
665
			return FALSE;
666
		}
667
668
		$dump = BlueScreen::highlightFile($file, $errorLine);
669
670
		return '<p><b>File:</b> ' . self::editorLink($file, $errorLine) . '</p>' . $dump;
671
	}
672
673
674
675
	/**
676
	 * @param \Reflector|\Nette\Reflection\ClassType|\Nette\Reflection\Method|\Nette\Reflection\Property $refl
677
	 * @param \Exception|\Throwable $e
678
	 * @param int|NULL $startLine
679
	 * @return int|NULL
680
	 */
681
	public static function calculateErrorLine(\Reflector $refl, $e, $startLine = NULL)
682
	{
683
		if ($startLine === NULL && method_exists($refl, 'getStartLine')) {
684
			$startLine = $refl->getStartLine();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Reflector as the method getStartLine() does only exist in the following implementations of said interface: Doctrine\Common\Reflection\StaticReflectionClass, Doctrine\Common\Reflection\StaticReflectionMethod, Nette\Reflection\ClassType, Nette\Reflection\GlobalFunction, Nette\Reflection\Method, ReflectionClass, ReflectionFunction, ReflectionFunctionAbstract, ReflectionMethod, ReflectionObject.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
685
		}
686
		if ($startLine === NULL) {
687
			return NULL;
688
		}
689
690
		if ($pos = Strings::match($e->getMessage(), '~position\s*(\d+)~')) {
691
			$targetLine = self::calculateAffectedLine($refl, $pos[1]);
692
693
		} elseif ($notImported = Strings::match($e->getMessage(), '~^\[Semantical Error\]\s+The annotation "([^"]*?)"~i')) {
694
			$parts = explode(self::findRenamed($refl, $notImported[1]), self::cleanedPhpDoc($refl), 2);
695
			$targetLine = self::calculateAffectedLine($refl, strlen($parts[0]));
696
697
		} elseif ($notFound = Strings::match($e->getMessage(), '~^\[Semantical Error\]\s+Couldn\'t find\s+(.*?)\s+(.*?),\s+~')) {
698
			// this is just a guess
699
			$parts = explode(self::findRenamed($refl, $notFound[2]), self::cleanedPhpDoc($refl), 2);
700
			$targetLine = self::calculateAffectedLine($refl, strlen($parts[0]));
701
702
		} else {
703
			$targetLine = self::calculateAffectedLine($refl, 1);
704
		}
705
706
		$phpDocLines = count(Strings::split($refl->getDocComment(), '~[\n\r]+~'));
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Reflector as the method getDocComment() does only exist in the following implementations of said interface: Doctrine\Common\Reflecti...ublicReflectionProperty, Doctrine\Common\Reflection\StaticReflectionClass, Doctrine\Common\Reflection\StaticReflectionMethod, Doctrine\Common\Reflecti...taticReflectionProperty, Doctrine\ORM\Mapping\ReflectionEmbeddedProperty, Nette\Reflection\ClassType, Nette\Reflection\GlobalFunction, Nette\Reflection\Method, Nette\Reflection\Property, ReflectionClass, ReflectionFunction, ReflectionFunctionAbstract, ReflectionMethod, ReflectionObject, ReflectionProperty.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
707
708
		return $startLine - ($phpDocLines - ($targetLine - 1));
709
	}
710
711
712
713
	/**
714
	 * @param \Reflector|\Nette\Reflection\ClassType|\Nette\Reflection\Method $refl
715
	 * @param int $symbolPos
716
	 * @return int
717
	 */
718
	protected static function calculateAffectedLine(\Reflector $refl, $symbolPos)
719
	{
720
		$doc = $refl->getDocComment();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Reflector as the method getDocComment() does only exist in the following implementations of said interface: Doctrine\Common\Reflecti...ublicReflectionProperty, Doctrine\Common\Reflection\StaticReflectionClass, Doctrine\Common\Reflection\StaticReflectionMethod, Doctrine\Common\Reflecti...taticReflectionProperty, Doctrine\ORM\Mapping\ReflectionEmbeddedProperty, Nette\Reflection\ClassType, Nette\Reflection\GlobalFunction, Nette\Reflection\Method, Nette\Reflection\Property, ReflectionClass, ReflectionFunction, ReflectionFunctionAbstract, ReflectionMethod, ReflectionObject, ReflectionProperty.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
721
		/** @var int|NULL $atPos */
722
		$atPos = NULL;
723
		$cleanedDoc = self::cleanedPhpDoc($refl, $atPos);
724
		$beforeCleanLines = count(Strings::split(substr($doc, 0, $atPos), '~[\n\r]+~'));
725
		$parsedDoc = substr($cleanedDoc, 0, $symbolPos + 1);
726
		$parsedLines = count(Strings::split($parsedDoc, '~[\n\r]+~'));
727
728
		return $parsedLines + max($beforeCleanLines - 1, 0);
729
	}
730
731
732
733
	/**
734
	 * @param \Reflector|Nette\Reflection\ClassType|Nette\Reflection\Method $refl
735
	 * @param string $annotation
736
	 * @return string
737
	 */
738
	private static function findRenamed(\Reflector $refl, $annotation)
739
	{
740
		$parser = new Doctrine\Common\Annotations\PhpParser();
741
		$imports = $parser->parseClass($refl instanceof \ReflectionClass ? $refl : $refl->getDeclaringClass());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Reflector as the method getDeclaringClass() does only exist in the following implementations of said interface: Doctrine\Common\Reflecti...ublicReflectionProperty, Doctrine\Common\Reflection\StaticReflectionMethod, Doctrine\Common\Reflecti...taticReflectionProperty, Doctrine\ORM\Mapping\ReflectionEmbeddedProperty, Nette\Reflection\Method, Nette\Reflection\Parameter, Nette\Reflection\Property, ReflectionMethod, ReflectionParameter, ReflectionProperty.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
742
743
		$annotationClass = ltrim($annotation, '@');
744
		foreach ($imports as $alias => $import) {
745
			if (!Strings::startsWith($annotationClass, $import)) {
746
				continue;
747
			}
748
749
			$aliased = str_replace(Strings::lower($import), $alias, Strings::lower($annotationClass));
750
			$searchFor = preg_quote(Strings::lower($aliased));
751
752
			if (!$m = Strings::match($refl->getDocComment(), "~(?P<usage>@?$searchFor)~i")) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Reflector as the method getDocComment() does only exist in the following implementations of said interface: Doctrine\Common\Reflecti...ublicReflectionProperty, Doctrine\Common\Reflection\StaticReflectionClass, Doctrine\Common\Reflection\StaticReflectionMethod, Doctrine\Common\Reflecti...taticReflectionProperty, Doctrine\ORM\Mapping\ReflectionEmbeddedProperty, Nette\Reflection\ClassType, Nette\Reflection\GlobalFunction, Nette\Reflection\Method, Nette\Reflection\Property, ReflectionClass, ReflectionFunction, ReflectionFunctionAbstract, ReflectionMethod, ReflectionObject, ReflectionProperty.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
753
				continue;
754
			}
755
756
			return $m['usage'];
757
		}
758
759
		return $annotation;
760
	}
761
762
763
764
	/**
765
	 * @param \Nette\Reflection\ClassType|\Nette\Reflection\Method|\Reflector $refl
766
	 * @param int|null $atPos
767
	 * @return string
768
	 */
769
	private static function cleanedPhpDoc(\Reflector $refl, &$atPos = NULL)
770
	{
771
		return trim(substr($doc = $refl->getDocComment(), $atPos = strpos($doc, '@') - 1), '* /');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Reflector as the method getDocComment() does only exist in the following implementations of said interface: Doctrine\Common\Reflecti...ublicReflectionProperty, Doctrine\Common\Reflection\StaticReflectionClass, Doctrine\Common\Reflection\StaticReflectionMethod, Doctrine\Common\Reflecti...taticReflectionProperty, Doctrine\ORM\Mapping\ReflectionEmbeddedProperty, Nette\Reflection\ClassType, Nette\Reflection\GlobalFunction, Nette\Reflection\Method, Nette\Reflection\Property, ReflectionClass, ReflectionFunction, ReflectionFunctionAbstract, ReflectionMethod, ReflectionObject, ReflectionProperty.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
772
	}
773
774
775
776
	/**
777
	 * Returns link to editor.
778
	 * @author David Grudl
779
	 * @param string $file
780
	 * @param int|null $line
781
	 * @param string $text
782
	 * @return Nette\Utils\Html
783
	 */
784
	private static function editorLink($file, $line, $text = NULL)
785
	{
786
		if (Debugger::$editor && is_file($file) && $text !== NULL) {
787
			return Nette\Utils\Html::el('a')
788
				->href(strtr(Debugger::$editor, ['%file' => rawurlencode($file), '%line' => $line]))
789
				->setAttribute('title', "$file:$line")
790
				->setHtml($text);
791
792
		} else {
793
			return Nette\Utils\Html::el()->setHtml(Helpers::editorLink($file, $line));
794
		}
795
	}
796
797
798
799
	/****************** Registration *********************/
800
801
802
803
	public function enableLogging()
804
	{
805
		if ($this->connection === NULL) {
806
			throw new Kdyby\Doctrine\InvalidStateException("Doctrine Panel is not bound to connection.");
807
		}
808
809
		$config = $this->connection->getConfiguration();
810
		$logger = $config->getSQLLogger();
811
812
		if ($logger instanceof Doctrine\DBAL\Logging\LoggerChain) {
813
			$logger->addLogger($this);
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\DBAL\Logging\LoggerChain::addLogger() has been deprecated with message: Inject list of loggers via constructor instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
814
815
		} else {
816
			$config->setSQLLogger($this);
817
		}
818
	}
819
820
821
822
	/**
823
	 * @param \Doctrine\DBAL\Connection $connection
824
	 * @return Panel
825
	 */
826
	public function bindConnection(Doctrine\DBAL\Connection $connection)
827
	{
828
		if ($this->connection !== NULL) {
829
			throw new Kdyby\Doctrine\InvalidStateException("Doctrine Panel is already bound to connection.");
830
		}
831
832
		$this->connection = $connection;
833
834
		// Tracy
835
		$this->registerBarPanel(Debugger::getBar());
836
		Debugger::getBlueScreen()->addPanel([$this, 'renderQueryException']);
837
838
		return $this;
839
	}
840
841
842
843
	/**
844
	 * @param Doctrine\ORM\EntityManager $em
845
	 * @return Panel
846
	 */
847
	public function bindEntityManager(Doctrine\ORM\EntityManager $em)
0 ignored issues
show
Bug introduced by
You have injected the EntityManager via parameter $em. This is generally not recommended as it might get closed and become unusable. Instead, it is recommended to inject the ManagerRegistry and retrieve the EntityManager via getManager() each time you need it.

The EntityManager might become unusable for example if a transaction is rolled back and it gets closed. Let’s assume that somewhere in your application, or in a third-party library, there is code such as the following:

function someFunction(ManagerRegistry $registry) {
    $em = $registry->getManager();
    $em->getConnection()->beginTransaction();
    try {
        // Do something.
        $em->getConnection()->commit();
    } catch (\Exception $ex) {
        $em->getConnection()->rollback();
        $em->close();

        throw $ex;
    }
}

If that code throws an exception and the EntityManager is closed. Any other code which depends on the same instance of the EntityManager during this request will fail.

On the other hand, if you instead inject the ManagerRegistry, the getManager() method guarantees that you will always get a usable manager instance.

Loading history...
848
	{
849
		$this->em = $em;
850
851
		if ($this->em instanceof Kdyby\Doctrine\EntityManager) {
852
			$uowPanel = new EntityManagerUnitOfWorkSnapshotPanel();
853
			$uowPanel->bindEntityManager($em);
854
		}
855
856
		if ($this->connection === NULL) {
857
			$this->bindConnection($em->getConnection());
858
		}
859
860
		return $this;
861
	}
862
863
864
865
	/**
866
	 * Registers panel to debugger
867
	 *
868
	 * @param \Tracy\Bar $bar
869
	 */
870
	public function registerBarPanel(Bar $bar)
871
	{
872
		$bar->addPanel($this);
873
	}
874
875
876
877
	/**
878
	 * Registers generic exception renderer
879
	 */
880
	public static function registerBluescreen(Nette\DI\Container $dic)
881
	{
882
		Debugger::getBlueScreen()->addPanel(function ($e) use ($dic) {
883
			return Panel::renderException($e, $dic);
884
		});
885
	}
886
887
}
888