php-forge / debug
Framework-neutral contracts and declarative presentation models for debugger extensions.
Requires
- php: >=8.3
Requires (Dev)
- infection/infection: ^0.35
- maglnet/composer-require-checker: ^4.1
- php-forge/coding-standard: ^0.3
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.2
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0.3
- phpunit/phpunit: ^12.5
- psr/log: ^3.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-13 12:57:42 UTC
README
Debug
Framework-neutral contracts for portable collectors and panels rendered by the debugger frontend.
Installation
composer require php-forge/debug:^0.1
The package ships contracts and presentation models only. Its sole requirement is PHP 8.3: it pulls in no framework,
HTML library, or debugger engine, and installing it activates nothing. A host such as
php-forge/debug-core renders whatever an extension declares through these
contracts.
A complete panel in two classes
A collector buffers diagnostics while the request runs; a panel turns the stored capture into a view. Neither imports a debugger engine, and both share one identifier.
use PHPForge\Debug\CollectorInterface; final class CacheCollector implements CollectorInterface { /** * @var list<array{string, string, string}> */ private array $operations = []; private bool $started = false; public function capture(): array|null { return $this->started ? ['operations' => $this->operations] : null; } public function id(): string { return 'cache'; } public function record(string $operation, string $key, string $result): void { if ($this->started) { $this->operations[] = [$operation, $key, $result]; } } public function shutdown(): void { $this->started = false; $this->operations = []; } public function startup(): void { $this->started = true; } }
use PHPForge\Debug\{ColumnStyle, Panel, PanelView}; final class CachePanel extends Panel { protected const string ICON = 'db'; protected const string ID = 'cache'; protected const string TITLE = 'Cache'; public function present(array $data): PanelView { $operations = is_array($data['operations'] ?? null) ? $data['operations'] : []; $view = PanelView::create() ->summary(count($operations) === 1 ? ' operation' : ' operations', count($operations)) ->toolbar('Cache', count($operations)) ->active($operations !== []); return $operations === [] ? $view->emptyState('No cache operations', 'The cache was observed, but nothing happened.') : $view->table( ['Operation', 'Key', 'Result'], $operations, collapsible: true, styles: [1 => ColumnStyle::IDENTIFIER], ); } }
Call record() from the application service that already knows about the operation. capture() returns null when
there is nothing to report, and an array otherwise: an empty array is an observed empty request, not absence. The
host encodes that array strictly, so omit secrets and keep values JSON-encodable.
Register it
Merge these fragments into an application whose debugger is already enabled. The collector's id() and the panel's
ID must match: that is how the host pairs a capture with its panel.
// Yii2: inside the YII_DEBUG guard. Declare 'modules' => [] in the application configuration so the offset stays // typed under PHPStan level max; the guard then only fills in the debug entry. $config['modules']['debug'] = [ 'class' => DebugModule::class, 'collectors' => ['cache-operations' => new CacheCollector()], 'panels' => ['cache-operations' => new CachePanel()], ];
// Yii3: return the extended registry from the application's development DI factory. $collector = new CacheCollector(); $registry = $registry ->withCollector($collector) ->withPanel(new CachePanel());
Both hosts derive the IDs from the objects themselves. CollectorInterface is the only collector contract the
debugger has, so the collector is registered as it is, and only the panel is adapted to the host's own panel type.
No catalog entry, icon enum, storage dispatch entry, or change to an official package is needed. Inject the same $collector into the application service that
calls record().
A runnable version of this example, capturing through PSR-3 instead of a direct call, lives in
tests/Support; python3 tools/check-consumer.py installs it as an independent Composer package and
replays a stored capture with no debugger host present.
Presentation vocabulary
Five types are published: CollectorInterface, Panel, PanelView, Tone, and ColumnStyle. Everything a panel can
display is a PanelView method, so there is no value class to import and no shape to build by hand.
use PHPForge\Debug\{ColumnStyle, PanelView, Tone}; PanelView::create() ->summary(' props', 2) ->toolbar('Props', 2) ->heading('Props', section: true) ->overview(['Component' => 'Site', 'State' => PanelView::badge('shared', Tone::INFO)]) ->paragraph('Rendered by ', PanelView::code('Inertia::render()')) ->callout(Tone::WARNING, 'Runtime inspection is unavailable.') ->table(['Prop', 'Value'], [['auth', PanelView::value(['id' => 1])]], styles: [0 => ColumnStyle::IDENTIFIER]) ->group('Component', PanelView::create()->paragraph('Nested content')) ->emptyState('No operations', 'The cache was observed, but nothing happened.') ->disclosure('Raw payload', $json) ->active(true);
Plain scalars and null become text. PanelView::text(), ::strong(), ::code(), ::preview(), ::badge(), and
::value() produce validated inline values accepted wherever a scalar is accepted. Every method validates its
arguments and rejects invalid input with an explicit InvalidArgumentException. The host reads the finished
description through summaryMetrics(), toolbarMetrics(), blocks(), and isActive().