Skip to content
LibxaFrame
Packages 12 August 2026 6 min read

Libxa Toolkit 0.1.0: a profiler that works for JSON responses

A debug bar has nowhere to render on a JSON response, a redirect or a download. Toolkit reports through Server-Timing instead, which browsers already know how to display, and which cannot alter the response body.

Libxa Toolkit is out: a profiler, a dumper and generators for the classes the framework has no maker for.

composer require libxa/toolkit --dev

A profiler with nowhere to put a debug bar

The usual approach is a bar rendered into the page. It works well, right up until the response is JSON, or a redirect, or a file download, none of which have anywhere to put one. Which is to say: it works for the requests you were not debugging.

Toolkit reports through a header instead:

Server-Timing: request0;dur=42.11, database1;dur=8.02, db;dur=8.02;desc="3 queries", total;dur=42.30

Browsers already display Server-Timing natively in the network panel, so there is nothing to render and nothing to style. It works for every response type. And it cannot alter the response body, which means switching profiling on cannot change what your application returns, a property worth more than it sounds.

Spans you open, not everything

$result = tk_measure('render invoice', fn () => $renderer->render($invoice));

The cost is proportional to what you asked for. An unmeasured section shows up as a gap in the timeline rather than as time attributed to whichever span happened to be nearby, which is the failure mode of instrument-everything profilers: they are never wrong, just quietly misleading.

hrtime, not microtime

The system clock can move backwards. NTP corrections, virtual machine migrations, a laptop waking up.

microtime() follows it, so a span can end before it started and report a negative duration. Which sends whoever sees it looking for a bug in their own code, because a negative duration is obviously impossible and therefore obviously their fault.

hrtime() is monotonic. It only goes forward.

Closing a span that threw

public function measure(string $name, Closure $callback): mixed
{
    $this->start($name);

    try {
        return $callback();
    } finally {
        $this->stop($name);
    }
}

Without the finally, an exception leaves the span open and every span opened afterwards is nested one level too deep. The output becomes unreadable exactly when something has gone wrong, which is the only time anybody is reading it.

Off by default outside local

Left unconfigured, profiling follows the environment: on in local, testing and development, off everywhere else. Doing nothing gives you the safe answer.

A developer tool that leaves itself on in production leaks timings, memory figures and occasionally more than that to anyone who asks for a page.

A dumper that keeps types

var_dump is unreadable past two levels and print_r loses types, which is usually the thing being checked. In print_r, null, false, 0 and "" all look like nothing at all, and they behave nothing alike.

null
true
"" (0)
" " (1)
1.0
array(2) [
  "name" => "Ada" (3),
  "roles" => array(1) [
    0 => "admin" (5)
  ]
]

Every string reports its length, because "" and " " are otherwise identical on screen and are a genuinely common source of confusion. Whole floats print as 1.0 so they are not mistaken for the integer. NAN and INF are shown rather than silently breaking the output.

Private and protected properties are included, since the interesting state is almost always the state the class was trying to hide.

That last one had a real bug the tests caught. PHP mangles non-public property names with null bytes, and an anonymous class name contains one too. The first version printed it straight into the output as an invisible character that broke everything reading it downstream, including the terminal.

Helpers are prefixed tk_, so installing this cannot fatally redeclare a dump() your application already has.

Generators

php libxa toolkit:make service Billing/Invoice   # App\Services\Billing\InvoiceService
php libxa toolkit:make action SendInvoice        # App\Actions\SendInvoiceAction
php libxa toolkit:make dto InvoiceData           # App\DTO\InvoiceData
php libxa toolkit:make repository User           # App\Repositories\UserRepository

Services, actions, DTOs and repositories are structure rather than framework features, which is why the framework has no make: for them. They are still written by hand a hundred times per project, always slightly differently.

The suffix is added when missing and never twice, so service Invoice and service InvoiceService both produce InvoiceService. A project containing both InvoiceService and InvoiceServiceService would be worse than either.

DTOs come out final readonly, because a DTO you can modify after construction is a mutable array with extra steps. Actions get exactly one method: an action that grows a second one is a service, and having to rename it is the signal.

Every stub is compiled in the test suite

exec(sprintf('%s -l %s', escapeshellarg(PHP_BINARY), escapeshellarg($file)));

Each generated stub is written to disk and run through php -l. Generating code that does not parse is the one failure a generator must never have, and it is invisible until somebody opens the file, by which point they assume they broke it.

That check caught the repository stub declaring : ?array when Atlas actually returns stdClass. It parsed fine. It would have fataled the first time anyone called the method.

Keep reading