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 @@ -87,6 +87,7 @@ All notable changes to `phalcon/debugbar` are documented here. The format is bas
- Keep host CSS out of the bar tables, code blocks and buttons, stop history requests after 10 seconds, and show "Unable to load" in a history row that fails. [#102](https://github.com/phalcon/debugbar/issues/102)
- Pin `pie.phar` to a release with a sha256 check, build the PHP 8.1 Docker image, declare `ext-mbstring`, and list the Phalcon version that each feature needs. [#104](https://github.com/phalcon/debugbar/issues/104)
- Name the collapse toggle and the tabs for screen readers, close the open panel or the history with Escape, and fix two `Phalcon\Debug` docblocks. [#106](https://github.com/phalcon/debugbar/issues/106)
- Open the ADR debug page only for an exception logged at `error` level or higher, and show a lower-level exception only in the `exceptions` panel. [#107](https://github.com/phalcon/debugbar/issues/107)
- Test HTML writes, the collapse state and `localStorage` errors in the JS tests, and keep every listener in the fake DOM. [#103](https://github.com/phalcon/debugbar/issues/103)

## [0.4.0](https://github.com/phalcon/debugbar/releases/tag/v0.4.0) (2026-07-14)
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ The provider decorates the router (`Phalcon\Contracts\ADR\Router\Router`) with `

Do not resolve the router during registration. `extend()` on a resolved service throws `CannotExtendResolved`. In a blocked environment, the provider does not change the router.

The `debug_page` key shows the debug page (`Phalcon\Debug`) for a server error. It is off by default. The provider reads the exception from the bar, so add `ExceptionAdapter` to your logger.
The `debug_page` key shows the debug page (`Phalcon\Debug`) for a server error. It is off by default. The provider reads the exception from the bar, so add `ExceptionAdapter` to your logger. Only an exception logged at `error` level or higher opens the debug page. An exception logged at a lower level shows only in the `exceptions` panel, so log an exception that your application handles at `warning` or lower.

```php
<?php
Expand Down
25 changes: 23 additions & 2 deletions src/DebugBar/Logger/ExceptionAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@

namespace Phalcon\DebugBar\Logger;

use Phalcon\DebugBar\Collector\ExceptionsCollector;
use Phalcon\DebugBar\Contracts\ExceptionAware;
use Phalcon\DebugBar\Debug;
use Phalcon\Logger\Adapter\AbstractAdapter;
use Phalcon\Logger\Enum;
use Phalcon\Logger\Item;
use Throwable;

Expand All @@ -25,6 +28,10 @@
* `Adapter`: an MVC application that logs an exception would record it two times.
* The adapter reads the active bar each time the logger writes, so you can create
* it before the bar boots.
*
* Only an item at error level or higher sets the last exception, which the ADR
* debug page shows. An item at a lower level is an exception that the
* application handled: it shows only in the Exceptions panel.
*/
final class ExceptionAdapter extends AbstractAdapter
{
Expand All @@ -36,8 +43,22 @@ public function close(): bool
public function process(Item $item): void
{
$exception = $item->getContext()['exception'] ?? null;
if ($exception instanceof Throwable) {
Debug::getBar()?->addException($exception);
$bar = Debug::getBar();
if (!$exception instanceof Throwable || null === $bar) {
return;
}

if ($item->getLevel() <= Enum::ERROR) {
$bar->addException($exception);

return;
}

$collector = $bar->hasCollector(ExceptionsCollector::NAME)
? $bar->getCollector(ExceptionsCollector::NAME)
: null;
if ($collector instanceof ExceptionAware) {
$collector->addThrowable($exception);
}
}
}
78 changes: 78 additions & 0 deletions tests/Unit/DebugBar/Logger/ExceptionAdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
use Phalcon\DebugBar\DebugBar;
use Phalcon\DebugBar\Logger\ExceptionAdapter;
use Phalcon\DebugBar\Security\Redactor;
use Phalcon\Logger\Enum;
use Phalcon\Logger\Item;
use Phalcon\Logger\Logger;
use Phalcon\Talon\PHPUnit\AbstractUnitTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use RuntimeException;

final class ExceptionAdapterTest extends AbstractUnitTestCase
Expand All @@ -41,6 +43,33 @@ protected function tearDown(): void
parent::tearDown();
}

/**
* @return array<string, array{0: int}>
*/
public static function errorLevelProvider(): array
{
return [
'emergency' => [Enum::EMERGENCY],
'alert' => [Enum::ALERT],
'critical' => [Enum::CRITICAL],
'error' => [Enum::ERROR],
];
}

/**
* @return array<string, array{0: int}>
*/
public static function lowerLevelProvider(): array
{
return [
'warning' => [Enum::WARNING],
'notice' => [Enum::NOTICE],
'info' => [Enum::INFO],
'debug' => [Enum::DEBUG],
'custom' => [Enum::CUSTOM],
];
}

public function testAdapterCreatedBeforeTheBarBootsForwardsToTheBar(): void
{
$logger = new Logger('debug', ['debugbar' => new ExceptionAdapter()]);
Expand All @@ -55,6 +84,55 @@ public function testAdapterCreatedBeforeTheBarBootsForwardsToTheBar(): void
$this->assertSame($exception, $bar->getLastException());
}

#[DataProvider('errorLevelProvider')]
public function testAnExceptionAtErrorLevelOrHigherIsTheLastException(int $level): void
{
$collector = new ExceptionsCollector();
$bar = (new DebugBar())->addCollector($collector);
Debug::setBar($bar);
$exception = new RuntimeException('boom');

(new ExceptionAdapter())->process(
new Item('failed', 'level', $level, new DateTimeImmutable(), ['exception' => $exception])
);

$this->assertSame(1, $collector->collect()['badge']);
$this->assertSame($exception, $bar->getLastException());
}

/**
* A handled exception that is logged below error level must not open the
* debug page of an ADR application.
*/
#[DataProvider('lowerLevelProvider')]
public function testAnExceptionBelowErrorLevelShowsInThePanelButIsNotTheLastException(int $level): void
{
$collector = new ExceptionsCollector();
$bar = (new DebugBar())->addCollector($collector);
Debug::setBar($bar);

(new ExceptionAdapter())->process(
new Item('handled', 'level', $level, new DateTimeImmutable(), ['exception' => new RuntimeException('boom')])
);

$this->assertSame(1, $collector->collect()['badge']);
$this->assertNull($bar->getLastException());
}

public function testAnExceptionBelowErrorLevelWithNoExceptionsCollectorIsIgnored(): void
{
$bar = new DebugBar();
Debug::setBar($bar);

$exception = new RuntimeException('boom');

(new ExceptionAdapter())->process(
new Item('handled', 'warning', Enum::WARNING, new DateTimeImmutable(), ['exception' => $exception])
);

$this->assertNull($bar->getLastException());
}

public function testCloseReturnsTrue(): void
{
$adapter = new ExceptionAdapter();
Expand Down
Loading