Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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 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)
- Bug #172: Fix closure rendering in stack traces and keep items when source is unavailable (@WarLikeLaux)
Comment thread
WarLikeLaux marked this conversation as resolved.
Outdated

## 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
Loading