Debug Bar¶
Overview¶
phalcon/debugbar is a separate package that provides two things from one install:
- The debug bar - a status bar injected into your application's HTML that surfaces per-request diagnostics through collectors.
- The debug page - the framework's exception and error page, migrated out of Phalcon\Support\Debug into this package as
Phalcon\Debug.
The bar runs only in development. It refuses to boot when the environment is undefined or blocked, and it renders nothing on non-HTML responses.
DANGER
The debug bar and the debug page reveal information about your application and server. Never enable either in a production environment.
Requirements¶
- PHP 8.1 or later.
- A Phalcon runtime: the Phalcon 5 C extension, or the Phalcon 6 PHP implementation (
phalcon/phalcon). matthiasmullie/minify, pulled in automatically by Composer. It minifies the bar's CSS and JavaScript before they are inlined.
Installation¶
The Debug Page¶
The debug page is Phalcon\Debug, migrated from Phalcon\Support\Debug with its public API unchanged. An existing application moves over by swapping the namespace:
The remainder of this document covers the debug bar.
Registering the Debug Bar¶
The bar is booted by Phalcon\DebugBar\Provider. It takes the MVC application and an optional configuration array. Its only coupling to the application is the application's events manager, so the application must have one set before the bar boots.
<?php
use Phalcon\DebugBar\Provider;
use Phalcon\Events\Manager as EventsManager;
use Phalcon\Mvc\Application;
$application = new Application($container);
$application->setEventsManager(new EventsManager());
(new Provider($application))->boot();
echo $application->handle($_SERVER['REQUEST_URI'])->getContent();
WARNING
Outside a permitted environment the bar does nothing: boot() returns without registering anything, so it is safe to call unconditionally. Set env.strict to true to make it throw Phalcon\DebugBar\Exceptions\CannotUseInProduction instead. The environment is read from APP_ENV (getenv(), then $_ENV, then $_SERVER); the default blocked list is production and prod.
If the application has no events manager, boot() still registers the Debug facade but attaches no response listener, so nothing is injected. Set an events manager to enable the bar.
Configuration¶
The second argument to Provider is a nested array. Every key is optional.
| Key | Type | Default | Purpose |
|---|---|---|---|
access.allow_ips | list<string> | [] (any client) | Client IP allowlist. Empty allows any address. |
access.callback | (Closure(): bool)\|null | null | Extra gate. When present, it must also return true. |
assets.nonce | string\|null | null | CSP nonce stamped on the injected <style> and <script> tags. |
collectors | array<string, bool> | all enabled | Per-collector switch, keyed by collector name. |
enabled | bool | true | Master switch. When false, boot() returns after the gate. |
env.blocked | list<string> | ['production', 'prod'] | Environment values that block the bar (case-insensitive). |
env.strict | bool | false | When true, boot() throws in a blocked/undefined environment instead of returning silently. |
env.var | string | APP_ENV | Environment variable inspected by the gate. |
headers | bool | true | Emit the X-Debug-Bar diagnostic header. |
redact.hidden | list<string> | [] | Keys dropped from the output entirely. |
redact.mask | list<string> | [] | Extra keys whose values are masked (added to the defaults). |
<?php
use Phalcon\DebugBar\Provider;
(new Provider($application, [
'env' => ['var' => 'APP_ENV', 'blocked' => ['production', 'staging']],
'access' => ['allow_ips' => ['127.0.0.1', '10.0.0.5']],
'collectors' => ['cache' => false, 'view' => false],
'redact' => ['mask' => ['api_key'], 'hidden' => ['secret_question']],
]))->boot();
Collectors¶
Each collector contributes one tab. A collector reads its data in one of four ways:
- Snapshot - reads state when the response is assembled.
- Streamed - subscribes to framework events and accumulates as they fire.
- Manual - fed through the
Debugfacade. - Adapter - captured from a
Phalcon\Loggerthrough the debug bar's logger adapter.
| Name | Source | Inflow |
|---|---|---|
cache | Cache operations and their keys | streamed |
config | The config service, flattened and redacted | snapshot |
database | SQL statements, bound parameters, and timings | streamed |
exceptions | Throwables, with stack traces | manual + streamed |
logger | Log entries captured from a Phalcon\Logger adapter | adapter |
messages | Messages recorded through the facade | manual |
request | Request method, URI, query, post, and headers (redacted) | snapshot |
route | Matched module, controller, action, and parameters | streamed |
session | $_SESSION, flattened and redacted | snapshot |
version | PHP and Phalcon versions | snapshot |
time | Total request time and named measures | manual + snapshot |
view | Rendered view paths and timings | streamed |
Streamed collectors read their data off the event source (the connection, view, router, or cache handed to them by the event). They never resolve a service from the container.
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.
<?php
use Phalcon\Events\Manager as EventsManager;
$container->setShared('eventsManager', function () {
return new EventsManager();
});
// On the application, so the bar can attach its response listener.
$application->setEventsManager($container->getShared('eventsManager'));
// On each component whose events feed a collector.
$connection->setEventsManager($container->getShared('eventsManager')); // database
$view->setEventsManager($container->getShared('eventsManager')); // view
$router->setEventsManager($container->getShared('eventsManager')); // route
$dispatcher->setEventsManager($container->getShared('eventsManager')); // exceptions
A collector whose component does not share the events manager reports nothing. Snapshot and manual collectors are unaffected.
Manual Instrumentation¶
Phalcon\DebugBar\Debug is a static facade. It forwards to the active bar and no-ops when the bar was never registered, so instrumentation left in the code is safe in any environment.
<?php
use Phalcon\DebugBar\Debug;
Debug::message('user resolved', 'auth');
Debug::warning($payload);
Debug::startMeasure('render');
// ... work ...
Debug::stopMeasure('render');
try {
$service->charge($order);
} catch (\Throwable $exception) {
Debug::addException($exception);
}
| Method | Collector | Effect |
|---|---|---|
addException(\Throwable $throwable) | exceptions | Records a throwable and its trace. |
info() / debug() / notice() / warning() / error() | messages | Record one message per argument at that level. |
message(mixed $value, string $label = 'info') | messages | Records a labelled message. |
startMeasure(string $name, ?string $label = null) | time | Opens a named measure. |
stopMeasure(string $name) | time | Closes the named measure. |
The exceptions collector also captures throwables automatically. It subscribes to dispatch:beforeException, records the throwable, and leaves it to bubble - the bar never handles or swallows it. A throwable appears in the panel only when the request still finishes with a rendered response; a caught exception, or one the application forwards to an error page, qualifies, while a fully unhandled one aborts before the bar renders.
Capturing Logs¶
Phalcon\DebugBar\Logger\Adapter is a Phalcon\Logger adapter. Attach it to the application logger and every logged item is captured in the bar's logger collector, shown in the "Logs" tab. This is separate from the messages collector: logger is the automatic application log, while messages is what the code records by hand through the Debug facade.
- Phalcon 5 (the C extension) exposes the
Phalcon\Loggeradapter 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.
<?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->info('user login', ['user_id' => 42]);
$logger->error('payment declined', ['order_id' => 1001]);
Each entry keeps its log level as the label and its message beside it. An entry logged with PSR-3 context becomes collapsible: the level and message stay visible and expand to the context, rendered as pretty-printed JSON. An entry logged without context renders as a plain row.
The logger collector is registered by default. Disable it, like any collector, through the collectors key:
<?php
use Phalcon\DebugBar\Provider;
(new Provider($application, [
'collectors' => ['logger' => false],
]))->boot();
Redaction and Access Control¶
Phalcon\DebugBar\Security\Redactor guards data before it leaves PHP. It matches keys case-insensitively through nested arrays and applies two tiers:
- 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.
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.
<?php
use Phalcon\DebugBar\Provider;
(new Provider($application, [
'access' => [
'allow_ips' => ['127.0.0.1'],
'callback' => fn (): bool => 'admin' === ($_SESSION['role'] ?? null),
],
'redact' => ['hidden' => ['card_number']],
]))->boot();
The Bar in the Browser¶
The bar's CSS and JavaScript are minified and injected inline; the bar has no external asset to host or serve. On CSP-restricted pages, set assets.nonce so the inline tags carry a nonce.
The bar sits at the bottom of the page. Each collector is a tab; a tab shows a badge when the collector reports a count or a summary value. Clicking a tab opens its panel:
- grid panels (version, request, config, session, route) render a key and value table.
- list panels (time, messages, database, view, cache) render labelled rows.
- exceptions panels render one collapsible entry per throwable; the summary line stays visible and expands to the stack trace.
- logs panels (logger) render one entry per logged item; an entry with context is collapsible - the level and message stay visible and expand to the context.
A handle at the right of the tab row collapses the whole bar to a corner button, so it never covers the host page's own controls. The collapsed state is remembered across page loads.
Example: Vökuró¶
The Vökuró sample application wires the debug bar end to end: a shared events manager registered as a service, handed to the database, view, router, and dispatcher providers, and a provider that boots the bar against the MVC application. It is a working reference for the setup described above.