Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Improve closure rendering in stack traces
  • Loading branch information
WarLikeLaux committed Mar 27, 2026
commit 56c155e77c7a5e541644186f196c2edcfca05842
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Enh #163: Explicitly import classes, functions, and constants in "use" section (@mspirkov)
- Bug #164: Fix missing items in stack trace HTML output when handling a PHP error (@vjik)
- Bug #166: Fix broken link to error handling guide (@vjik)
- Enh #172: Improve closure rendering in stack traces and keep items when source is unavailable (@WarLikeLaux)

## 4.3.2 January 09, 2026

Expand Down
41 changes: 35 additions & 6 deletions src/Renderer/HtmlRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ public function renderCallStack(Throwable $t, array $trace = []): string
$function = null;
if (!empty($traceItem['function']) && $traceItem['function'] !== 'unknown') {
$function = $traceItem['function'];
if (!str_contains($function, '{closure}')) {
if (!str_contains($function, '{closure')) {
try {
if ($class !== null && class_exists($class)) {
$parameters = (new ReflectionMethod($class, $function))->getParameters();
Expand Down Expand Up @@ -565,6 +565,34 @@ public function removeAnonymous(string $value): string
return $anonymousPosition !== false ? substr($value, 0, $anonymousPosition) : $value;
}

/**
* Formats a trace function name for display.
*
* Handles PHP 8.4+ closure format `{closure:Context:line}` by extracting the definition context.
* For regular functions, prepends the class name when available.
*/
public function formatTraceFunctionName(?string $class, string $function): string
{
// PHP 8.4+: {closure:Context:line} - already contains full definition context.
if (preg_match('/^\{closure:(.+):(\d+)\}$/', $function, $matches)) {
return '{closure} ' . $matches[1] . ':' . $matches[2];
}

// PHP < 8.4 namespaced closure: Namespace\{closure} - strip redundant namespace.
if (str_contains($function, '\\{closure')) {
if ($class !== null && $class !== 'Closure') {
return $this->removeAnonymous($class) . '::{closure}';
}
return $function;
}

if ($class === null || $class === 'Closure') {
return $function;
}

return $this->removeAnonymous($class) . '::' . $function;
}

/**
* Extracts a user-facing description from throwable class PHPDoc.
*
Expand Down Expand Up @@ -740,12 +768,13 @@ private function renderCallStackItem(
if ($file !== null && $line !== null) {
$line--; // adjust line number from one-based to zero-based
$lines = @file($file);
if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line) {
return '';
if ($line >= 0 && $lines !== false && ($lineCount = count($lines)) > $line) {
Comment thread
WarLikeLaux marked this conversation as resolved.
Outdated
$half = (int) (($index === 1 ? $this->maxSourceLines : $this->maxTraceLines) / 2);
$begin = $line - $half > 0 ? $line - $half : 0;
$end = $line + $half < $lineCount ? $line + $half : $lineCount - 1;
} else {
$lines = [];
}
$half = (int) (($index === 1 ? $this->maxSourceLines : $this->maxTraceLines) / 2);
$begin = $line - $half > 0 ? $line - $half : 0;
$end = $line + $half < $lineCount ? $line + $half : $lineCount - 1;
}

return $this->renderTemplate($this->defaultTemplatePath . '/_call-stack-item.php', [
Expand Down
2 changes: 1 addition & 1 deletion templates/_call-stack-item.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
<span class="function-info word-break">
<?php
echo $file === null ? "$index. " : '&mdash;&nbsp;';
$function = $class === null ? $function : "{$this->removeAnonymous($class)}::$function";
$function = $this->formatTraceFunctionName($class, $function);

echo '<span class="function">' . $this->htmlEncode($function) . '</span>';
echo '<span class="arguments">(';
Expand Down
Loading