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-17 20:17:00 UTC
README
Debug
Framework-neutral contracts for portable collectors and panels rendered by the debugger frontend.
Installation
composer require php-forge/debug:^0.3
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)); 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 array key is the stable ID: it must
equal the collector's id() and the panel's ID, and both hosts reject a mismatch. TITLE and ICON are only the
defaults the panel ships with, so the host configuration may override both without touching the class.
// 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' => $cacheCollector], 'panels' => [ 'cache' => ['class' => CachePanel::class, 'title' => 'Cache operations', 'icon' => 'db', 'position' => 1], ], ];
A plain CachePanel::class string or new CachePanel() registers the panel with the provider defaults. Inject the
same $cacheCollector instance into the application service that calls record().
Yii3 reads the same shape from the application configuration, not from a registry object.
// config/web/params.php 'yii3/debug' => [ 'collectors' => ['cache' => CacheCollector::class], 'panels' => [ 'cache' => ['class' => CachePanel::class, 'title' => 'Cache operations', 'icon' => 'db', 'position' => 1], ], ],
The container resolves CacheCollector::class, so the same instance serves the application service and the capture.
Entry options:
class: the collector or panel class, required unless the value is a class string or, in Yii2, an instance.title: the panel title, defaulting to the panel'sTITLEconstant.icon: a Debug Core icon key, defaulting to the panel'sICONconstant.enabled: set tofalseto skip an entry without installing its package.position: order among extensions; unpositioned extensions follow alphabetically.
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.
A runnable version of this example, capturing through PSR-3 instead of a direct call, lives in tests/Support.
Presentation vocabulary
Five types are published for panel authors: CollectorInterface, Panel, PanelView, Tone, and ColumnStyle.
Everything a panel can display is a PanelView method, so an extension imports no value class and builds nothing 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);
Plain scalars and null become text. PanelView::text(), ::strong(), ::code(), ::preview(), ::badge(), and
::value() produce validated inline values accepted wherever a scalar is accepted. Methods with explicit value
validation reject invalid input with an InvalidArgumentException; arguments with incompatible declared types raise
PHP's native TypeError.
The host reads the finished description through summaryMetrics(), toolbarMetrics(), and blocks().
Those accessors return PHPForge\Debug\Presenter value objects: SummaryMetric, ToolbarMetric, and the blocks,
entries, and inline values behind them. A renderer narrows each value with instanceof over the sealed Block and
Inline unions, which static analysis proves exhaustive, and reads its public properties. Inline text carries a
TextStyle case and a table carries its ColumnStyle cases, so semantics reach the markup without parsing strings.
use PHPForge\Debug\Presenter\{HeadingBlock, ParagraphBlock, TableBlock}; $html = match (true) { $block instanceof HeadingBlock => $this->heading($block->title, $block->section), $block instanceof ParagraphBlock => $this->paragraph($block->content, $block->tone), $block instanceof TableBlock => $this->table($block->headers, $block->rows, $block->styles), // one arm per block; PHPStan reports a missing arm as an unhandled match value. };