From c699a75b26310efc4da25071e18115c067fd54a3 Mon Sep 17 00:00:00 2001
From: David Grudl
+ {if $type === main}
+
diff --git a/src/Tracy/Bar/assets/bar.phtml b/src/Tracy/Bar/assets/bar.phtml
deleted file mode 100644
index 146798944..000000000
--- a/src/Tracy/Bar/assets/bar.phtml
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
diff --git a/src/Tracy/Bar/assets/loader.latte b/src/Tracy/Bar/assets/loader.latte
new file mode 100644
index 000000000..a34468b78
--- /dev/null
+++ b/src/Tracy/Bar/assets/loader.latte
@@ -0,0 +1,22 @@
+{*
+ * @var bool $async
+ * @var string $requestId
+ *}
+
+{do $nonce = Tracy\Helpers::getNonce()}
+{do $baseUrl = $_SERVER['REQUEST_URI'] ?? ''}
+{do $baseUrl .= strpos($baseUrl, '?') === false ? '?' : '&'}
+
+{if empty($content)}
+
+{else}
+
+
+
+
+
+
+
+{/if}
diff --git a/src/Tracy/Bar/assets/loader.phtml b/src/Tracy/Bar/assets/loader.phtml
deleted file mode 100644
index 11247c664..000000000
--- a/src/Tracy/Bar/assets/loader.phtml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Tracy/Bar/assets/panels.latte b/src/Tracy/Bar/assets/panels.latte
new file mode 100644
index 000000000..f159a1f0c
--- /dev/null
+++ b/src/Tracy/Bar/assets/panels.latte
@@ -0,0 +1,19 @@
+{*
+ * @var string $type
+ * @var stdClass[] $panels
+ *}
+{use Tracy\Dumper}
+
+{do $icons = '
+
+'}
+
+
diff --git a/src/Tracy/Bar/dist/dumps.panel.phtml b/src/Tracy/Bar/dist/dumps.panel.phtml
new file mode 100644
index 000000000..3054b46b7
--- /dev/null
+++ b/src/Tracy/Bar/dist/dumps.panel.phtml
@@ -0,0 +1,24 @@
+
+ $data
+ */ ?>
+
+Dumps
+
+= Tracy\Helpers::escapeHtml($item['title']) ?>
+
+
+ = $item['dump'] ?>
+
+System info
+
+
+ $val): ?>
+
+
+ 25): ?>
+ = Tracy\Helpers::escapeHtml($key) ?>
+ = Tracy\Helpers::escapeHtml($val) ?>
+
+ = Tracy\Helpers::escapeHtml($key) ?>
+ = Tracy\Helpers::escapeHtml($val) ?>
+
+
+ Composer Packages (= Tracy\Helpers::escapeHtml(count($packages)) ?>
+= Tracy\Helpers::escapeHtml($devPackages ? ' + ' . count($devPackages) . ' dev' : '') ?>
+)
+
+
+
+
+
+
+
+ = Tracy\Helpers::escapeHtml($package->name) ?>
+
+ = Tracy\Helpers::escapeHtml($package->version) ?>
+= Tracy\Helpers::escapeHtml(strpos($package->version, 'dev') !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : '') ?>
+
+ Dev Packages
+
+
+
+
+
+ = Tracy\Helpers::escapeHtml($package->name) ?>
+
+ = Tracy\Helpers::escapeHtml($package->version) ?>
+= Tracy\Helpers::escapeHtml(strpos($package->version, 'dev') !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : '') ?>
+
+ Warnings
+
+
+ $count): ?>
+
+
+ = Tracy\Helpers::escapeHtml($count ? $count . '×' : '') ?>
+
+
+ = Tracy\Helpers::escapeHtml($message) ?>
+ in = Tracy\Helpers::editorLink($file, (int) $line) ?>
+
Dumps
+
+{$item[title]}
+ {/if}
+
+ {$item[dump]|noescape}
+ {/foreach}
+Dumps
-
-= Helpers::escapeHtml($item['title']) ?>
-
-
- = $item['dump'] ?>
-
-System info
+
+
+ {foreach $info as $key => $val}
+
+
+ {if $packages || $devPackages}
+
+ {if strlen($val) > 25}
+
+ {/foreach}
+ {$key} {$val}
+ {else}
+ {$key} {$val}
+ {/if}
+
+ Composer Packages ({count($packages)}{$devPackages ? ' + ' . count($devPackages) . ' dev' : ''})
+
+
+
+ {foreach $packages as $package}
+
+ {/if}
+
+ {if $devPackages}
+
+
+ {/foreach}
+ {$package->name}
+ {$package->version}{strpos($package->version, dev) !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : ''}
+ Dev Packages
+
+
+ {foreach $devPackages as $package}
+
+ {/if}
+
+
+ {/foreach}
+ {$package->name}
+ {$package->version}{strpos($package->version, dev) !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : ''}
+ System info
-
-
- $val): ?>
-
-
-
-
- 25): ?>
-
-
-= Helpers::escapeHtml($key) ?> = Helpers::escapeHtml($val) ?>
-
- = Helpers::escapeHtml($key) ?> = Helpers::escapeHtml($val) ?>
-
-Composer Packages (= count($packages), $devPackages ? ' + ' . count($devPackages) . ' dev' : '' ?>)
-
-
-
-
-
-
-
-
-
- = Helpers::escapeHtml($package->name) ?> = Helpers::escapeHtml($package->version . (strpos($package->version, 'dev') !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : '')) ?> Dev Packages
-
-
-
-
-
-
- = Helpers::escapeHtml($package->name) ?> = Helpers::escapeHtml($package->version . (strpos($package->version, 'dev') !== false && $package->hash ? ' #' . substr($package->hash, 0, 4) : '')) ?> Warnings
+
+
+ {foreach $data as $item => $count}
+ {do [$file, $line, $message] = explode('|', $item, 3)}
+
+
+
+ {/foreach}
+ {$count ? $count . '×' : ''}
+
+ {$message} in {Tracy\Helpers::editorLink($file, (int) $line)}Warnings
-
-
- $count): [$file, $line, $message] = explode('|', $item, 3) ?>
-
-
-
-
-= $count ? "$count\xC3\x97" : '' ?>
-
-= Helpers::escapeHtml($message), ' in ', Helpers::editorLink($file, (int) $line) ?>
{$panel->tab}
+
+ {$panel->tab}
+
+ = Helpers::escapeHtml($panel->tab) ?>
-
- = Helpers::escapeHtml($panel->tab) ?>
-
-
php{$tmp[1]}
+ {/if}
+
+ {if isset($_SERVER[argv])}
+ | {$k} | +{$dump($v, $k)} | +
|---|
php= Helpers::escapeHtml($tmp[1]) ?>- - - -
| = Helpers::escapeHtml($k) ?> | = $dump($v, $k) ?> |
|---|
| {$k} | +{$dump($v, $k)} | +
|---|
| {$k} | +{if $k === __NF}Nette Session{else}{$dump($v, $k)}{/if} | +
|---|
| {$k} | +{$dump($v, $k)} | +
|---|
| {$k} | +{$dump($v, $k)} | +
|---|
| = Helpers::escapeHtml($k) ?> | = $dump($v, $k) ?> |
|---|
| = Helpers::escapeHtml($k) ?> | = $k === '__NF' ? 'Nette Session' : $dump($v, $k) ?> |
|---|
| = Helpers::escapeHtml($k) ?> | = $dump($v, $k) ?> |
|---|
| = Helpers::escapeHtml($k) ?> | = $dump($v, $k) ?> |
|---|
{$title}{$code}
{/if} + += Helpers::escapeHtml($title . $code) ?>
- - -| {$k} | +{$dump($v, $k)} | +
|---|
empty
+ {else} +| {$k} | +{$dump($v, $k)} | +
|---|
empty
+ {else} +| {$k} | +{$dump($v, $k)} | +
|---|
empty
+ {else} +| {$k} | +{$dump($v, $k)} | +
|---|
| {$s[0]} | +{$dump(trim($s[1]), $s[0])} | +
|---|
no headers
+ {/if} + + {if $headersSent && $headersFile && $headersLine !== null && @is_file($headersFile)} +Headers have been sent, output started at {Helpers::editorLink($headersFile, $headersLine)} source
+ +Headers have been sent
+ {else} +Headers were not sent at the time the exception was thrown
+ {/if} +| = Helpers::escapeHtml($k) ?> | = $dump($v, $k) ?> |
|---|
empty
- -| = Helpers::escapeHtml($k) ?> | = $dump($v, $k) ?> |
|---|
empty
- -| = Helpers::escapeHtml($k) ?> | = $dump($v, $k) ?> |
|---|
empty
- -| = Helpers::escapeHtml($k) ?> | = $dump($v, $k) ?> |
|---|
| = Helpers::escapeHtml($s[0]) ?> | = $dump(trim($s[1]), $s[0]) ?> |
|---|
no headers
- - - - -Headers have been sent, output started at = Helpers::editorLink($headersFile, $headersLine) ?> source
-Headers have been sent
- -Headers were not sent at the time the exception was thrown
- -Note: the last muted error may have nothing to do with the thrown exception.
+ + {if @is_file($lastError[file])} +{Helpers::editorLink($lastError[file], $lastError[line])}
+ +inner-code:{$lastError[line]}
+ {/if} +Note: the last muted error may have nothing to do with the thrown exception.
- - -= Helpers::editorLink($lastError['file'], $lastError['line']) ?>
-inner-code
- -| {is_string($argName) ? '$' : '#'}{$argName} | +{$dump($v, (string) $argName)} | +
|---|
File: {Helpers::editorLink(...$sourceOriginal)}
+ {BlueScreen::highlightFile(...$sourceOriginal)} +File: {Helpers::editorLink($sourceMapped[file], $sourceMapped[line])}
+ {BlueScreen::highlightFile($sourceMapped[file], line: $sourceMapped[line], column: $sourceMapped[column], php: false)} +File: {Helpers::editorLink($file, $line)}
+ {if $sourceOriginal}{BlueScreen::highlightFile(...$sourceOriginal)}{/if} + {/if} +File: = Helpers::editorLink(...$sourceOriginal) ?>
- = BlueScreen::highlightFile(...$sourceOriginal) ?> -File: = Helpers::editorLink($sourceMapped['file'], $sourceMapped['line']) ?>
- = BlueScreen::highlightFile($sourceMapped['file'], line: $sourceMapped['line'] ?? 0, column: $sourceMapped['column'] ?? 0, php: false) ?> -File: = Helpers::editorLink($file, $line) ?>
- - - -php= Tracy\Helpers::escapeHtml($tmp[1]) ?> ++ +
| = Tracy\Helpers::escapeHtml($k) ?> + | += $dump($v, $k) ?> + | +
|---|
| = Tracy\Helpers::escapeHtml($k) ?> + | += $dump($v, $k) ?> + | +
|---|
| = Tracy\Helpers::escapeHtml($k) ?> + | ++Nette Session += $dump($v, $k) ?> + + | +
|---|
| = Tracy\Helpers::escapeHtml($k) ?> + | += $dump($v, $k) ?> + | +
|---|
| = Tracy\Helpers::escapeHtml($k) ?> + | += $dump($v, $k) ?> + | +
|---|
= Tracy\Helpers::escapeHtml($title) ?> += Tracy\Helpers::escapeHtml($code) ?> +
+ + +| = Tracy\Helpers::escapeHtml($k) ?> + | += $dump($v, $k) ?> + | +
|---|
empty
+| = Tracy\Helpers::escapeHtml($k) ?> + | += $dump($v, $k) ?> + | +
|---|
empty
+| = Tracy\Helpers::escapeHtml($k) ?> + | += $dump($v, $k) ?> + | +
|---|
empty
+| = Tracy\Helpers::escapeHtml($k) ?> + | += $dump($v, $k) ?> + | +
|---|
| = Tracy\Helpers::escapeHtml($s[0]) ?> + | += $dump(trim($s[1]), $s[0]) ?> + | +
|---|
no headers
+ +Headers have been sent, output started at = Helpers::editorLink($headersFile, $headersLine) ?> + source
+ +Headers have been sent
+Headers were not sent at the time the exception was thrown
+Note: the last muted error may have nothing to do with the thrown exception.
+ += Helpers::editorLink($lastError['file'], $lastError['line']) ?> +
+ +inner-code:= Tracy\Helpers::escapeHtml($lastError['line']) ?> +
+File: = Helpers::editorLink(...$sourceOriginal) ?> +
+ = BlueScreen::highlightFile(...$sourceOriginal) ?> + +File: = Helpers::editorLink($sourceMapped['file'], $sourceMapped['line']) ?> +
+ = BlueScreen::highlightFile($sourceMapped['file'], line: $sourceMapped['line'], column: $sourceMapped['column'], php: false) ?> + +File: = Helpers::editorLink($file, $line) ?> +
+ + = BlueScreen::highlightFile(...$sourceOriginal) ?> + + +We're sorry! The server encountered an internal error and + was unable to complete your request. Please try again later.
+ +error 500 | {date('j. n. Y H:i')}{if !$logged}
Tracy is unable to log error.{/if}
We're sorry! The server encountered an internal error and was unable to complete your request. Please try again later.
-error 500 |
Tracy is unable to log error.
error 500 | = Tracy\Helpers::escapeHtml(date('j. n. Y H:i')) ?>
+
+
Tracy is unable to log error.
+
barDump('value') 📍'value'+
| 1%a% | -Notice: Only variables should be assigned by reference in %a%:%d% |
-
| 1%a% | -Warning: hex2bin(): Hexadecimal input string must have an even length in %a%:%d% |
-
| 1%a% | -Compile Warning: Unsupported declare 'foo' in %a%:%d% |
-
Notice: Only variables should be assigned by reference in %a%:%d%
Warning: hex2bin(): Hexadecimal input string must have an even length in %a%:%d%
Compile Warning: Unsupported declare 'foo' in %a%:%d%
User Error
-File: %a% : eval()'d code:1
- +File: %a% : eval()'d code:1
| $message | + |
+ ||||
|---|---|---|---|---|---|
| $error%a% | +256
+ |
+
+ +
+ +
| $user | +'root'
+ |
+
|---|---|
| $pass | ++ | +
- -
- -
| $user | 'root'
- |
|---|---|
| $pass | - |
%A%+ +
Headers were not sent at the time the exception was thrown
+Headers were not sent at the time the exception was thrown
+Exception #123
-Exception #123
-Exception #123
-File: %a%
--%d%: +File: %a%Debugger.exception.html.phpt:%d%
+%d%: %d%: %d%: function second($arg1, $arg2) %d%: { @@ -51,23 +53,25 @@- Call stack
++ Call stack +
-- - - -- third (...) -- --%d%: ++ + +- - -+ +third (...) + ++ ++- - - --%d%: %d%: %d%: function first($arg1, $arg2) %d%: { @@ -84,22 +88,26 @@ %d%: function third($arg1)-
-- $arg1 - - second (...) -- ---+ + +19: ++
++ +$arg1 ++ ++ +second (...) + ++ ++- - - --%d%: %d%: %d%: Debugger::$productionMode = false; %d%: setHtmlMode(); @@ -116,24 +124,31 @@ %d%: function second($arg1, $arg2)-
-- $arg1 true-- $arg2 false-- first (...) -- --+ + +%d%: ++
++ +$arg1 ++ true++ +$arg2 ++ false++ +first (...) + ++ ++-%d%: %d%: %d%: function third($arg1) %d%: { @@ -146,133 +161,126 @@ %d%: first(10, 'any string');-
+- $arg1 10-- $arg2 'any string'-+
++ +$arg1 ++ 10++ +$arg2 ++ 'any string'+- - -Last muted error
++ Last muted error +
++Warning: hex2bin(): Hexadecimal input string must have an even length
-Warning: hex2bin(): Hexadecimal input string must have an even length
-Note: the last muted error may have nothing to do with the thrown exception.
+Note: the last muted error may have nothing to do with the thrown exception.
-%a%Debugger.exception.html.phpt:%d%
-%A%+%a%Debugger.exception.html.phpt:%d%
+ +%A%- Environment
++ Environment +
- -- - - ---- - - - - -- %A% -
--- - -- %A% -
--%A%%A% - - - -
+ @@ -16,24 +17,25 @@-- - -++ + ++++ ++ %A% +
++++ %A% +
++%A%+ +++ +- HTTP
++ HTTP +
- -- - -- - + + diff --git a/tests/Tracy/expected/Debugger.exception.in-generator.html.expect b/tests/Tracy/expected/Debugger.exception.in-generator.html.expect index 7d4d5238c..8292b7857 100644 --- a/tests/Tracy/expected/Debugger.exception.in-generator.html.expect +++ b/tests/Tracy/expected/Debugger.exception.in-generator.html.expect @@ -1,4 +1,5 @@- - - - -- ---Code: %d%
--- - -%A%
-Headers were not sent at the time the exception was thrown
++ + ++ + + ++Code: %d%
+ ++++%A% +
+Headers were not sent at the time the exception was thrown
+ + - - - + Exception #123
-The my exception - search► +
+ The my exception + search►
-- Source file
++ Source file +
-File: %a%Debugger.exception.in-generator.html.phpt:%d%
--%d%: +File: %a%Debugger.exception.in-generator.html.phpt:%d%
+%d%: %d%: %d%: Debugger::$productionMode = false; %d%: setHtmlMode(); @@ -51,23 +53,25 @@- Call stack
++ Call stack +
-- - - -- {closure%a?%} () -- --+ @@ -16,204 +17,211 @@%A% + + + +%A% \ No newline at end of file diff --git a/tests/Tracy/expected/Debugger.strict.html.expect b/tests/Tracy/expected/Debugger.strict.html.expect index 6c0d14b74..b5bbcd216 100644 --- a/tests/Tracy/expected/Debugger.strict.html.expect +++ b/tests/Tracy/expected/Debugger.strict.html.expect @@ -1,4 +1,5 @@%d%: setHtmlMode(); ++ + + + ++ - + -+- - +-%d%: setHtmlMode(); %d%: %d%: Debugger::enable(); %d%: @@ -84,18 +88,19 @@ %d%: $fn($generator);--%d%: ++-%d%: %d%: $generator = (function (): iterable { %d%: yield 5; %d%: throw new Exception('The my exception', 123); @@ -108,12 +113,19 @@ %d%: $fn($generator);-
+- #0 - +
++ +#0 ++ + + - - - + Notice
-Only variables should be assigned by reference - search► +
+ Only variables should be assigned by reference + search►
-- -Source file
++ Source file +
- Call stack
++ Call stack +
-- - - --- third (...) -- --- - - -- --
-- $arg1 - - second (...) -- --- - - -- --
-- $arg1 true-- $arg2 false-- first (...) -- ---- --
+- $arg1 %d%-- $arg2 'any string'-+ + ++ +third (...) + ++ +++ + ++ ++
++ +$arg1 ++ ++ +second (...) + ++ +++ + ++ ++
++ +$arg1 ++ true++ +$arg2 ++ false++ +first (...) + ++ +++ ++
++ +$arg1 ++ 10++ +$arg2 ++ 'any string'+- - - - -Exception
++ Exception +
++- Environment
++ Environment +
- -- - - ---- - - - - -- %A% -
--- - -- %A% -
--%A%%A% - - - -
-- - -++ + ++++ ++ %A% +
++++ %A% +
++%A%+ ++%A%+ +- HTTP
++ HTTP +
- -'; @@ -295,11 +295,12 @@ private function renderObject(Value $object, int $depth): string ); } - $pos = strrpos($object->value, '\\'); + $name = (string) $object->value; + $pos = strrpos($name, '\\'); $out = '' . ($pos - ? Helpers::escapeHtml(substr($object->value, 0, $pos + 1)) . '' . Helpers::escapeHtml(substr($object->value, $pos + 1)) . '' - : Helpers::escapeHtml($object->value)) + ? Helpers::escapeHtml(substr($name, 0, $pos + 1)) . '' . Helpers::escapeHtml(substr($name, $pos + 1)) . '' + : Helpers::escapeHtml($name)) . '' . ($object->id && $this->hash ? ' #' . $object->id . '' : ''); @@ -360,8 +361,8 @@ private function renderObject(Value $object, int $depth): string private function renderResource(Value $resource, int $depth): string { - $out = '' . Helpers::escapeHtml($resource->value) . ' ' - . ($this->hash ? '@' . substr($resource->id, 1) . '' : ''); + $out = '' . Helpers::escapeHtml((string) $resource->value) . ' ' + . ($this->hash ? '@' . substr((string) $resource->id, 1) . '' : ''); if (!$resource->items) { return $out; diff --git a/src/Tracy/Helpers.php b/src/Tracy/Helpers.php index 1ae08ba78..1c59dea3f 100644 --- a/src/Tracy/Helpers.php +++ b/src/Tracy/Helpers.php @@ -173,7 +173,7 @@ public static function improveException(\Throwable $e): void ) { // do nothing } elseif (preg_match('~Argument #(\d+)(?: \(\$\w+\))? must be of type callable, (.+ given)~', $message, $m)) { - $arg = $e->getTrace()[0]['args'][$m[1] - 1] ?? null; + $arg = $e->getTrace()[0]['args'][(int) $m[1] - 1] ?? null; if (is_string($arg) && str_contains($arg, '::')) { $arg = explode('::', $arg, 2); } @@ -204,7 +204,7 @@ public static function improveException(\Throwable $e): void $replace = ["$m[2](", "$hint("]; } - } elseif (preg_match('#^Undefined property: ([\w\\\]+)::\$(\w+)#', $message, $m)) { + } elseif (preg_match('#^Undefined property: ([\w\\\]+)::\$(\w+)#', $message, $m) && class_exists($m[1])) { $rc = new \ReflectionClass($m[1]); $items = array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), fn($prop) => !$prop->isStatic()); if ($hint = self::getSuggestion($items, $m[2])) { @@ -212,7 +212,10 @@ public static function improveException(\Throwable $e): void $replace = ["->$m[2]", "->$hint"]; } - } elseif (preg_match('#^Access to undeclared static property:? ([\w\\\]+)::\$(\w+)#', $message, $m)) { + } elseif ( + preg_match('#^Access to undeclared static property:? ([\w\\\]+)::\$(\w+)#', $message, $m) + && class_exists($m[1]) + ) { $rc = new \ReflectionClass($m[1]); $items = array_filter($rc->getProperties(\ReflectionProperty::IS_STATIC), fn($prop) => $prop->isPublic()); if ($hint = self::getSuggestion($items, $m[2])) { @@ -239,7 +242,7 @@ public static function improveException(\Throwable $e): void /** @internal */ public static function improveError(string $message): string { - if (preg_match('#^Undefined property: ([\w\\\]+)::\$(\w+)#', $message, $m)) { + if (preg_match('#^Undefined property: ([\w\\\]+)::\$(\w+)#', $message, $m) && class_exists($m[1])) { $rc = new \ReflectionClass($m[1]); $items = array_filter($rc->getProperties(\ReflectionProperty::IS_PUBLIC), fn($prop) => !$prop->isStatic()); $hint = self::getSuggestion($items, $m[2]); @@ -443,7 +446,7 @@ public static function utf8Length(string $s): int { return match (true) { extension_loaded('mbstring') => mb_strlen($s, 'UTF-8'), - extension_loaded('iconv') => iconv_strlen($s, 'UTF-8'), + extension_loaded('iconv') => iconv_strlen($s, 'UTF-8') ?: strlen($s), default => strlen(@utf8_decode($s)), // deprecated }; } diff --git a/src/Tracy/Logger/Logger.php b/src/Tracy/Logger/Logger.php index 5670967f0..c17e02760 100644 --- a/src/Tracy/Logger/Logger.php +++ b/src/Tracy/Logger/Logger.php @@ -136,6 +136,7 @@ public function getExceptionFile(\Throwable $exception, string $level = self::EX } $hash = substr(hash('xxh128', serialize($data)), 0, 10); + assert($this->directory !== null); $dir = strtr($this->directory . '/', '\/', DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR); foreach (new \DirectoryIterator($this->directory) as $file) { if (strpos($file->getBasename(), $hash)) { diff --git a/src/Tracy/Session/FileSession.php b/src/Tracy/Session/FileSession.php index 1291feafe..a92f8370a 100644 --- a/src/Tracy/Session/FileSession.php +++ b/src/Tracy/Session/FileSession.php @@ -59,12 +59,12 @@ private function open(): void $file = @fopen($path = $this->dir . '/' . self::FilePrefix . $id, 'c+'); // intentionally @ if ($file === false) { - throw new \RuntimeException("Unable to create file '$path'. " . error_get_last()['message']); + throw new \RuntimeException("Unable to create file '$path'. " . (error_get_last()['message'] ?? '')); } } if (!@flock($file, LOCK_EX)) { // intentionally @ - throw new \RuntimeException("Unable to acquire exclusive lock on '$path'. " . error_get_last()['message']); + throw new \RuntimeException("Unable to acquire exclusive lock on '$path'. " . (error_get_last()['message'] ?? '')); } $this->file = $file; @@ -85,7 +85,7 @@ public function &getData(): array public function clean(): void { $old = strtotime('-1 week'); - foreach (glob($this->dir . '/' . self::FilePrefix . '*') as $file) { + foreach (glob($this->dir . '/' . self::FilePrefix . '*') ?: [] as $file) { if (filemtime($file) < $old) { unlink($file); } From 7cdd66ee3c3b92c3d709462acb52074c3553b4f5 Mon Sep 17 00:00:00 2001 From: David Grudl- - -- - + + -%A%%A% \ No newline at end of file +%A%%A% From fba4ac5edd9ac2a0bbb3a0c75954eb403842b6cf Mon Sep 17 00:00:00 2001 From: David Grudl- - - - -- ---Code: %d%
--- - -%A%
-Headers were not sent at the time the exception was thrown
++ + ++ + + ++Code: %d%
+ ++++ %A% +
+Headers were not sent at the time the exception was thrown
+Date: Tue, 10 Feb 2026 18:41:07 +0100 Subject: [PATCH 06/18] uses nette/phpstan-rules --- composer.json | 9 ++++++++- phpstan.neon | 2 -- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 97d54a97f..0ff1cffad 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,9 @@ "nette/tester": "^2.6", "latte/latte": "^2.5 || ^3.0", "psr/log": "^1.0 || ^2.0 || ^3.0", - "phpstan/phpstan": "^2.0@stable" + "phpstan/phpstan": "^2.1@stable", + "phpstan/extension-installer": "^1.4@stable", + "nette/phpstan-rules": "^1.0" }, "conflict": { "nette/di": "<3.0" @@ -48,5 +50,10 @@ "branch-alias": { "dev-master": "2.11-dev" } + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + } } } diff --git a/phpstan.neon b/phpstan.neon index 94b109077..88a7a5995 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,8 +4,6 @@ parameters: paths: - src - checkMissingCallableSignature: true - ignoreErrors: # Template variables used in required .phtml files via variable scope - From 65931702a84e6dc5fc3bb09485477bc2b6c6dee6 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 20 Feb 2026 06:47:38 +0100 Subject: [PATCH 07/18] upgraded to PHPStan level 8 --- phpstan.neon | 58 +++++++++++++++++++--- src/Bridges/Nette/Bridge.php | 12 ++--- src/Bridges/Nette/TracyExtension.php | 3 +- src/Tracy/Bar/panels/info.panel.php | 2 +- src/Tracy/Debugger/Debugger.php | 4 +- src/Tracy/Debugger/DevelopmentStrategy.php | 2 +- src/Tracy/Dumper/Describer.php | 10 ++-- src/Tracy/Dumper/Dumper.php | 4 +- src/Tracy/Dumper/Exposer.php | 2 + src/Tracy/Dumper/Renderer.php | 19 +++---- src/Tracy/Helpers.php | 13 +++-- src/Tracy/Logger/Logger.php | 1 + src/Tracy/Session/FileSession.php | 6 +-- 13 files changed, 93 insertions(+), 43 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 88a7a5995..cdecbdc15 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,31 +1,51 @@ parameters: - level: 6 + level: 8 paths: - src + fileExtensions: + - php + - phtml + ignoreErrors: # Template variables used in required .phtml files via variable scope - identifier: closure.unusedUse path: src/Tracy/Bar/Bar.php + - + identifier: closure.unusedUse + path: src/Tracy/BlueScreen/dist/markdown.phtml # Tracy doesn't need generic type parameters for Fiber, ArrayObject, DOMNodeList, etc. - identifier: missingType.generics - # Private methods called from .phtml template files - - - identifier: method.unused - path: src/Tracy/BlueScreen/BlueScreen.php - # Runtime validation of callable-string and Closure types - identifier: function.alreadyNarrowedType + paths: + - src/Tracy/Bar/dist/loader.phtml + - src/Tracy/BlueScreen/BlueScreen.php + - src/Tracy/Helpers.php # Tracy uses dynamic properties on exceptions and panels - identifier: property.notFound + paths: + - src/Tracy/Bar/dist/info.panel.phtml + - src/Tracy/Bar/dist/info.tab.phtml + - src/Tracy/Bar/panels/info.panel.php + - src/Tracy/Debugger/DevelopmentStrategy.php + - src/Tracy/Helpers.php + + # Private methods called from .phtml template files + - + identifier: method.unused + path: src/Tracy/BlueScreen/BlueScreen.php + - + identifier: method.private + path: src/Tracy/BlueScreen/dist # PHPStan doesn't track reference assignments to snapshot array - @@ -72,3 +92,29 @@ parameters: - identifier: missingType.return path: src/Tracy/Logger/ILogger.php + + # Arrow function callback receives class names from get_declared_classes() etc. + - + identifier: argument.type + message: '#class\-string#' + path: src/Tracy/Bar/panels/info.panel.php + + # getPanel() returns ?IBarPanel but panel is always registered; dynamic props correct by design + - + identifier: property.nonObject + path: src/Tracy/Debugger/DevelopmentStrategy.php + + # Value::$id and $value are always non-null when used as array keys (snapshot/above maps) + - + identifier: offsetAccess.invalidOffset + paths: + - src/Tracy/Dumper/Describer.php + - src/Tracy/Dumper/Renderer.php + + # Generated phtml templates use is_bool() as a runtime type guard; PHPStan sees it as always-false for string-typed vars + - + identifier: function.impossibleType + paths: + - src/Tracy/Bar/dist + - src/Tracy/BlueScreen/dist + - src/Tracy/Debugger/dist diff --git a/src/Bridges/Nette/Bridge.php b/src/Bridges/Nette/Bridge.php index f5272479c..2ad596922 100644 --- a/src/Bridges/Nette/Bridge.php +++ b/src/Bridges/Nette/Bridge.php @@ -46,16 +46,12 @@ public static function renderMemberAccessException(?\Throwable $e): ?array $loc = Tracy\Debugger::mapSource($loc['file'], $loc['line']) ?? $loc; if (preg_match('#Cannot (?:read|write to) an undeclared property .+::\$(\w+), did you mean \$(\w+)\?#A', $e->getMessage(), $m)) { - return [ - 'link' => Helpers::editorUri($loc['file'], $loc['line'], 'fix', '->' . $m[1], '->' . $m[2]), - 'label' => 'fix it', - ]; + $link = Helpers::editorUri($loc['file'], $loc['line'], 'fix', '->' . $m[1], '->' . $m[2]); + return $link !== null ? ['link' => $link, 'label' => 'fix it'] : null; } elseif (preg_match('#Call to undefined (static )?method .+::(\w+)\(\), did you mean (\w+)\(\)?#A', $e->getMessage(), $m)) { $operator = $m[1] ? '::' : '->'; - return [ - 'link' => Helpers::editorUri($loc['file'], $loc['line'], 'fix', $operator . $m[2] . '(', $operator . $m[3] . '('), - 'label' => 'fix it', - ]; + $link = Helpers::editorUri($loc['file'], $loc['line'], 'fix', $operator . $m[2] . '(', $operator . $m[3] . '('); + return $link !== null ? ['link' => $link, 'label' => 'fix it'] : null; } return null; diff --git a/src/Bridges/Nette/TracyExtension.php b/src/Bridges/Nette/TracyExtension.php index c966812f2..2d3f19b7f 100644 --- a/src/Bridges/Nette/TracyExtension.php +++ b/src/Bridges/Nette/TracyExtension.php @@ -182,7 +182,8 @@ public function afterCompile(Nette\PhpGenerator\ClassType $class): void private function parseErrorSeverity(string|array $value): int { $value = implode('|', (array) $value); - $res = (int) @parse_ini_string('e = ' . $value)['e']; // @ may fail + $ini = @parse_ini_string('e = ' . $value); // @ may fail + $res = (int) ($ini['e'] ?? 0); if (!$res) { throw new Nette\InvalidStateException("Syntax error in expression '$value'"); } diff --git a/src/Tracy/Bar/panels/info.panel.php b/src/Tracy/Bar/panels/info.panel.php index 231ae79a5..c5ec8697f 100644 --- a/src/Tracy/Bar/panels/info.panel.php +++ b/src/Tracy/Bar/panels/info.panel.php @@ -11,7 +11,7 @@ /** @var DefaultBarPanel $this */ if (isset($this->cpuUsage) && $this->time) { - foreach (getrusage() as $key => $val) { + foreach (getrusage() ?: [] as $key => $val) { $this->cpuUsage[$key] -= $val; } $userUsage = -round(($this->cpuUsage['ru_utime.tv_sec'] * 1e6 + $this->cpuUsage['ru_utime.tv_usec']) / $this->time / 10000); diff --git a/src/Tracy/Debugger/Debugger.php b/src/Tracy/Debugger/Debugger.php index b70dad09a..9e8bde584 100644 --- a/src/Tracy/Debugger/Debugger.php +++ b/src/Tracy/Debugger/Debugger.php @@ -468,7 +468,7 @@ public static function getSessionStorage(): SessionStorage self::$sessionStorage = @is_dir($dir = (string) session_save_path()) || @is_dir($dir = (string) ini_get('upload_tmp_dir')) || @is_dir($dir = sys_get_temp_dir()) - || ($dir = self::$logDirectory) + || ($dir = (string) self::$logDirectory) ? new FileSession($dir) : new NativeSession; } @@ -598,7 +598,7 @@ public static function mapSource(string $file, int $line): ?array { foreach (self::$sourceMappers as $mapper) { if ($res = $mapper($file, $line)) { - return $res; + return $res + ['line' => 0, 'column' => 0, 'active' => true]; } } diff --git a/src/Tracy/Debugger/DevelopmentStrategy.php b/src/Tracy/Debugger/DevelopmentStrategy.php index 4fd4e37c9..319ecf289 100644 --- a/src/Tracy/Debugger/DevelopmentStrategy.php +++ b/src/Tracy/Debugger/DevelopmentStrategy.php @@ -59,7 +59,7 @@ private function renderExceptionCli(\Throwable $exception): void } if (Helpers::detectColors() && @is_file($exception->getFile())) { - echo "\n\n" . CodeHighlighter::highlightPhpCli(file_get_contents($exception->getFile()), $exception->getLine()) . "\n"; + echo "\n\n" . CodeHighlighter::highlightPhpCli((string) file_get_contents($exception->getFile()), $exception->getLine()) . "\n"; } echo "$exception\n" . ($logFile ? "\n(stored in $logFile)\n" : ''); diff --git a/src/Tracy/Dumper/Describer.php b/src/Tracy/Dumper/Describer.php index 1868fc948..b18d25d58 100644 --- a/src/Tracy/Dumper/Describer.php +++ b/src/Tracy/Dumper/Describer.php @@ -187,7 +187,7 @@ private function describeObject(object $obj, int $depth = 0): Value $rc = $obj instanceof \Closure ? new \ReflectionFunction($obj) : new \ReflectionClass($obj); - if ($rc->getFileName() && ($editor = Helpers::editorUri($rc->getFileName(), $rc->getStartLine()))) { + if ($rc->getFileName() && ($editor = Helpers::editorUri($rc->getFileName(), $rc->getStartLine() ?: null))) { $value->editor = (object) ['file' => $rc->getFileName(), 'line' => $rc->getStartLine(), 'url' => $editor]; } } @@ -196,7 +196,7 @@ private function describeObject(object $obj, int $depth = 0): Value $value->items = []; $props = $this->exposeObject($obj, $value); foreach ($props ?? [] as $k => $v) { - $this->addPropertyTo($value, (string) $k, $v, Value::PropertyVirtual, $this->getReferenceId($props, $k)); + $this->addPropertyTo($value, (string) $k, $v, Value::PropertyVirtual, $this->getReferenceId($props ?? [], $k)); } } @@ -252,11 +252,11 @@ public function addPropertyTo( ): void { if ($value->depth && $this->maxItems && count($value->items ?? []) >= $this->maxItems) { - $value->length = ($value->length ?? count($value->items)) + 1; + $value->length = ($value->length ?? count($value->items ?? [])) + 1; return; } - $class ??= $value->value; + $class ??= is_string($value->value) ? $value->value : null; $value->items[] = [ $this->describeKey($k), $type !== Value::PropertyVirtual && $this->isSensitive($k, $v, $class) @@ -307,7 +307,7 @@ private static function hideValue(mixed $val): string /** @param class-string $class */ public function describeEnumProperty(string $class, string $property, mixed $value): ?Value { - [$set, $constants] = $this->enumProperties["$class::$property"] ?? null; + [$set, $constants] = $this->enumProperties["$class::$property"] ?? [false, []]; if (!is_int($value) || !$constants || !($constants = Helpers::decomposeFlags($value, $set, $constants)) diff --git a/src/Tracy/Dumper/Dumper.php b/src/Tracy/Dumper/Dumper.php index 1f3f37da4..5a9d0014a 100644 --- a/src/Tracy/Dumper/Dumper.php +++ b/src/Tracy/Dumper/Dumper.php @@ -116,7 +116,7 @@ public static function dump(mixed $var, array $options = []): mixed if (Helpers::isCli()) { $useColors = self::$terminalColors && Helpers::detectColors(); $dumper = new self($options); - fwrite(STDOUT, $dumper->asTerminal($var, $useColors ? self::$terminalColors : [])); + fwrite(STDOUT, $dumper->asTerminal($var, $useColors ? self::$terminalColors ?? [] : [])); } elseif (Helpers::isHtmlMode()) { $options[self::LOCATION] ??= true; @@ -157,7 +157,7 @@ public static function toText(mixed $var, array $options = []): string */ public static function toTerminal(mixed $var, array $options = []): string { - return (new self($options))->asTerminal($var, self::$terminalColors); + return (new self($options))->asTerminal($var, self::$terminalColors ?? []); } diff --git a/src/Tracy/Dumper/Exposer.php b/src/Tracy/Dumper/Exposer.php index ceda7697d..ab1424766 100644 --- a/src/Tracy/Dumper/Exposer.php +++ b/src/Tracy/Dumper/Exposer.php @@ -217,6 +217,7 @@ public static function exposeSplObjectStorage(\SplObjectStorage $obj, Value $val $describer->addPropertyTo($pair, 'key', $v); $describer->addPropertyTo($pair, 'value', $obj[$v]); $describer->addPropertyTo($value, '', null, described: $pair); + assert($value->items !== null); $value->items[count($value->items) - 1][0] = ''; } } @@ -231,6 +232,7 @@ public static function exposeWeakMap(\WeakMap $obj, Value $value, Describer $des $describer->addPropertyTo($pair, 'key', $k); $describer->addPropertyTo($pair, 'value', $v); $describer->addPropertyTo($value, '', null, described: $pair); + assert($value->items !== null); $value->items[count($value->items) - 1][0] = ''; } } diff --git a/src/Tracy/Dumper/Renderer.php b/src/Tracy/Dumper/Renderer.php index 7564518d7..2c34d1463 100644 --- a/src/Tracy/Dumper/Renderer.php +++ b/src/Tracy/Dumper/Renderer.php @@ -145,7 +145,7 @@ private function renderString(string|Value $str, int $depth, string|int|null $ke $indent = ' ' . str_repeat('| ', $depth - 1) . ' '; return '' . "'" - . (is_string($str) ? Helpers::escapeHtml($str) : str_replace("\n", "\n" . $indent, $str->value)) + . (is_string($str) ? Helpers::escapeHtml($str) : str_replace("\n", "\n" . $indent, (string) $str->value)) . "'" . ''; @@ -164,7 +164,7 @@ private function renderString(string|Value $str, int $depth, string|int|null $ke . ($title ? 'tracy-dump-private' : $classes[$keyType]) . '"' . $title . '>' . (is_string($str) ? Helpers::escapeHtml($str) - : "'" . str_replace("\n", "\n" . $indent, $str->value) . "'") + : "'" . str_replace("\n", "\n" . $indent, (string) $str->value) . "'") . ''; } elseif (is_string($str)) { @@ -179,7 +179,7 @@ private function renderString(string|Value $str, int $depth, string|int|null $ke } else { $unit = $str->type === Value::TypeStringHtml ? 'characters' : 'bytes'; - $count = substr_count($str->value, "\n"); + $count = substr_count((string) $str->value, "\n"); if ($count) { $collapsed = $indent1 = $toggle = null; $indent = ' '; @@ -195,7 +195,7 @@ private function renderString(string|Value $str, int $depth, string|int|null $ke . '" title="' . $str->length . ' ' . $unit . '">' . $indent1 . ''" - . str_replace("\n", "\n" . $indent, $str->value) + . str_replace("\n", "\n" . $indent, (string) $str->value) . "'" . ($depth ? "\n" : '') . ' Date: Thu, 19 Feb 2026 18:50:37 +0100 Subject: [PATCH 08/18] moved isAjax() from Helpers to DeferredContent::isDeferred() --- src/Tracy/Bar/Bar.php | 2 +- src/Tracy/Debugger/DeferredContent.php | 13 +++++++++++-- src/Tracy/Debugger/DevelopmentStrategy.php | 4 ++-- src/Tracy/Helpers.php | 7 ------- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Tracy/Bar/Bar.php b/src/Tracy/Bar/Bar.php index ff65bdaa3..216af4195 100644 --- a/src/Tracy/Bar/Bar.php +++ b/src/Tracy/Bar/Bar.php @@ -73,7 +73,7 @@ public function render(DeferredContent $defer): void $redirectQueue = &$defer->getItems('redirect'); $requestId = $defer->getRequestId(); - if (Helpers::isAjax()) { + if ($defer->isDeferred()) { if ($defer->isAvailable()) { $defer->addSetup('Tracy.Debug.loadAjax', $this->renderPartial('ajax', '-ajax:' . $requestId)); } diff --git a/src/Tracy/Debugger/DeferredContent.php b/src/Tracy/Debugger/DeferredContent.php index 66a443ef3..2399ebd80 100644 --- a/src/Tracy/Debugger/DeferredContent.php +++ b/src/Tracy/Debugger/DeferredContent.php @@ -18,6 +18,7 @@ */ final class DeferredContent { + private readonly bool $deferred; private readonly string $requestId; private bool $useSession = false; @@ -25,7 +26,15 @@ final class DeferredContent public function __construct( private readonly SessionStorage $sessionStorage, ) { - $this->requestId = $_SERVER['HTTP_X_TRACY_AJAX'] ?? Helpers::createId(); + $ajax = $_SERVER['HTTP_X_TRACY_AJAX'] ?? ''; + $this->deferred = (bool) preg_match('#^\w{10,15}$#D', $ajax); + $this->requestId = $this->deferred ? $ajax : Helpers::createId(); + } + + + public function isDeferred(): bool + { + return $this->deferred; } @@ -103,7 +112,7 @@ public function sendAssets(): bool return true; } - if (Helpers::isAjax()) { + if ($this->deferred) { header('X-Tracy-Ajax: 1'); // session must be already locked } diff --git a/src/Tracy/Debugger/DevelopmentStrategy.php b/src/Tracy/Debugger/DevelopmentStrategy.php index 319ecf289..ec6a73c30 100644 --- a/src/Tracy/Debugger/DevelopmentStrategy.php +++ b/src/Tracy/Debugger/DevelopmentStrategy.php @@ -33,7 +33,7 @@ public function initialize(): void public function handleException(\Throwable $exception, bool $firstTime): void { - if (Helpers::isAjax() && $this->defer->isAvailable()) { + if ($this->defer->isDeferred() && $this->defer->isAvailable()) { $this->blueScreen->renderToAjax($exception, $this->defer); } elseif ($firstTime && Helpers::isHtmlMode()) { @@ -93,7 +93,7 @@ public function handleError( $message = Helpers::errorTypeToString($severity) . ': ' . Helpers::improveError($message); $count = &$this->bar->getPanel('Tracy:warnings')->data["$file|$line|$message"]; - if (!$count++ && !Helpers::isHtmlMode() && !Helpers::isAjax()) { + if (!$count++ && !Helpers::isHtmlMode() && !$this->defer->isDeferred()) { echo "\n$message in $file on line $line\n"; } diff --git a/src/Tracy/Helpers.php b/src/Tracy/Helpers.php index 1c59dea3f..be6bf27d9 100644 --- a/src/Tracy/Helpers.php +++ b/src/Tracy/Helpers.php @@ -310,13 +310,6 @@ public static function isHtmlMode(): bool } - /** @internal */ - public static function isAjax(): bool - { - return isset($_SERVER['HTTP_X_TRACY_AJAX']) && preg_match('#^\w{10,15}$#D', $_SERVER['HTTP_X_TRACY_AJAX']); - } - - /** @internal */ public static function isRedirect(): bool { From e936d12418c595c0197549c99b48d5eabc97e1ba Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 20 Feb 2026 04:02:41 +0100 Subject: [PATCH 09/18] moved environment guards from Debugger to strategies --- src/Tracy/Debugger/Debugger.php | 10 ++-------- src/Tracy/Debugger/DevelopmentStrategy.php | 14 ++++++++++++-- src/Tracy/Debugger/ProductionStrategy.php | 3 +-- tests/Tracy/Debugger.customCssFiles.phpt | 4 ++-- tests/Tracy/Debugger.customJsFiles.phpt | 4 ++-- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/Tracy/Debugger/Debugger.php b/src/Tracy/Debugger/Debugger.php index 9e8bde584..fd0f8fefc 100644 --- a/src/Tracy/Debugger/Debugger.php +++ b/src/Tracy/Debugger/Debugger.php @@ -254,13 +254,7 @@ public static function enable( public static function dispatch(): void { - if ( - !Helpers::isCli() - && self::getStrategy()->sendAssets() - ) { - self::$showBar = false; - exit; - } + self::getStrategy()->dispatch(); } @@ -300,7 +294,7 @@ public static function shutdownHandler(): void self::$reserved = null; - if (self::$showBar && !Helpers::isCli()) { + if (self::$showBar) { try { self::getStrategy()->renderBar(); } catch (\Throwable $e) { diff --git a/src/Tracy/Debugger/DevelopmentStrategy.php b/src/Tracy/Debugger/DevelopmentStrategy.php index ec6a73c30..4ffad8b63 100644 --- a/src/Tracy/Debugger/DevelopmentStrategy.php +++ b/src/Tracy/Debugger/DevelopmentStrategy.php @@ -18,6 +18,9 @@ */ final class DevelopmentStrategy { + private bool $assetsSent = false; + + public function __construct( private readonly Bar $bar, private readonly BlueScreen $blueScreen, @@ -103,9 +106,12 @@ public function handleError( } - public function sendAssets(): bool + public function dispatch(): void { - return $this->defer->sendAssets(); + if (!Helpers::isCli() && $this->defer->sendAssets()) { + $this->assetsSent = true; + exit; + } } @@ -117,6 +123,10 @@ public function renderLoader(): void public function renderBar(): void { + if ($this->assetsSent || Helpers::isCli()) { + return; + } + if (function_exists('ini_set')) { ini_set('display_errors', '1'); } diff --git a/src/Tracy/Debugger/ProductionStrategy.php b/src/Tracy/Debugger/ProductionStrategy.php index f097bd0c4..ba21dae97 100644 --- a/src/Tracy/Debugger/ProductionStrategy.php +++ b/src/Tracy/Debugger/ProductionStrategy.php @@ -69,9 +69,8 @@ public function handleError( } - public function sendAssets(): bool + public function dispatch(): void { - return false; } diff --git a/tests/Tracy/Debugger.customCssFiles.phpt b/tests/Tracy/Debugger.customCssFiles.phpt index 90cab7f00..34e7bede7 100644 --- a/tests/Tracy/Debugger.customCssFiles.phpt +++ b/tests/Tracy/Debugger.customCssFiles.phpt @@ -18,10 +18,10 @@ $output = ob_get_clean(); Assert::contains('custom-asset{}', $output); -$handler = Tracy\Debugger::getStrategy(); +$defer = new Tracy\DeferredContent(Tracy\Debugger::getSessionStorage()); ob_start(); $_GET['_tracy_bar'] = 'js'; -$handler->sendAssets(); +$defer->sendAssets(); $output = ob_get_clean(); Assert::contains('custom-asset{}', $output); diff --git a/tests/Tracy/Debugger.customJsFiles.phpt b/tests/Tracy/Debugger.customJsFiles.phpt index 26453dab5..ab956378b 100644 --- a/tests/Tracy/Debugger.customJsFiles.phpt +++ b/tests/Tracy/Debugger.customJsFiles.phpt @@ -9,10 +9,10 @@ require __DIR__ . '/../bootstrap.php'; Tracy\Debugger::$customJsFiles[] = __DIR__ . '/fixtures/custom.asset'; -$handler = Tracy\Debugger::getStrategy(); +$defer = new Tracy\DeferredContent(Tracy\Debugger::getSessionStorage()); ob_start(); $_GET['_tracy_bar'] = 'js'; -$handler->sendAssets(); +$defer->sendAssets(); $output = ob_get_clean(); Assert::contains('custom-asset {}', $output); From 5138fdf5fd9fdf94edd894ee9c1060835323580b Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 20 Feb 2026 04:02:41 +0100 Subject: [PATCH 10/18] moved $cpuUsage from Debugger to strategies --- src/Tracy/Debugger/Debugger.php | 8 +------- src/Tracy/Debugger/DevelopmentStrategy.php | 3 +++ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Tracy/Debugger/Debugger.php b/src/Tracy/Debugger/Debugger.php index fd0f8fefc..79312cacc 100644 --- a/src/Tracy/Debugger/Debugger.php +++ b/src/Tracy/Debugger/Debugger.php @@ -144,9 +144,6 @@ class Debugger /** @var array<\Closure(string, int): ?array{file: string, label: string, line?: int, column?: int, active?: bool}> */ private static array $sourceMappers = []; - /** @var ?array */ - private static ?array $cpuUsage = null; - /********************* services ****************d*g**/ private static BlueScreen $blueScreen; @@ -188,8 +185,6 @@ public static function enable( self::$reserved ??= str_repeat('t', self::$reservedMemorySize); self::$time ??= $_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(as_float: true); self::$obLevel ??= ob_get_level(); - self::$cpuUsage ??= !self::$productionMode && function_exists('getrusage') ? (getrusage() ?: null) : null; - // logging configuration self::$email = $email ?? self::$email; self::$logDirectory = $logDirectory ?? self::$logDirectory; @@ -404,8 +399,7 @@ public static function getBar(): Bar { if (empty(self::$bar)) { self::$bar = new Bar; - self::$bar->addPanel($info = new DefaultBarPanel('info'), 'Tracy:info'); - $info->cpuUsage = self::$cpuUsage; + self::$bar->addPanel(new DefaultBarPanel('info'), 'Tracy:info'); self::$bar->addPanel(new DefaultBarPanel('warnings'), 'Tracy:warnings'); // filled by errorHandler() } diff --git a/src/Tracy/Debugger/DevelopmentStrategy.php b/src/Tracy/Debugger/DevelopmentStrategy.php index 4ffad8b63..d5c497d22 100644 --- a/src/Tracy/Debugger/DevelopmentStrategy.php +++ b/src/Tracy/Debugger/DevelopmentStrategy.php @@ -31,6 +31,9 @@ public function __construct( public function initialize(): void { + $this->bar->getPanel('Tracy:info')->cpuUsage = function_exists('getrusage') + ? (getrusage() ?: null) + : null; } From 9b265515240a6768347525d463ef04207b4e7d98 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 20 Feb 2026 06:56:17 +0100 Subject: [PATCH 11/18] Released version 2.11.2 --- src/Tracy/Debugger/Debugger.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tracy/Debugger/Debugger.php b/src/Tracy/Debugger/Debugger.php index 79312cacc..080c10cd0 100644 --- a/src/Tracy/Debugger/Debugger.php +++ b/src/Tracy/Debugger/Debugger.php @@ -19,7 +19,7 @@ */ class Debugger { - public const Version = '2.11.1'; + public const Version = '2.11.2'; /** server modes for Debugger::enable() */ public const From 0e7ef4719051cacde7feb89ae19d697eafde63d9 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 10 Dec 2021 05:33:06 +0100 Subject: [PATCH 12/18] opened 3.0-dev --- composer.json | 2 +- src/Tracy/Debugger/Debugger.php | 34 +++++++++++++++++++++++---------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index 0ff1cffad..32b3d654c 100644 --- a/composer.json +++ b/composer.json @@ -48,7 +48,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.11-dev" + "dev-master": "3.0-dev" } }, "config": { diff --git a/src/Tracy/Debugger/Debugger.php b/src/Tracy/Debugger/Debugger.php index 080c10cd0..9886add79 100644 --- a/src/Tracy/Debugger/Debugger.php +++ b/src/Tracy/Debugger/Debugger.php @@ -19,7 +19,7 @@ */ class Debugger { - public const Version = '2.11.2'; + public const Version = '3.0-dev'; /** server modes for Debugger::enable() */ public const @@ -141,9 +141,12 @@ class Debugger /** @var string[] */ public static array $customJsFiles = []; - /** @var array<\Closure(string, int): ?array{file: string, label: string, line?: int, column?: int, active?: bool}> */ + /** @var array<\Closure(string, int): ?array{file: string, line: int, column?: int}> */ private static array $sourceMappers = []; + /** @var ?array */ + private static ?array $cpuUsage = null; + /********************* services ****************d*g**/ private static BlueScreen $blueScreen; @@ -185,6 +188,8 @@ public static function enable( self::$reserved ??= str_repeat('t', self::$reservedMemorySize); self::$time ??= $_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(as_float: true); self::$obLevel ??= ob_get_level(); + self::$cpuUsage ??= !self::$productionMode && function_exists('getrusage') ? (getrusage() ?: null) : null; + // logging configuration self::$email = $email ?? self::$email; self::$logDirectory = $logDirectory ?? self::$logDirectory; @@ -249,7 +254,13 @@ public static function enable( public static function dispatch(): void { - self::getStrategy()->dispatch(); + if ( + !Helpers::isCli() + && self::getStrategy()->sendAssets() + ) { + self::$showBar = false; + exit; + } } @@ -289,7 +300,7 @@ public static function shutdownHandler(): void self::$reserved = null; - if (self::$showBar) { + if (self::$showBar && !Helpers::isCli()) { try { self::getStrategy()->renderBar(); } catch (\Throwable $e) { @@ -309,7 +320,8 @@ public static function exceptionHandler(\Throwable $exception): void self::$reserved = null; self::$obStatus = ob_get_status(full_status: true); - @http_response_code(500); + @http_response_code(isset($_SERVER['HTTP_USER_AGENT']) && str_contains($_SERVER['HTTP_USER_AGENT'], 'MSIE ') ? 503 : 500); // may not have an effect + Helpers::improveException($exception); self::removeOutputBuffers(errorOccurred: true); @@ -327,6 +339,7 @@ public static function exceptionHandler(\Throwable $exception): void /** * Handler to catch warnings and notices. + * @return false * @throws ErrorException * @internal */ @@ -399,7 +412,8 @@ public static function getBar(): Bar { if (empty(self::$bar)) { self::$bar = new Bar; - self::$bar->addPanel(new DefaultBarPanel('info'), 'Tracy:info'); + self::$bar->addPanel($info = new DefaultBarPanel('info'), 'Tracy:info'); + $info->cpuUsage = self::$cpuUsage; self::$bar->addPanel(new DefaultBarPanel('warnings'), 'Tracy:warnings'); // filled by errorHandler() } @@ -456,7 +470,7 @@ public static function getSessionStorage(): SessionStorage self::$sessionStorage = @is_dir($dir = (string) session_save_path()) || @is_dir($dir = (string) ini_get('upload_tmp_dir')) || @is_dir($dir = sys_get_temp_dir()) - || ($dir = (string) self::$logDirectory) + || ($dir = self::$logDirectory) ? new FileSession($dir) : new NativeSession; } @@ -572,7 +586,7 @@ public static function tryLog(mixed $message, string $level = ILogger::INFO): ?\ /** - * @param callable(string, int): ?array{file: string, label: string, line?: int, column?: int, active?: bool} $mapper + * @param callable(string, int): ?array{file: string, line: int, column?: int} $mapper * @internal */ public static function addSourceMapper(callable $mapper): void @@ -581,12 +595,12 @@ public static function addSourceMapper(callable $mapper): void } - /** @return ?array{file: string, label: string, line: int, column: int, active: bool} */ + /** @return ?array{file: string, line: int, column?: int} */ public static function mapSource(string $file, int $line): ?array { foreach (self::$sourceMappers as $mapper) { if ($res = $mapper($file, $line)) { - return $res + ['line' => 0, 'column' => 0, 'active' => true]; + return $res; } } From 61d1330cf495c2140a6ef1eb5dc11c2ff4717600 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 24 Nov 2025 03:13:40 +0100 Subject: [PATCH 13/18] open in editor: added support for columns --- src/Tracy/BlueScreen/BlueScreen.php | 2 +- src/Tracy/Helpers.php | 3 ++- tools/open-in-editor/linux/open-editor.sh | 14 +++++++++----- tools/open-in-editor/windows/open-editor.js | 15 ++++++++------- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/Tracy/BlueScreen/BlueScreen.php b/src/Tracy/BlueScreen/BlueScreen.php index dad9683d9..fdbb93cb0 100644 --- a/src/Tracy/BlueScreen/BlueScreen.php +++ b/src/Tracy/BlueScreen/BlueScreen.php @@ -336,7 +336,7 @@ public static function highlightFile( ? CodeHighlighter::highlightPhp($source, $line, $column) : ' '; - if ($editor = Helpers::editorUri($file, $line)) { + if ($editor = Helpers::editorUri($file, line: $line, column: $column)) { $source = substr_replace($source, ' title="Ctrl-Click to open in editor" data-tracy-href="' . Helpers::escapeHtml($editor) . '"', 4, 0); } diff --git a/src/Tracy/Helpers.php b/src/Tracy/Helpers.php index be6bf27d9..2d8090ae0 100644 --- a/src/Tracy/Helpers.php +++ b/src/Tracy/Helpers.php @@ -57,6 +57,7 @@ public static function editorUri( string $action = 'open', string $search = '', string $replace = '', + ?int $column = null, ): ?string { if (Debugger::$editor && $file && ($action === 'create' || @is_file($file))) { // @ - may trigger error @@ -67,7 +68,7 @@ public static function editorUri( return strtr(Debugger::$editor, [ '%action' => $action, '%file' => rawurlencode($file), - '%line' => $line ?: 1, + '%line' => ($line ?: 1) . ($column ? ':' . $column : ''), '%search' => rawurlencode($search), '%replace' => rawurlencode($replace), ]); diff --git a/tools/open-in-editor/linux/open-editor.sh b/tools/open-in-editor/linux/open-editor.sh index 69e37be83..8cabf2789 100755 --- a/tools/open-in-editor/linux/open-editor.sh +++ b/tools/open-in-editor/linux/open-editor.sh @@ -6,7 +6,7 @@ declare -A mapping # # Visual Studio Code -#editor='code --goto "$FILE":"$LINE"' +#editor='code --goto "$FILE":"$LINE":"$COLUMN"' # Emacs #editor='emacs +$LINE "$FILE"' # gVim @@ -17,11 +17,9 @@ declare -A mapping #editor='pluma +$LINE "$FILE"' # PHPStorm # To enable PHPStorm command-line interface, folow this guide: https://www.jetbrains.com/help/phpstorm/working-with-the-ide-features-from-command-line.html -#editor='phpstorm --line $LINE "$FILE"' +#editor='phpstorm --line $LINE --column $COLUMN "$FILE"' # VS Codium #editor='codium --goto "$FILE":"$LINE"' -# Visual Studio Code -#editor='code --goto "$FILE":"$LINE"' # # Optionally configure custom mapping here: @@ -56,16 +54,21 @@ action=`echo $url | sed -r "s/$regex/\1/i"` uri_params=`echo $url | sed -r "s/$regex/\2/i"` file=`get_param $uri_params "file"` -line=`get_param $uri_params "line"` +line_param=`get_param $uri_params "line"` search=`get_param $uri_params "search"` replace=`get_param $uri_params "replace"` +# Parse line and column from line parameter (format: "12:5" or just "12") +IFS=':' read -r line column <<< "$line_param" +column="${column:-1}" + # Debug? #echo "action '$action'" #echo "file '$file'" #echo "line '$line'" #echo "search '$search'" #echo "replace '$replace'" +#echo "column '$column'" # Convert URI encoded codes to normal characters (e.g. '%2F' => '/'). printf -v file "${file//%/\\x}" @@ -102,6 +105,7 @@ fi # Format the command according to the selected editor. command="${editor//\$FILE/$file}" command="${command//\$LINE/$line}" +command="${command//\$COLUMN/$column}" # Debug? #echo $command diff --git a/tools/open-in-editor/windows/open-editor.js b/tools/open-in-editor/windows/open-editor.js index ac1093e70..86f699483 100644 --- a/tools/open-in-editor/windows/open-editor.js +++ b/tools/open-in-editor/windows/open-editor.js @@ -1,7 +1,7 @@ var settings = { // PhpStorm - // editor: '"C:\\Program Files\\JetBrains\\PhpStorm 2018.1.2\\bin\\phpstorm64.exe" --line %line% "%file%"', + // editor: '"C:\\Program Files\\JetBrains\\PhpStorm 2018.1.2\\bin\\phpstorm64.exe" --line %line% --column %column% "%file%"', // title: 'PhpStorm', // NetBeans @@ -14,7 +14,7 @@ var settings = { // editor: '"C:\\Program Files\\SciTE\\scite.exe" "-open:%file%" -goto:%line%', // EmEditor - // editor: '"C:\\Program Files\\EmEditor\\EmEditor.exe" "%file%" /l %line%', + // editor: '"C:\\Program Files\\EmEditor\\EmEditor.exe" "%file%" /l %line% /cl %column%', // PSPad Editor // editor: '"C:\\Program Files\\PSPad editor\\PSPad.exe" -%line% "%file%"', @@ -26,7 +26,7 @@ var settings = { // editor: '"C:\\Program Files\\Sublime Text 2\\sublime_text.exe" "%file%:%line%"', // Visual Studio Code / VSCodium - // editor: '"C:\\Program Files\\Microsoft VS Code\\Code.exe" --goto "%file%:%line%"', + // editor: '"C:\\Program Files\\Microsoft VS Code\\Code.exe" --goto "%file%:%line%:%column%"', mappings: { // '/remotepath': '/localpath' @@ -41,7 +41,7 @@ if (!settings.editor) { } var url = WScript.Arguments(0); -var match = /^editor:\/\/(open|create|fix)\/?\?file=([^&]+)&line=(\d+)(?:&search=([^&]*)&replace=([^&]*))?/.exec(url); +var match = /^editor:\/\/(open|create|fix)\/?\?file=([^&]+)&line=(\d+)(?::(\d+))?(?:&search=([^&]*)&replace=([^&]*))?/.exec(url); if (!match) { WScript.Echo('Unexpected URI ' + url); WScript.Quit(); @@ -53,8 +53,9 @@ for (var i in match) { var action = match[1]; var file = match[2]; var line = match[3]; -var search = match[4]; -var replace = match[5]; +var column = match[4] || 1; +var search = match[5]; +var replace = match[6]; var shell = new ActiveXObject('WScript.Shell'); var fileSystem = new ActiveXObject('Scripting.FileSystemObject'); @@ -76,7 +77,7 @@ if (action === 'create' && !fileSystem.FileExists(file)) { fileSystem.OpenTextFile(file, 2).Write(lines.join('\n')); } -var command = settings.editor.replace(/%line%/, line).replace(/%file%/, file); +var command = settings.editor.replace(/%line%/, line).replace(/%column%/, column).replace(/%file%/, file); shell.Exec(command); if (settings.title) { From 31295a62ea16f21255192c4ed3276c179877db21 Mon Sep 17 00:00:00 2001 From: David Grudl' . CodeHighlighter::highlightLine(htmlspecialchars($source, ENT_IGNORE, 'UTF-8'), $line, $column) . 'Date: Mon, 5 Dec 2022 01:21:34 +0100 Subject: [PATCH 14/18] uses PascalCase constants --- src/Bridges/Nette/TracyExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bridges/Nette/TracyExtension.php b/src/Bridges/Nette/TracyExtension.php index 2d3f19b7f..9f71bffb4 100644 --- a/src/Bridges/Nette/TracyExtension.php +++ b/src/Bridges/Nette/TracyExtension.php @@ -138,7 +138,7 @@ public function afterCompile(Nette\PhpGenerator\ClassType $class): void if ($this->debugMode) { foreach ($config->bar as $item) { if (is_string($item) && str_starts_with($item, '@')) { - $item = new Statement(['@' . $builder::THIS_CONTAINER, 'getService'], [substr($item, 1)]); + $item = new Statement(['@' . $builder::ThisContainer, 'getService'], [substr($item, 1)]); } elseif (is_string($item)) { $item = new Statement($item); } From fcea7245b57f615ac5fa11f36e1e1e11ad4b4842 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 3 Feb 2022 17:37:54 +0100 Subject: [PATCH 15/18] Logger: added typehints WIP --- src/Bridges/Nette/TracyExtension.php | 2 +- src/Tracy/Logger/Logger.php | 47 ++++++++++------------------ 2 files changed, 17 insertions(+), 32 deletions(-) diff --git a/src/Bridges/Nette/TracyExtension.php b/src/Bridges/Nette/TracyExtension.php index 9f71bffb4..1342bbd50 100644 --- a/src/Bridges/Nette/TracyExtension.php +++ b/src/Bridges/Nette/TracyExtension.php @@ -131,7 +131,7 @@ public function afterCompile(Nette\PhpGenerator\ClassType $class): void } $initialize->addBody($builder->formatPhp('if ($logger instanceof Tracy\Logger) $logger->mailer = ?;', [ - [new Statement(Tracy\Bridges\Nette\MailSender::class, $params), 'send'], + [new Statement(Tracy\Bridges\Nette\MailSender::class, $params), 'send'], // TODO: nette/di must be able to create closures ])); } diff --git a/src/Tracy/Logger/Logger.php b/src/Tracy/Logger/Logger.php index c17e02760..bdd027c5f 100644 --- a/src/Tracy/Logger/Logger.php +++ b/src/Tracy/Logger/Logger.php @@ -18,23 +18,22 @@ */ class Logger implements ILogger { - /** @var ?string name of the directory where errors should be logged */ - public $directory; + /** name of the directory where errors should be logged */ + public ?string $directory = null; - /** @var string|string[]|null email or emails to which send error notifications */ - public $email; + /** @var string|string[]|null email or emails to which send error notifications */ + public string|array|null $email = null; - /** @var ?string sender of email notifications */ - public $fromEmail; + /** sender of email notifications */ + public ?string $fromEmail = null; - /** @var string|int interval for sending email is 2 days */ - public $emailSnooze = '2 days'; + /** interval for sending email is 2 days */ + public string|int $emailSnooze = '2 days'; - /** @var callable(mixed $message, string $email): void handler for sending emails */ - public $mailer; + /** @var \Closure(mixed $message, string $email): void handler for sending emails */ + public ?\Closure $mailer = null; - /** @var ?BlueScreen */ - private $blueScreen; + private ?BlueScreen $blueScreen = null; /** @@ -54,7 +53,7 @@ public function __construct(?string $directory, string|array|null $email = null, * For levels ERROR, EXCEPTION and CRITICAL it sends email. * @return ?string logged error filename */ - public function log(mixed $message, string $level = self::INFO) + public function log(mixed $message, string $level = self::INFO): ?string { if (!$this->directory) { throw new \LogicException('Logging directory is not specified.'); @@ -84,10 +83,7 @@ public function log(mixed $message, string $level = self::INFO) } - /** - * @param mixed $message - */ - public static function formatMessage($message): string + public static function formatMessage(mixed $message): string { if ($message instanceof \Throwable) { $tmp = []; @@ -108,10 +104,7 @@ public static function formatMessage($message): string } - /** - * @param mixed $message - */ - public static function formatLogLine($message, ?string $exceptionFile = null): string + public static function formatLogLine(mixed $message, ?string $exceptionFile = null): string { return implode(' ', [ date('[Y-m-d H-i-s]'), @@ -161,10 +154,7 @@ protected function logException(\Throwable $exception, ?string $file = null): st } - /** - * @param mixed $message - */ - protected function sendEmail($message): void + protected function sendEmail(mixed $message): void { $snooze = is_numeric($this->emailSnooze) ? $this->emailSnooze @@ -181,12 +171,7 @@ protected function sendEmail($message): void } - /** - * Default mailer. - * @param mixed $message - * @internal - */ - public function defaultMailer($message, string $email): void + private function defaultMailer(mixed $message, string $email): void { $host = preg_replace('#[^\w.-]+#', '', $_SERVER['SERVER_NAME'] ?? php_uname('n')); mail( From c9bb746af50bbbfcc91cc64b1287e3ab758b92e8 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 3 Apr 2024 20:38:58 +0200 Subject: [PATCH 16/18] error.log changed to warning.log --- src/Tracy/Debugger/ProductionStrategy.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tracy/Debugger/ProductionStrategy.php b/src/Tracy/Debugger/ProductionStrategy.php index ba21dae97..81d9141ec 100644 --- a/src/Tracy/Debugger/ProductionStrategy.php +++ b/src/Tracy/Debugger/ProductionStrategy.php @@ -65,7 +65,7 @@ public function handleError( $err = 'PHP ' . Helpers::errorTypeToString($severity) . ': ' . Helpers::improveError($message) . " in $file:$line"; } - Debugger::tryLog($err, Debugger::ERROR); + Debugger::tryLog($err, Debugger::WARNING); } From 785fbfa0276b5c164c52e92e5833fc660c89e448 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 23 Nov 2024 17:55:20 +0100 Subject: [PATCH 17/18] used attribute Deprecated --- src/Tracy/Debugger/Debugger.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Tracy/Debugger/Debugger.php b/src/Tracy/Debugger/Debugger.php index 9886add79..e4fa67cef 100644 --- a/src/Tracy/Debugger/Debugger.php +++ b/src/Tracy/Debugger/Debugger.php @@ -29,19 +29,19 @@ class Debugger public const CookieSecret = 'tracy-debug'; - /** @deprecated use Debugger::Version */ + #[\Deprecated('use Debugger::Version')] public const VERSION = self::Version; - /** @deprecated use Debugger::Development */ + #[\Deprecated('use Debugger::Development')] public const DEVELOPMENT = self::Development; - /** @deprecated use Debugger::Production */ + #[\Deprecated('use Debugger::Production')] public const PRODUCTION = self::Production; - /** @deprecated use Debugger::Detect */ + #[\Deprecated('use Debugger::Detect')] public const DETECT = self::Detect; - /** @deprecated use Debugger::CookieSecret */ + #[\Deprecated('use Debugger::CookieSecret')] public const COOKIE_SECRET = self::CookieSecret; /** in production mode is suppressed any debugging output */ From 44bb6fcdba0f682fbbc21391a765ad2e55d50db0 Mon Sep 17 00:00:00 2001 From: Jakub Duchek Date: Thu, 5 Mar 2026 16:00:14 +0100 Subject: [PATCH 18/18] feat: Add lazy panel loading support (#530) Panels registered with `lazy: true` have their getPanel() deferred to a shutdown function. The tab renders immediately, but panel content is stored in session and fetched via AJAX on first interaction. This avoids expensive panel rendering blocking the page response. Usage: Debugger::getBar()->addPanel($panel, 'id', lazy: true); --- examples/lazy-panels.php | 143 +++++++++++++++++++++ src/Tracy/Bar/Bar.php | 76 ++++++++++- src/Tracy/Bar/assets/bar.js | 53 +++++++- src/Tracy/Bar/dist/bar.phtml | 2 +- src/Tracy/Bar/dist/panels.phtml | 2 +- src/Tracy/Debugger/DeferredContent.php | 15 +++ src/Tracy/Debugger/DevelopmentStrategy.php | 7 +- src/Tracy/Debugger/ProductionStrategy.php | 3 +- 8 files changed, 289 insertions(+), 12 deletions(-) create mode 100644 examples/lazy-panels.php diff --git a/examples/lazy-panels.php b/examples/lazy-panels.php new file mode 100644 index 000000000..8d0f8398e --- /dev/null +++ b/examples/lazy-panels.php @@ -0,0 +1,143 @@ +⚡ Normal'; + } + + public function getPanel(): string + { + return ' Normal Panel
' + . '' + . ''; + } +} + + +/** + * Example: A "heavy" panel that simulates expensive computation. + * When registered with lazy: true, getPanel() is NOT called during the request. + * Instead, it is rendered in the shutdown function and served via AJAX on click. + */ +class HeavyPanel implements IBarPanel +{ + public function getTab(): string + { + return '🐢 Heavy'; + } + + public function getPanel(): string + { + // Simulate expensive operation (e.g., database profiling, API calls) + usleep(500_000); // 500ms delay + + return 'This panel was rendered during the request (eager).
' + . 'Time: ' . date('H:i:s') . '
' + . 'Heavy Panel (lazy loaded)
' + . '' + . ''; + } +} + + +/** + * Example: Another lazy panel showing database-like profiling info. + */ +class DatabasePanel implements IBarPanel +{ + public function getTab(): string + { + return '🗄️ DB'; + } + + public function getPanel(): string + { + usleep(300_000); // 300ms delay + + $queries = [ + ['SELECT * FROM users WHERE id = 1', '0.5ms'], + ['SELECT * FROM posts WHERE user_id = 1 ORDER BY created_at DESC LIMIT 10', '2.1ms'], + ['UPDATE users SET last_login = NOW() WHERE id = 1', '0.3ms'], + ]; + + $html = 'This panel was rendered after the response (lazy).
' + . 'It simulates a 500ms expensive computation.
' + . 'Time: ' . date('H:i:s') . '
' + . '' + . '
' + . ' Key Value ' + . ' PHP Version ' . PHP_VERSION . ' ' + . ' Memory Peak ' . number_format(memory_get_peak_usage() / 1024 / 1024, 2) . ' MB ' + . ' Extensions ' . count(get_loaded_extensions()) . ' loaded Database Panel (lazy loaded)
' + . '' + . ''; + return $html; + } +} + + +// Register panels: +// Normal panel (eager) — rendered during the request +Debugger::getBar()->addPanel(new NormalPanel, 'example-normal'); + +// Heavy panel — lazy: true means getPanel() is deferred to shutdown function +Debugger::getBar()->addPanel(new HeavyPanel, 'example-heavy', lazy: true); + +// Database panel — also lazy +Debugger::getBar()->addPanel(new DatabasePanel, 'example-database', lazy: true); + +?> + + +Simulated database queries — rendered lazily after the response was sent.
' + . '
'; + + foreach ($queries as $i => [$query, $time]) { + $html .= ' # Query Time '; + } + + $html .= ' ' . ($i + 1) . ' ' . htmlspecialchars($query) . '' . $time . ' Tracy: Lazy Panel Loading Demo
+ +How it works
+This demo shows the
+ +lazy: trueparameter forDebugger::getBar()->addPanel().+
+ +- ⚡ Normal — A regular panel. Its
+getPanel()is called during the request.- 🐢 Heavy — A lazy panel simulating a 500ms expensive operation. Content loads on click.
+- 🗄️ DB — A lazy panel simulating database query profiling. Content loads on click.
+Usage
++ +// Register a lazy panel — getPanel() is NOT called during the request +Debugger::getBar()->addPanel(new MyExpensivePanel, 'my-panel', lazy: true); +Lazy panels have their
+ +getTab()called normally (so the tab is always visible), +butgetPanel()is deferred to a shutdown function. The content is stored in the session +and fetched via AJAX when you click or hover over the panel tab.This is useful for panels that perform expensive operations like database profiling, +API call logging, or heavy data analysis — they won't slow down your page response time.
+ +For security reasons, Tracy is visible only on localhost. Look into the source code to see how to enable Tracy.'; +} diff --git a/src/Tracy/Bar/Bar.php b/src/Tracy/Bar/Bar.php index 216af4195..c47a957a4 100644 --- a/src/Tracy/Bar/Bar.php +++ b/src/Tracy/Bar/Bar.php @@ -19,13 +19,19 @@ class Bar { /** @var IBarPanel[] */ private array $panels = []; + + /** @var arraypanel ID => lazy flag */ + private array $lazyPanels = []; private bool $loaderRendered = false; /** * Add custom panel. + * @param bool $lazy If true, panel content is rendered after the response is sent + * and loaded via AJAX when the user clicks on the tab. + * Use for panels whose getPanel() is expensive and not needed on every request. */ - public function addPanel(IBarPanel $panel, ?string $id = null): static + public function addPanel(IBarPanel $panel, ?string $id = null, bool $lazy = false): static { if ($id === null) { $c = 0; @@ -35,6 +41,10 @@ public function addPanel(IBarPanel $panel, ?string $id = null): static } $this->panels[$id] = $panel; + if ($lazy) { + $this->lazyPanels[$id] = true; + } + return $this; } @@ -141,9 +151,14 @@ private function renderPanels(string $suffix = ''): array foreach ($this->panels as $id => $panel) { $idHtml = preg_replace('#[^a-z0-9]+#i', '-', $id) . $suffix; + $lazy = isset($this->lazyPanels[$id]); try { $tab = (string) $panel->getTab(); - $panelHtml = $tab ? $panel->getPanel() : null; + if ($lazy && $tab) { + $panelHtml = null; // will be rendered later via shutdown function + } else { + $panelHtml = $tab ? $panel->getPanel() : null; + } } catch (\Throwable $e) { while (ob_get_level() > $obLevel) { // restore ob-level if broken @@ -153,13 +168,68 @@ private function renderPanels(string $suffix = ''): array $idHtml = "error-$idHtml"; $tab = "Error in $id"; $panelHtml = " Error: $id
" . nl2br(Helpers::escapeHtml($e)) . ''; + $lazy = false; unset($e); } - $panels[] = (object) ['id' => $idHtml, 'tab' => $tab, 'panel' => $panelHtml]; + $panels[] = (object) ['id' => $idHtml, 'tab' => $tab, 'panel' => $panelHtml, 'lazy' => $lazy]; } restore_error_handler(); return $panels; } + + + /** + * Renders lazy panels in shutdown function and stores them in session. + * @internal + */ + public function renderLazyPanels(DeferredContent $defer): void + { + if (!$defer->isAvailable()) { + return; + } + + set_error_handler(function (int $severity, string $message, string $file, int $line): bool { + if (error_reporting() & $severity) { + throw new \ErrorException($message, 0, $severity, $file, $line); + } + + return true; + }); + + $obLevel = ob_get_level(); + + foreach ($this->panels as $id => $panel) { + if (!isset($this->lazyPanels[$id])) { + continue; + } + + try { + $tab = (string) $panel->getTab(); + $panelHtml = $tab ? $panel->getPanel() : null; + } catch (\Throwable $e) { + while (ob_get_level() > $obLevel) { + ob_end_clean(); + } + + $panelHtml = "Error: $id
" . nl2br(Helpers::escapeHtml($e)) . ''; + unset($e); + } + + if ($panelHtml !== null) { + $icons = ''; + $lazyItems = &$defer->getItems('lazy-panels'); + $lazyItems[$defer->getRequestId() . '.' . preg_replace('#[^a-z0-9]+#i', '-', $id)] = [ + 'content' => $panelHtml . "\n" . $icons, + 'time' => time(), + ]; + } + } + + restore_error_handler(); + } } diff --git a/src/Tracy/Bar/assets/bar.js b/src/Tracy/Bar/assets/bar.js index 56f237958..41d1b7de8 100644 --- a/src/Tracy/Bar/assets/bar.js +++ b/src/Tracy/Bar/assets/bar.js @@ -31,10 +31,16 @@ class Panel { let elem = this.elem; this.init = function () {}; - elem.innerHTML = elem.tracyContent = elem.dataset.tracyContent; - delete elem.dataset.tracyContent; - Tracy.Dumper.init(Debug.layer); - evalScripts(elem); + + if (elem.dataset.tracyLazy && !elem.dataset.tracyContent) { + elem.innerHTML = elem.tracyContent = 'Loading\u2026
'; + this.fetchLazyContent(); + } else { + elem.innerHTML = elem.tracyContent = elem.dataset.tracyContent; + delete elem.dataset.tracyContent; + Tracy.Dumper.init(Debug.layer); + evalScripts(elem); + } draggable(elem, { handles: elem.querySelectorAll('h1'), @@ -87,6 +93,45 @@ class Panel { } + fetchLazyContent() { + let elem = this.elem; + let panelId = elem.id.replace('tracy-debug-panel-', ''); + let url = baseUrl + '_tracy_bar=lazy-panel.' + requestId + '.' + panelId + '&XDEBUG_SESSION_STOP=1&v=' + Math.random(); + + fetch(url) + .then((response) => response.json()) + .then((data) => { + if (data.content) { + elem.innerHTML = elem.tracyContent = data.content; + delete elem.dataset.tracyLazy; + Tracy.Dumper.init(Debug.layer); + evalScripts(elem); + + elem.querySelectorAll('.tracy-icons a').forEach((link) => { + link.addEventListener('click', (e) => { + if (link.dataset.tracyAction === 'close') { + this.toPeek(); + } else if (link.dataset.tracyAction === 'window') { + this.toWindow(); + } + e.preventDefault(); + e.stopImmediatePropagation(); + }); + }); + + if (this.is('tracy-panel-persist')) { + Tracy.Toggle.persist(elem); + } + } else { + elem.innerHTML = elem.tracyContent = 'Loading panel content\u2026
Error
'; + } + }) + .catch(() => { + elem.innerHTML = elem.tracyContent = 'Lazy panel content not available. The panel may have expired from the session.
Error
'; + }); + } + + is(mode) { return this.elem.classList.contains(mode); } diff --git a/src/Tracy/Bar/dist/bar.phtml b/src/Tracy/Bar/dist/bar.phtml index dc27044b5..db0f23bf1 100644 --- a/src/Tracy/Bar/dist/bar.phtml +++ b/src/Tracy/Bar/dist/bar.phtml @@ -17,7 +17,7 @@ https://tracy.nette.org">Failed to load lazy panel content.
AJAX -tab): ?>panel): ?> +tab): ?> panel || ($panel->lazy ?? false)): ?> = trim($panel->tab) ?> diff --git a/src/Tracy/Bar/dist/panels.phtml b/src/Tracy/Bar/dist/panels.phtml index e9b62187b..0e06718d2 100644 --- a/src/Tracy/Bar/dist/panels.phtml +++ b/src/Tracy/Bar/dist/panels.phtml @@ -12,7 +12,7 @@ declare(strict_types=1); ' ?> panel ? $panel->panel . "\n" . $icons : '' ?>+"lazy ?? false): ?> data-tracy-lazy="1" data-tracy-content='= str_replace(['&', "'"], ['&', '''], $content) ?> '>> diff --git a/src/Tracy/Debugger/DeferredContent.php b/src/Tracy/Debugger/DeferredContent.php index 2399ebd80..8a8d0c2df 100644 --- a/src/Tracy/Debugger/DeferredContent.php +++ b/src/Tracy/Debugger/DeferredContent.php @@ -112,6 +112,21 @@ public function sendAssets(): bool return true; } + if (is_string($asset) && preg_match('#^lazy-panel\.([\w.+-]+)$#', $asset, $m)) { + $key = $m[1]; + header('Content-Type: application/json; charset=UTF-8'); + header('Cache-Control: no-cache'); + header_remove('Set-Cookie'); + $lazyItems = &$this->getItems('lazy-panels'); + $content = $lazyItems[$key]['content'] ?? null; + unset($lazyItems[$key]); + $str = json_encode(['content' => $content], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE); + header('Content-Length: ' . strlen($str)); + echo $str; + flush(); + return true; + } + if ($this->deferred) { header('X-Tracy-Ajax: 1'); // session must be already locked } diff --git a/src/Tracy/Debugger/DevelopmentStrategy.php b/src/Tracy/Debugger/DevelopmentStrategy.php index d5c497d22..036a2f827 100644 --- a/src/Tracy/Debugger/DevelopmentStrategy.php +++ b/src/Tracy/Debugger/DevelopmentStrategy.php @@ -109,12 +109,14 @@ public function handleError( } - public function dispatch(): void + public function sendAssets(): bool { if (!Helpers::isCli() && $this->defer->sendAssets()) { $this->assetsSent = true; - exit; + return true; } + + return false; } @@ -135,5 +137,6 @@ public function renderBar(): void } $this->bar->render($this->defer); + $this->bar->renderLazyPanels($this->defer); } } diff --git a/src/Tracy/Debugger/ProductionStrategy.php b/src/Tracy/Debugger/ProductionStrategy.php index 81d9141ec..aadf9520d 100644 --- a/src/Tracy/Debugger/ProductionStrategy.php +++ b/src/Tracy/Debugger/ProductionStrategy.php @@ -69,8 +69,9 @@ public function handleError( } - public function dispatch(): void + public function sendAssets(): bool { + return false; }