diff --git a/CHANGELOG.md b/CHANGELOG.md index 7059bc6..2f05922 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Chg #162: Replace deprecated `ThrowableResponseFactory` class usage to new one, and remove it (@vjik) - 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) ## 4.3.2 January 09, 2026 diff --git a/src/ErrorHandler.php b/src/ErrorHandler.php index e78b68c..00dc35a 100644 --- a/src/ErrorHandler.php +++ b/src/ErrorHandler.php @@ -137,11 +137,10 @@ public function register(): void } $backtrace = debug_backtrace(0); - if (isset($backtrace[0]['file'])) { - unset($backtrace[0]['function'], $backtrace[0]['class'], $backtrace[0]['type'], $backtrace[0]['args']); - } else { + if (!isset($backtrace[0]['file'])) { array_shift($backtrace); } + array_shift($backtrace); throw new ErrorException($message, $severity, $severity, $file, $line, null, $backtrace); }); diff --git a/src/Renderer/HtmlRenderer.php b/src/Renderer/HtmlRenderer.php index d9074d8..a3c8eb6 100644 --- a/src/Renderer/HtmlRenderer.php +++ b/src/Renderer/HtmlRenderer.php @@ -321,10 +321,6 @@ public function renderCallStack(Throwable $t, array $trace = []): string ); $index = 1; - if ($t instanceof ErrorException) { - $index = 0; - } - foreach ($trace as $traceItem) { $file = !empty($traceItem['file']) ? $traceItem['file'] : null; $line = !empty($traceItem['line']) ? $traceItem['line'] : null; diff --git a/tests/ErrorHandlerTest.php b/tests/ErrorHandlerTest.php index 0d1ec79..eb1772c 100644 --- a/tests/ErrorHandlerTest.php +++ b/tests/ErrorHandlerTest.php @@ -114,8 +114,8 @@ public function testHandleErrorWithCatching(): void $backtrace = $exception->getBacktrace(); $this->assertNotEmpty($backtrace); - $this->assertSame(__FILE__, $backtrace[0]['file']); - $this->assertSame(['file', 'line'], array_keys($backtrace[0])); + $this->assertArrayHasKey('class', $backtrace[0]); + $this->assertSame(self::class, $backtrace[0]['class']); $this->errorHandler->unregister(); } @@ -138,8 +138,8 @@ public function testHandleTriggerErrorWithCatching(): void $backtrace = $exception->getBacktrace(); $this->assertNotEmpty($backtrace); - $this->assertArrayHasKey('file', $backtrace[0]); - $this->assertSame(__FILE__, $backtrace[0]['file']); + $this->assertArrayHasKey('class', $backtrace[0]); + $this->assertSame(self::class, $backtrace[0]['class']); $this->errorHandler->unregister(); } diff --git a/tests/Renderer/HtmlRendererTest.php b/tests/Renderer/HtmlRendererTest.php index 2304231..3d464f0 100644 --- a/tests/Renderer/HtmlRendererTest.php +++ b/tests/Renderer/HtmlRendererTest.php @@ -15,6 +15,7 @@ use RuntimeException; use Yiisoft\ErrorHandler\Exception\ErrorException; use Yiisoft\ErrorHandler\Renderer\HtmlRenderer; +use Yiisoft\ErrorHandler\Tests\Support\TestHelper; use function dirname; use function file_exists; @@ -160,6 +161,22 @@ static function (int $code, string $message) use (&$errorMessage) { $this->assertSame('file(not-exist): Failed to open stream: No such file or directory', $errorMessage); } + public function testRenderCallStackWithErrorException(): void + { + $renderer = new HtmlRenderer(); + + $result = $renderer->renderCallStack( + new ErrorException('test-message'), + TestHelper::generateTrace([true, true, false, true]), + ); + + $this->assertStringContainsString('1. ', $result); + $this->assertStringContainsString('2. ', $result); + $this->assertStringContainsString('3. ', $result); + $this->assertStringContainsString('4. ', $result); + $this->assertStringContainsString('5. ', $result); + } + public function testRenderRequest(): void { $renderer = new HtmlRenderer(); diff --git a/tests/Support/TestHelper.php b/tests/Support/TestHelper.php index db285ef..af081aa 100644 --- a/tests/Support/TestHelper.php +++ b/tests/Support/TestHelper.php @@ -9,6 +9,8 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; +use function dirname; + final class TestHelper { public static function createRequest( @@ -30,4 +32,32 @@ public static function getResponseContent(ResponseInterface $response): string $body->rewind(); return $body->getContents(); } + + /** + * Generates a trace array in the format identical to `debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)`. + * + * @param bool[] $isVendor List of boolean values where `true` means a vendor file and `false` means an application file. + * + * @return array + */ + public static function generateTrace(array $isVendor): array + { + $rootPath = dirname(__DIR__, 2); + $vendorFile = $rootPath . '/vendor/autoload.php'; + $appFile = $rootPath . '/src/ErrorHandler.php'; + + $trace = []; + + foreach ($isVendor as $index => $vendor) { + $trace[] = [ + 'file' => $vendor ? $vendorFile : $appFile, + 'line' => $index + 1, + 'function' => 'testFunction' . $index, + 'class' => 'TestClass', + 'type' => '->', + ]; + } + + return $trace; + } }