Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
.gitignore export-ignore
ncs.* export-ignore
phpstan*.neon export-ignore
src/**/*.latte export-ignore
tests/ export-ignore

*.php* diff=php
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/coding-style.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
coverage: none

- run: composer create-project nette/code-checker temp/code-checker ^3 --no-progress
- run: php temp/code-checker/code-checker --strict-types --no-progress -i tests/Tracy/fixtures -i examples/assets
- run: php temp/code-checker/code-checker --strict-types --no-progress -i tests/Tracy/fixtures -i examples/assets -i *.latte


nette_cs:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
code_coverage:
name: Code Coverage
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
Expand Down
11 changes: 9 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -46,7 +48,12 @@
},
"extra": {
"branch-alias": {
"dev-master": "2.11-dev"
"dev-master": "3.0-dev"
}
},
"config": {
"allow-plugins": {
"phpstan/extension-installer": true
}
}
}
143 changes: 143 additions & 0 deletions examples/lazy-panels.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php

declare(strict_types=1);

require __DIR__ . '/../src/tracy.php';

use Tracy\Debugger;
use Tracy\IBarPanel;

// For security reasons, Tracy is visible only on localhost.
// You may force Tracy to run in development mode by passing the Debugger::Development instead of Debugger::Detect.
Debugger::enable(Debugger::Detect, __DIR__ . '/log');


/**
* Example: A normal (eager) panel — getPanel() is called during the request.
*/
class NormalPanel implements IBarPanel
{
public function getTab(): string
{
return '<span title="Normal Panel">⚡ Normal</span>';
}

public function getPanel(): string
{
return '<h1>Normal Panel</h1>'
. '<div class="tracy-inner">'
. '<p>This panel was rendered <strong>during the request</strong> (eager).</p>'
. '<p>Time: ' . date('H:i:s') . '</p>'
. '</div>';
}
}


/**
* 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 '<span title="Heavy Panel (lazy)">🐢 Heavy</span>';
}

public function getPanel(): string
{
// Simulate expensive operation (e.g., database profiling, API calls)
usleep(500_000); // 500ms delay

return '<h1>Heavy Panel (lazy loaded)</h1>'
. '<div class="tracy-inner">'
. '<p>This panel was rendered <strong>after the response</strong> (lazy).</p>'
. '<p>It simulates a 500ms expensive computation.</p>'
. '<p>Time: ' . date('H:i:s') . '</p>'
. '<table><tr><th>Key</th><th>Value</th></tr>'
. '<tr><td>PHP Version</td><td>' . PHP_VERSION . '</td></tr>'
. '<tr><td>Memory Peak</td><td>' . number_format(memory_get_peak_usage() / 1024 / 1024, 2) . ' MB</td></tr>'
. '<tr><td>Extensions</td><td>' . count(get_loaded_extensions()) . ' loaded</td></tr>'
. '</table>'
. '</div>';
}
}


/**
* Example: Another lazy panel showing database-like profiling info.
*/
class DatabasePanel implements IBarPanel
{
public function getTab(): string
{
return '<span title="Database Panel (lazy)">🗄️ DB</span>';
}

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 = '<h1>Database Panel (lazy loaded)</h1>'
. '<div class="tracy-inner">'
. '<p>Simulated database queries — rendered lazily after the response was sent.</p>'
. '<table><tr><th>#</th><th>Query</th><th>Time</th></tr>';

foreach ($queries as $i => [$query, $time]) {
$html .= '<tr><td>' . ($i + 1) . '</td><td><code>' . htmlspecialchars($query) . '</code></td><td>' . $time . '</td></tr>';
}

$html .= '</table></div>';
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);

?>
<!DOCTYPE html><html class=arrow><link rel="stylesheet" href="assets/style.css">

<h1>Tracy: Lazy Panel Loading Demo</h1>

<h2>How it works</h2>
<p>This demo shows the <code>lazy: true</code> parameter for <code>Debugger::getBar()->addPanel()</code>.</p>

<ul>
<li><strong>⚡ Normal</strong> — A regular panel. Its <code>getPanel()</code> is called during the request.</li>
<li><strong>🐢 Heavy</strong> — A lazy panel simulating a 500ms expensive operation. Content loads on click.</li>
<li><strong>🗄️ DB</strong> — A lazy panel simulating database query profiling. Content loads on click.</li>
</ul>

<h2>Usage</h2>
<pre><code>// Register a lazy panel — getPanel() is NOT called during the request
Debugger::getBar()->addPanel(new MyExpensivePanel, 'my-panel', lazy: true);
</code></pre>

<p>Lazy panels have their <code>getTab()</code> called normally (so the tab is always visible),
but <code>getPanel()</code> 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.</p>

<p>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.</p>

<?php

if (Debugger::$productionMode) {
echo '<p><b>For security reasons, Tracy is visible only on localhost. Look into the source code to see how to enable Tracy.</b></p>';
}
58 changes: 51 additions & 7 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,33 +1,51 @@
parameters:
level: 6
level: 8

paths:
- src

checkMissingCallableSignature: true
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
-
Expand Down Expand Up @@ -74,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
12 changes: 4 additions & 8 deletions src/Bridges/Nette/Bridge.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 4 additions & 3 deletions src/Bridges/Nette/TracyExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,14 @@ 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
]));
}

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);
}
Expand Down Expand Up @@ -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'");
}
Expand Down
Loading