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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,30 @@ All notable changes to `phalcon/debugbar` are documented here. The format is bas
- The `debug_page` option shows the debug page for a server error in ADR applications. [#48](https://github.com/phalcon/debugbar/issues/48)
- `Mvc\Bridge` and `ServiceBridge` feed the built-in collectors, which no longer hold framework event names. [#15](https://github.com/phalcon/debugbar/issues/15)
- `DebugBar::addCollector()` subscribes a collector that implements `Contracts\Subscriber`. [#15](https://github.com/phalcon/debugbar/issues/15)
- `Core::createHistory()` creates the request history from the `history` config, and `History\HistoryRecorder` holds its workflow. [#62](https://github.com/phalcon/debugbar/issues/62)
- `Core::getBar()` returns the bar that `boot()` created. [#62](https://github.com/phalcon/debugbar/issues/62)
- `RequestContext` holds the client address and the AJAX and HTTPS flags for each decision, and `Injector::isHtml()` tells if a response is an HTML page. [#62](https://github.com/phalcon/debugbar/issues/62)
- `Security\Redactor::redactUri()` applies the redaction rule to the query of a URI. [#62](https://github.com/phalcon/debugbar/issues/62)
- A shared fixture, `tests/support/assets/DebugBar/panel-contract.json`, makes the PHP and JavaScript tests use the same payload. [#62](https://github.com/phalcon/debugbar/issues/62)

### Changed

- **Breaking:** the built-in collectors no longer implement `Contracts\Subscriber` and have no `subscribe()` method. [#15](https://github.com/phalcon/debugbar/issues/15)
- **Breaking:** `Logger\Adapter` and `Logger\ExceptionAdapter` have no constructor argument. They read the active bar each time the logger writes, so you can create them before the bar boots. Replace `new Adapter(Debug::getBar())` with `new Adapter()`. [#62](https://github.com/phalcon/debugbar/issues/62)
- **Breaking:** `LoggerCollector` takes a `Security\Redactor`, and it redacts the log context. [#62](https://github.com/phalcon/debugbar/issues/62)
- `Core::boot()` and `ResponseListener` take one `?History\HistoryRecorder` in place of the history, endpoint and cookie arguments. [#62](https://github.com/phalcon/debugbar/issues/62)
- `History\HistoryEndpoint::CONTROLLER_NAMESPACE` is now `Mvc\HistoryListener::CONTROLLER_NAMESPACE`. [#62](https://github.com/phalcon/debugbar/issues/62)
- The providers validate the `history` config after they check for the events manager, and for the container or the router. An invalid `history.path` does not throw when history cannot run. [#62](https://github.com/phalcon/debugbar/issues/62)
- A collector no longer sends `widget.icon` and `AbstractCollector` has no `$icon` property, because the browser client never read them. `indicator.icon` does not change. [#62](https://github.com/phalcon/debugbar/issues/62)
- `DebugBarTypes` holds the wire contract and the config shapes, and it lists the panel names, so static analysis rejects an unknown panel name. [#62](https://github.com/phalcon/debugbar/issues/62)
- The debug page in ADR applications uses `Injector::isHtml()` and no longer depends on the injection rules of the bar. [#62](https://github.com/phalcon/debugbar/issues/62)

### Fixed

- History: check storage at boot, remove empty browser directories, fix glob paths on Windows, add `nosniff` to JSON responses.
- The browser asset exports test helpers only outside a DOM, and no longer hard-codes the request collector name.
- The `request` panel and request history no longer show or store a query value under a masked key in the URI. [#62](https://github.com/phalcon/debugbar/issues/62)
- The documentation states the effect of a long-lived worker on request history. [#62](https://github.com/phalcon/debugbar/issues/62)

## [0.4.0](https://github.com/phalcon/debugbar/releases/tag/v0.4.0) (2026-07-14)

Expand Down
35 changes: 24 additions & 11 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ use Phalcon\ADR\Responder\StatusMapper;
use Phalcon\ADR\Responder\ViewResponder;
use Phalcon\Contracts\Logger\Logger as LoggerContract;
use Phalcon\Contracts\View\Renderer;
use Phalcon\DebugBar\Debug;
use Phalcon\DebugBar\Logger\ExceptionAdapter;
use Phalcon\Logger\Adapter\Stream;
use Phalcon\Logger\Logger;
Expand All @@ -174,7 +173,7 @@ $container->set(
'app',
[
'main' => new Stream('/var/log/app.log'),
'debugbar' => new ExceptionAdapter(Debug::getBar()),
'debugbar' => new ExceptionAdapter(),
]
);
}
Expand All @@ -191,7 +190,7 @@ $container->set(
);
```

The closure calls `Debug::getBar()` when the application first uses the logger. This is after the provider boots the bar. The adapter does not change the response and does not keep the exception from the error responder. The panel also shows `RouteNotFound` for a URL with no route, because the error responder logs this too. If the error responder itself fails, the application returns a bare 500 response and logs nothing, so the panel does not show the exception.
The adapter reads the active bar each time the logger writes, so you can create the logger before the provider boots the bar. An exception that the logger writes before the boot is lost. The adapter does not change the response and does not keep the exception from the error responder. The panel also shows `RouteNotFound` for a URL with no route, because the error responder logs this too. If the error responder itself fails, the application returns a bare 500 response and logs nothing, so the panel does not show the exception.

The `history` keys turn on request history. You do not have to add a route. Set `history.base_uri` when the application is not at the root of the host, because an ADR application has no `url` service by default.

Expand Down Expand Up @@ -244,7 +243,7 @@ $debug = (new Debug())->setBlacklist(['server' => ['DB_PASSWORD', 'APP_CRYPT_SAL

These limits apply in an ADR application:

- **One request for each process.** `Phalcon\ADR\Application::handle()` supports a reused request in a long-lived worker, but collectors have no reset method. In a worker, their data grows from request to request. `HistoryCookie::fromGlobals()` also reads only at boot.
- **One request for each process.** `Phalcon\ADR\Application::handle()` supports a reused request in a long-lived worker, but collectors have no reset method. In a worker, their data grows from request to request. `HistoryCookie::fromGlobals()` also reads only at boot, so history uses the cookie of the first browser for all requests in the worker. The request data of one browser can then appear in the history of another browser.
- **Events manager.** The events manager must implement `Phalcon\Events\ManagerInterface`. The default manager does. A manager that implements only `Phalcon\Contracts\Events\Manager` gets no listener, so the bar is not injected.
- **Empty panels.** The `database`, `view` and `cache` panels get data only when the application gives the events manager to these services. The `exceptions` panel gets data only when the application adds `ExceptionAdapter` to its logger. The provider reads no `config` service, so the `config` panel is empty.

Expand Down Expand Up @@ -279,9 +278,18 @@ if (null !== $listener) {
}
```

To use request history, create the recorder from the `history` keys and give it to `boot()`. `createHistory()` returns `null` when the bar cannot boot or history is off. It does not route the URL: send `history.url` to `Phalcon\DebugBar\History\HistoryHandler` in your host, with `$recorder->history()` and `$core->getAccessGate()`.

```php
<?php

$recorder = $core->createHistory(fn () => $request);
$core->boot(null, $request, null, $recorder);
```

## Configuration

The second argument to `Provider` is a nested array. Every key is optional. `Phalcon\DebugBar\ADR\Provider` and `Phalcon\DebugBar\Core` take the same array as their only argument. `Core` does not read the `debug_page` and `history` keys.
The second argument to `Provider` is a nested array. Every key is optional. `Phalcon\DebugBar\ADR\Provider` and `Phalcon\DebugBar\Core` take the same array as their only argument. `Core` does not read the `debug_page` key.

| Key | Type | Default | Purpose |
|--------------------|---------------------------|--------------------------|------------------------------------------------------------------|
Expand Down Expand Up @@ -348,9 +356,9 @@ before applying application controller rules:
```php
<?php

use Phalcon\DebugBar\History\HistoryEndpoint;
use Phalcon\DebugBar\Mvc\HistoryListener;

if (HistoryEndpoint::CONTROLLER_NAMESPACE === $dispatcher->getNamespaceName()) {
if (HistoryListener::CONTROLLER_NAMESPACE === $dispatcher->getNamespaceName()) {
return true;
}
```
Expand Down Expand Up @@ -486,6 +494,8 @@ Debug::getBar()?->addCollector(new OrdersCollector());

`addCollector()` calls `subscribe()` when the bar has an events manager. A collector with no events does not implement `Subscriber`, and you add it the same way. A collector with the name of a built-in collector replaces it. The bridges do not feed the replacement, so it must attach its own listeners.

The bar does not redact the data of a custom collector. Run sensitive data through `Phalcon\DebugBar\Security\Redactor` in the collector. The collector does not receive the `redact.mask` and `redact.hidden` values, so give them to its `Redactor`.

## Sharing the Events Manager

Streamed collectors only receive events from components that fire on the same events manager the bar is subscribed to. Phalcon components fire events only on an events manager set on them explicitly; they do not read one from the container. Register a single events manager as a shared service and hand it to the application and to each component whose events you want.
Expand Down Expand Up @@ -551,18 +561,17 @@ The `exceptions` collector also captures throwables automatically. In an MVC app
- Phalcon 5 (the C extension) exposes the `Phalcon\Logger` adapter contract the adapter builds on.
- Phalcon 6 (`phalcon/phalcon`) exposes the same contract. The adapter attaches the same way on either runtime.

The adapter forwards each `Phalcon\Logger\Item` to the active bar through `Phalcon\DebugBar\Debug::getBar()`, reading the item's level name, message, and PSR-3 context. Forwarding no-ops when the `logger` collector is absent or disabled, so the adapter is safe to leave attached. The adapter also accepts a `null` bar - which `Debug::getBar()` returns until the bar boots - so it can be attached unconditionally.
The adapter forwards each `Phalcon\Logger\Item` to the active bar through `Phalcon\DebugBar\Debug::getBar()`, reading the item's level name, message, and PSR-3 context. Forwarding no-ops when the `logger` collector is absent or disabled, so the adapter is safe to leave attached. The adapter reads the active bar each time the logger writes, so you can attach it before the bar boots. Items that the logger writes before the boot are lost.

```php
<?php

use Phalcon\DebugBar\Debug;
use Phalcon\DebugBar\Logger\Adapter;
use Phalcon\Logger\Adapter\Stream;
use Phalcon\Logger\Logger;

$logger = new Logger('messages', ['main' => new Stream('/var/log/app.log')]);
$logger->addAdapter('debugbar', new Adapter(Debug::getBar()));
$logger->addAdapter('debugbar', new Adapter());

$logger->info('user login', ['user_id' => 42]);
$logger->error('payment declined', ['order_id' => 1001]);
Expand All @@ -589,7 +598,11 @@ use Phalcon\DebugBar\Provider;
- **Masked** keys keep their key but replace the value with `***`.
- **Hidden** keys are dropped entirely.

The default masked keys are `authorization`, `cookie`, `csrf`, `key`, `password`, `secret`, and `token`. The `request`, `config`, and `session` collectors run their data through the redactor. The `session` collector also omits the raw session id and every `$PHALCON/*` internal key, including the CSRF token.
The default masked keys are `authorization`, `cookie`, `csrf`, `key`, `password`, `secret`, and `token`. The redactor matches whole key names, so `token` does not match `reset_token`. Add other keys to `redact.mask`.

The `request`, `config`, `session`, and `logger` collectors run their data through the redactor. The `request` panel and request history also redact the query of the URI. When the redactor changes a value in a query, it rebuilds the query, so the encoding of the other values can change. The `session` collector also omits the raw session id and every `$PHALCON/*` internal key, including the CSRF token.

The `database` panel and request history show and store query bindings as they are, because generated names such as `:APL0` do not match a key. The redactor does not change log messages, exception messages, or stack traces.

`Phalcon\DebugBar\Security\AccessGate` decides who receives the bar within an allowed environment. An empty IP allowlist admits any client. When both an allowlist and a callback are given, both must pass.

Expand Down
10 changes: 5 additions & 5 deletions src/DebugBar/ADR/DebugPageListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,14 @@
use Phalcon\Debug;
use Phalcon\DebugBar\DebugBar;
use Phalcon\DebugBar\Injector;
use Phalcon\DebugBar\RequestContext;
use Phalcon\DebugBar\ResponseListener;
use Phalcon\DebugBar\Security\AccessGate;
use Phalcon\Events\EventInterface;
use Phalcon\Http\RequestInterface;
use Phalcon\Http\ResponseInterface;
use Throwable;

use function is_string;

/**
* Shows the debug page for a server error in an ADR application. The application
* catches each exception before `Debug::listen()` can see it, so this class reads
Expand Down Expand Up @@ -65,10 +64,11 @@ public function __invoke(EventInterface $event, mixed $source, mixed $response):
*/
private function applies(ResponseInterface $response): bool
{
$clientIp = $this->request?->getClientAddress();
$context = RequestContext::fromRequest($this->request);

return ($response->getStatusCode() ?? 0) >= 500
&& $this->accessGate->allows(is_string($clientIp) ? $clientIp : null)
&& (new Injector())->shouldInject($response, $this->request?->isAjax() ?? false);
&& $this->accessGate->allows($context->clientIp)
&& !$context->isAjax
&& (new Injector())->isHtml($response);
}
}
54 changes: 13 additions & 41 deletions src/DebugBar/ADR/Provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,11 @@
use Phalcon\Contracts\Events\Manager as ManagerContract;
use Phalcon\Contracts\Http\AttributeRequest;
use Phalcon\Debug as DebugPage;
use Phalcon\DebugBar\Collector\HistoryCollector;
use Phalcon\DebugBar\Contracts\History;
use Phalcon\DebugBar\Core;
use Phalcon\DebugBar\Debug;
use Phalcon\DebugBar\DebugBar;
use Phalcon\DebugBar\DebugBarTypes;
use Phalcon\DebugBar\Exceptions\CannotUseInProduction;
use Phalcon\DebugBar\History\FilesystemHistory;
use Phalcon\DebugBar\History\HistoryCookie;
use Phalcon\DebugBar\History\HistoryEndpoint;
use Phalcon\DebugBar\History\HistoryOptions;
use Phalcon\DebugBar\History\HistoryRecorder;
use Phalcon\DebugBar\ResponseListener;
use Phalcon\Events\ManagerInterface;
use Phalcon\Http\RequestInterface;
Expand All @@ -51,20 +45,13 @@ final class Provider implements ProviderContract

private bool | DebugPage $debugPage;

private ?HistoryCookie $historyCookie = null;

private ?HistoryEndpoint $historyEndpoint = null;

private HistoryOptions $historyOptions;

/**
* @param provider_config $config
*/
public function __construct(array $config = [])
{
$this->core = new Core($config);
$this->debugPage = $config['debug_page'] ?? false;
$this->historyOptions = HistoryOptions::fromConfig($config['history'] ?? []);
$this->core = new Core($config);
$this->debugPage = $config['debug_page'] ?? false;
}

/**
Expand All @@ -80,16 +67,13 @@ public function provide(Collection $services): void
$eventsManager = $eventsManager instanceof ManagerInterface ? $eventsManager : null;
$request = $this->resolve($services, AttributeRequest::class);
$request = $request instanceof RequestInterface ? $request : null;
$history = $this->registerHistory($services, $eventsManager, $request);
$listener = $this->core->boot(
$eventsManager,
$request,
null,
$history,
$this->historyEndpoint,
$this->historyCookie
$this->registerHistory($services, $eventsManager, $request)
);
$bar = Debug::getBar();
$bar = $this->core->getBar();

if (null === $eventsManager || null === $listener || null === $bar) {
return;
Expand All @@ -107,31 +91,19 @@ private function registerHistory(
Collection $services,
?ManagerInterface $eventsManager,
?RequestInterface $request
): ?History {
if (
!$this->historyOptions->enabled
|| !$this->core->isCollectorEnabled(HistoryCollector::NAME)
) {
): ?HistoryRecorder {
if (null === $eventsManager || !$services->has(Router::class)) {
return null;
}

$this->historyOptions->validate();

if (null === $eventsManager || !$services->has(Router::class)) {
$recorder = $this->core->createHistory(fn (): ?RequestInterface => $request);
if (null === $recorder) {
return null;
}

$this->historyCookie = HistoryCookie::fromGlobals();
$history = new FilesystemHistory($this->historyOptions, null, $this->historyCookie);
$history->validateStorage();
$accessGate = $this->core->getAccessGate();
$baseUri = $this->historyOptions->baseUri;
$endpoint = new HistoryEndpoint(
$this->historyOptions,
fn (): ?RequestInterface => $request,
fn (): string => $baseUri
);
$this->historyEndpoint = $endpoint;
$accessGate = $this->core->getAccessGate();
$endpoint = $recorder->endpoint();
$history = $recorder->history();

$services->set(
HistoryAction::class,
Expand All @@ -142,7 +114,7 @@ private function registerHistory(
fn (Router $router): Router => new HistoryRouter($router, $endpoint)
);

return $history;
return $recorder;
}

private function resolve(Collection $services, string $name): mixed
Expand Down
11 changes: 6 additions & 5 deletions src/DebugBar/Collector/AbstractCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,27 @@

/**
* Optional base for collectors. Supplies the `NAME` wiring and `getWidget()`
* scaffolding from three protected members, so a subclass only sets its icon,
* label, panel type, and `collect()`.
* scaffolding from protected members, so a subclass only sets its label, panel
* type, and `collect()`.
*
* @phpstan-import-type widget from DebugBarTypes
* @phpstan-import-type indicator from DebugBarTypes
* @phpstan-import-type panel_type from DebugBarTypes
*/
abstract class AbstractCollector implements Renderable
{
public const NAME = '';

protected string $icon = '';

/**
* @var indicator|null
*/
protected ?array $indicator = null;

protected string $label = '';

/**
* @var panel_type
*/
protected string $panel = 'grid';

public function getName(): string
Expand All @@ -54,7 +56,6 @@ public function getWidget(): array
{
$widget = [
'label' => ('' !== $this->label) ? $this->label : $this->getName(),
'icon' => $this->icon,
'panel' => $this->panel,
];

Expand Down
2 changes: 0 additions & 2 deletions src/DebugBar/Collector/CacheCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ final class CacheCollector extends AbstractCollector
{
public const NAME = 'cache';

protected string $icon = 'icon-cache';

protected string $label = 'Cache';

protected string $panel = 'list';
Expand Down
2 changes: 0 additions & 2 deletions src/DebugBar/Collector/ConfigCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ final class ConfigCollector extends AbstractCollector

public const NAME = 'config';

protected string $icon = 'icon-config';

protected string $label = 'Config';

protected string $panel = 'grid';
Expand Down
Loading
Loading