Search by

php-forge / debug

terabytesoftw

Framework-neutral contracts and declarative presentation models for debugger extensions.

Package info

github.com/php-forge/debug

pkg:composer/php-forge/debug

Statistics

Installs: 2 935

Dependents: 5

Suggesters: 0

Stars: 1

Open Issues: 0

0.3.0 2026-09-17 20:13 UTC

This package is auto-updated.

Last update: 2026-09-17 20:17:00 UTC


README

PHP Forge

Debug


PHPUnit Mutation Testing PHPStan Security

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's TITLE constant.
  • icon: a Debug Core icon key, defaulting to the panel's ICON constant.
  • enabled: set to false to 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.
};

Documentation

Package information

PHP PHPStan Level Max Latest Stable Version Total Downloads

Code quality

Codecov Quality StyleCI

Social networks

Follow on X

License

License