Skip to content
Merged
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 2 additions & 3 deletions src/ErrorHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
private readonly LoggerInterface $logger,
private readonly ThrowableRendererInterface $defaultRenderer,
private readonly ?EventDispatcherInterface $eventDispatcher = null,
private readonly int $exitShutdownHandlerDepth = 2,

Check warning on line 56 in src/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "IncrementInteger": --- Original +++ New @@ @@ * @param EventDispatcherInterface|null $eventDispatcher Event dispatcher for error events. * @param int $exitShutdownHandlerDepth Depth of the exit() shutdown handler to ensure it's executed last. */ - public function __construct(private readonly LoggerInterface $logger, private readonly ThrowableRendererInterface $defaultRenderer, private readonly ?EventDispatcherInterface $eventDispatcher = null, private readonly int $exitShutdownHandlerDepth = 2) + public function __construct(private readonly LoggerInterface $logger, private readonly ThrowableRendererInterface $defaultRenderer, private readonly ?EventDispatcherInterface $eventDispatcher = null, private readonly int $exitShutdownHandlerDepth = 3) { } /**

Check warning on line 56 in src/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "DecrementInteger": --- Original +++ New @@ @@ * @param EventDispatcherInterface|null $eventDispatcher Event dispatcher for error events. * @param int $exitShutdownHandlerDepth Depth of the exit() shutdown handler to ensure it's executed last. */ - public function __construct(private readonly LoggerInterface $logger, private readonly ThrowableRendererInterface $defaultRenderer, private readonly ?EventDispatcherInterface $eventDispatcher = null, private readonly int $exitShutdownHandlerDepth = 2) + public function __construct(private readonly LoggerInterface $logger, private readonly ThrowableRendererInterface $defaultRenderer, private readonly ?EventDispatcherInterface $eventDispatcher = null, private readonly int $exitShutdownHandlerDepth = 1) { } /**
) {}

/**
Expand All @@ -70,7 +70,7 @@
$renderer ??= $this->defaultRenderer;

try {
$this->logger->error($t->getMessage(), ['throwable' => $t]);

Check warning on line 73 in src/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "MethodCallRemoval": --- Original +++ New @@ @@ { $renderer ??= $this->defaultRenderer; try { - $this->logger->error($t->getMessage(), ['throwable' => $t]); + return $this->debug ? $renderer->renderVerbose($t, $request) : $renderer->render($t, $request); } catch (Throwable $t) { return new ErrorData((string) $t);

Check warning on line 73 in src/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "ArrayItemRemoval": --- Original +++ New @@ @@ { $renderer ??= $this->defaultRenderer; try { - $this->logger->error($t->getMessage(), ['throwable' => $t]); + $this->logger->error($t->getMessage(), []); return $this->debug ? $renderer->renderVerbose($t, $request) : $renderer->render($t, $request); } catch (Throwable $t) { return new ErrorData((string) $t);
return $this->debug ? $renderer->renderVerbose($t, $request) : $renderer->render($t, $request);
} catch (Throwable $t) {
return new ErrorData((string) $t);
Expand Down Expand Up @@ -110,14 +110,14 @@
return;
}

if ($this->memoryReserveSize > 0) {

Check warning on line 113 in src/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "GreaterThanNegotiation": --- Original +++ New @@ @@ if ($this->enabled) { return; } - if ($this->memoryReserveSize > 0) { + if ($this->memoryReserveSize <= 0) { $this->memoryReserve = str_repeat('x', $this->memoryReserveSize); } $this->initializeOnce();

Check warning on line 113 in src/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "GreaterThan": --- Original +++ New @@ @@ if ($this->enabled) { return; } - if ($this->memoryReserveSize > 0) { + if ($this->memoryReserveSize >= 0) { $this->memoryReserve = str_repeat('x', $this->memoryReserveSize); } $this->initializeOnce();
$this->memoryReserve = str_repeat('x', $this->memoryReserveSize);
}

$this->initializeOnce();

Check warning on line 117 in src/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "MethodCallRemoval": --- Original +++ New @@ @@ if ($this->memoryReserveSize > 0) { $this->memoryReserve = str_repeat('x', $this->memoryReserveSize); } - $this->initializeOnce(); + // Handles throwable that isn't caught otherwise, echo output and exit. set_exception_handler(function (Throwable $t) : void { if (!$this->enabled) {

// Handles throwable that isn't caught otherwise, echo output and exit.
set_exception_handler(function (Throwable $t): void {

Check warning on line 120 in src/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "FunctionCallRemoval": --- Original +++ New @@ @@ $this->memoryReserve = str_repeat('x', $this->memoryReserveSize); } $this->initializeOnce(); - // Handles throwable that isn't caught otherwise, echo output and exit. - set_exception_handler(function (Throwable $t) : void { - if (!$this->enabled) { - return; - } - $this->renderThrowableAndTerminate($t); - }); + // Handles PHP execution errors such as warnings and notices. set_error_handler(function (int $severity, string $message, string $file, int $line) : bool { if (!$this->enabled) {
if (!$this->enabled) {
return;
}
Expand All @@ -131,17 +131,16 @@
return false;
}

if (!(error_reporting() & $severity)) {

Check warning on line 134 in src/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "BitwiseAnd": --- Original +++ New @@ @@ if (!$this->enabled) { return false; } - if (!(error_reporting() & $severity)) { + if (!(error_reporting() | $severity)) { // This error code is not included in error_reporting. return true; }
// This error code is not included in error_reporting.
return true;
}

$backtrace = debug_backtrace(0);

Check warning on line 139 in src/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "DecrementInteger": --- Original +++ New @@ @@ // This error code is not included in error_reporting. return true; } - $backtrace = debug_backtrace(0); + $backtrace = debug_backtrace(-1); if (!isset($backtrace[0]['file'])) { array_shift($backtrace); }
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);
});
Expand Down
4 changes: 0 additions & 4 deletions src/Renderer/HtmlRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions tests/ErrorHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -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();
}
Expand Down
17 changes: 17 additions & 0 deletions tests/Renderer/HtmlRendererTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
30 changes: 30 additions & 0 deletions tests/Support/TestHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;

use function dirname;

final class TestHelper
{
public static function createRequest(
Expand All @@ -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;
}
}
Loading