dumpio/client

PHP client for the Dumpio dump viewer — sends faithful, typed value dumps and structured messages (exceptions, queries, HTTP, logs, models, …) over HTTP.

0
Packagist stažení
0
Verzí
0.6.0
Aktuální verze
Závislosti
php >=8.0

Dumpio PHP client — complete guide

dumpio/client sends faithful, typed debug data from your PHP application to the Dumpio desktop viewer over the network. Think of it as a dd() / var_dump() that streams to a real UI instead of polluting your response — with full type fidelity, color flags, channels, exception parsing, SQL/HTTP/event timelines, and flood control for loops.

This guide explains every feature with runnable examples. For a one-page reference see ../README.md; for the underlying wire format see ../../BUILDING.md.


Table of contents

  1. How it works
  2. Requirements & installation
  3. Quick start
  4. Configuration
  5. Core concepts: dumps, flags, channels, labels
  6. var dumps — the flagship
  7. The fluent builder (dio, Dumpio::make)
  8. Flood control for loops
  9. Timing with the stopwatch
  10. Typed messages
  11. Global helper functions
  12. Laravel integration
  13. Symfony integration
  14. Recipes
  15. Troubleshooting
  16. Safety & production behavior
  17. API cheat sheet

1. How it works

Every call builds a small JSON envelope and POSTs it to the viewer at http://{host}:{port}/dumps (default localhost:21234), fire-and-forget.

Three properties matter:

  • Fire-and-forget. The client never waits for a meaningful response and never retries. If the viewer is closed, the dump is silently dropped.
  • It never breaks your app. Every public method is wrapped in a try/catch that swallows its own errors. A broken debug line must never take down a request.
  • Type-faithful. Values are serialized with Reflection, so visibility, class names, enums, typed/uninitialized properties and reference cycles survive the trip — you see the object as PHP sees it, not a flattened string.
1your code ──> Dumpio::dump()/dio()/... ──> JSON envelope ──HTTP POST──> Dumpio viewer

2. Requirements & installation

  • PHP 8.0+ (the package itself; tooling examples assume 8.1+ for enums).
  • The Dumpio desktop app running and listening (default localhost:21234).
  • ext-curl is used when available; otherwise the client falls back to a stream context, so no hard extension requirement.
1composer require dumpio/client --dev

Install it as a dev dependency. The client is a debugging tool; it should not ship enabled to production (see §16).

The global helpers (dio, dumpio, ddio, …) are autoloaded via Composer's files autoloader — no bootstrap needed.


3. Quick start

Start the Dumpio app, then:

1use Dumpio\Dumpio;
2 
3// 1. The simplest possible dump
4dio('hello from PHP');
5 
6// 2. A value with a label and color
7dio($user, 'current user')->green();
8 
9// 3. Dump-and-die (stops execution after sending)
10ddio($request->all());

Smoke-test without writing any PHP — pipe JSON straight into the viewer:

1echo '{"message":"hi","flag":"green"}' | nc localhost 21234 # TCP
2curl -XPOST localhost:21234/dumps -d '{"message":"hi"}' # HTTP

If something shows up in the viewer, you're wired correctly.


4. Configuration

The client reads its configuration from the environment on first use. Every option has a sensible default, so with a local viewer running you need zero configuration.

Environment variables

Variable Default Meaning
DUMPIO_HOST localhost Host the viewer listens on.
DUMPIO_PORT 21234 Port the viewer listens on.
DUMPIO_TOKEN "" Shared token; sent as X-Dumpio-Token if non-empty.
DUMPIO_DISABLE (unset) Set to any non-empty value to disable the client.

Runtime override

Dumpio::configure() merges over the current config. Call it once during bootstrap (Laravel/Symfony do this for you — see below).

1use Dumpio\Dumpio;
2 
3Dumpio::configure([
4 'host' => '127.0.0.1',
5 'port' => 21234,
6 'token' => 'secret',
7 'enabled' => true,
8 'timeoutMs' => 1500, // per-request timeout
9 'maxDepth' => 6, // serializer: nesting depth
10 'maxItems' => 100, // serializer: array/object children per level
11 'maxString' => 2000, // serializer: string length before truncation
12]);

The serializer limits (maxDepth / maxItems / maxString) bound how much of a large structure is walked, so a huge object graph can't stall a request or flood the viewer. Raise them deliberately when you need to see deeper.


5. Core concepts

Every dump shares the same envelope. You'll see these four ideas everywhere:

Flag (color)

A flag is a color label used to scan the timeline at a glance. The fixed set is:

1red yellow blue gray purple pink green

Pick them with the named builder methods (->red(), ->green(), …) or ->flag('purple'). Typed messages choose a sensible flag automatically (exceptions are red, queries purple, HTTP by status, logs by level).

Channel

A free-text string used to group and filter dumps in the viewer (e.g. auth, billing, query, jobs). Defaults to default. Set it with ->channel('billing') or the channel option on typed helpers.

Label / message

A short human title for the entry. For var dumps it's the label; for typed messages it's derived (the SQL, the URL, the event name) or set via options.

Caller (file:line)

The client automatically captures the first stack frame outside the SDK, so each dump is tagged with the exact file:line that produced it — no manual annotation needed. This is also what flood control keys on.


6. var dumps — the flagship

A var dump captures an arbitrary PHP value with full fidelity. There are three entry points, by ergonomics:

Entry point Returns Use when…
dio($x) a fluent builder you want to chain color/label/flood control
dumpio($x) $x itself you want to wrap an expression (tap-style)
Dumpio::dump($x) void you prefer the explicit static call

dio() — fluent, the default

1dio($user); // sends a blue var dump
2dio($user, 'current user'); // with a label
3dio($user)->green()->label('user')->channel('auth');

dio() returns a PendingDump builder. The dump ships when you call ->send() or automatically when the builder goes out of scope, so the trailing ->send() is optional. See the next section for the full chain.

dumpio() — tap-style passthrough

dumpio() sends the dump and returns the value unchanged, so it can wrap an expression inside a larger one without changing behavior:

1return dumpio($user, 'user'); // dumps, then returns $user
2$total = array_sum(dumpio($prices, 'prices'));

Why two functions? dio() returns a builder so you can chain; dumpio() returns the original value so you can tap. A single function can't do both (PHP can't proxy an arbitrary value), so the SDK splits the two roles cleanly.

ddio() — dump and die

1ddio($a, $b, $c); // dumps each argument, then exit(1)

Equivalent to Dumpio::dd($a, $b, $c). Use it like Laravel's dd() when you want execution to stop right there.

What fidelity you get

The serializer (Dumpio\Serializer) uses Reflection, matching the goal of Symfony's VarCloner without a hard dependency:

  • Scalarsnull, bool, int, float (including NAN/INF/-INF), and string (truncated past maxString, flagged truncated).
  • Arrays — keys preserved; bounded by maxItems per level and maxDepth.
  • Objects — class name plus every property with its visibility (public/protected/private). Uninitialized typed properties show as undefined rather than erroring.
  • Enums — pure and backed enums carry their name (and backing value), not internal machinery.
  • Closures / resources — rendered as their kind/type, not expanded.
  • Reference cycles — broken with ref nodes (an object seen twice points back instead of recursing forever).

Framework-aware shapes render their logical value instead of internals (detection is by class name, so the core stays framework-agnostic and these are inert when the framework is absent):

  • DateTimeInterface → a single formatted timestamp.
  • Eloquent Model → its casted, visible attributes (via attributesToArray()), including loaded relations — not the raw $attributes/$casts machinery.
  • Laravel Collection → its items as an array, tagged with the class.
1enum Status: string { case Active = 'active'; case Banned = 'banned'; }
2 
3class Account {
4 public int $id = 7;
5 protected Status $status = Status::Active;
6 private ?string $secret = null;
7}
8 
9dio(new Account());
10// Viewer shows: Account { +id: 7, #status: Status::Active('active'), -secret: null }

7. The fluent builder

dio($value) and Dumpio::make($value) both return a Dumpio\PendingDump. It is a chainable builder; the dump ships on ->send() or automatically when the builder is destroyed (end of statement, or end of scope if you assign it).

1dio($payload)->red()->label('webhook')->channel('stripe'); // auto-sends
2Dumpio::make($payload)->purple()->send(); // explicit send

Color methods

1->red() ->yellow() ->blue() ->gray() ->purple() ->pink() ->green()
2->flag('red') // or set any flag string directly

Metadata

1->label('order #42') // the entry title
2->channel('billing') // grouping/filtering bucket

Conditional sending

Only ship when a condition holds. A failed gate drops the dump and does not count toward once()/limit()/count():

1dio($payload)->when($request->boolean('debug'))->yellow(); // only if ?debug=1
2dio($cart)->unless($cart->isEmpty())->label('cart'); // skip empty carts

Idempotency

->send() is idempotent (calling it twice ships once). The destructor calls it for you, so the shorthand dio($x)->red(); is complete on its own.


8. Flood control for loops

Dumping inside a hot loop floods the viewer. Three modifiers tame it. By default they are keyed by call-site (file:line), per process:

1foreach ($rows as $row) {
2 dio($row)->once(); // only the FIRST iteration is sent
3 dio($row)->limit(5); // at most 5 are sent, then silence
4 dio($row)->count(); // ONE entry that live-updates with "×N"
5}
  • once() — send the first hit from this line, ignore the rest.
  • limit(int $max) — send at most $max hits from this line.
  • count(?string $name = null) — collapse every hit onto a single, live-updating entry in the viewer (it carries a dedupeKey and a running count). Ships immediately on each call.

Named counters

By default count() groups per call-site. Pass a name to share one counter across different call-sites — useful for counting a condition that fires in several places:

1foreach ($orders as $order) {
2 if ($order->isPaid()) dio($order)->count('paid');
3 if ($order->isRefund()) dio($order)->count('refunded');
4}
5// Two entries: "paid ×N" and "refunded ×M", regardless of how many lines feed them.

9. Timing with the stopwatch

Dumpio::stopwatch() returns a running timer (the equivalent of Ray's measure()). Call ->stop() for the final timing, or ->lap() for intermediate splits while it keeps running. Each call ships a measure dump with the elapsed milliseconds and the memory delta since start.

1use Dumpio\Dumpio;
2 
3$sw = Dumpio::stopwatch('import users');
4 
5$rows = parseCsv($path);
6$sw->lap('parsed'); // intermediate split, timer keeps running
7 
8User::insert($rows);
9$sw->stop('done'); // final timing + memory delta

If you already have a duration measured by other means, report it directly:

1Dumpio::measure('render dashboard', 84.2, ['memory' => 2_097_152]);

10. Typed messages

Beyond var dumps, the client ships structured message types the viewer renders with purpose-built UI (stack traces, SQL with bindings, request/response, tables, …). Each is a static method on Dumpio. Every $opts array accepts the envelope overrides flag, channel, and origin, plus the type-specific keys noted below.

exception() — structured errors

1try {
2 risky();
3} catch (\Throwable $e) {
4 Dumpio::exception($e, [
5 'request' => ['url' => $url, 'method' => 'POST'],
6 'user' => ['id' => 1, 'email' => 'a@b.c'],
7 ]);
8 throw $e;
9}

Sends the class, message, file/line, code, and a structured stack trace (file/line/function/class per frame). The optional $context array is shown alongside (request/user/database/…). Flag defaults to red. Pass ['framework' => 'laravel'] to help the viewer's parser.

query() — SQL

1Dumpio::query(
2 'select * from users where email = ? and active = ?',
3 ['a@b.c', 1],
4 1.8, // execution time in ms (optional)
5 ['connection' => 'mysql'] // optional
6);

Shows the SQL with its bindings and timing. Flag defaults to purple.

http() — request/response

1Dumpio::http('POST', 'https://api.stripe.com/v1/charges', 201, [
2 'headers' => ['Authorization' => 'Bearer …'],
3 'body' => ['amount' => 1999, 'currency' => 'usd'],
4 'responseTime' => 120, // ms
5]);

Flag is chosen from the status: ≥500 red, ≥400 yellow, ≥300 blue, else green (and blue when status is unknown).

log() — log lines

1Dumpio::log('warning', 'Login throttled', ['ip' => $ip, 'attempts' => 6]);

Flag from the level: error/critical → red, warning/notice → yellow, info → blue, else gray.

model() — a domain object

1Dumpio::model(\App\Models\User::class, $user->getAttributes(), [
2 'exists' => true,
3 'relations' => ['roles' => $user->roles->toArray()],
4 'connection' => 'mysql',
5]);

Renders a single record (Eloquent / Django / Prisma / struct) with its attributes and optional relations.

collection() — a list

1Dumpio::collection($users, ['message' => 'active users']);

table() — explicit columns and rows

1Dumpio::table(
2 ['id', 'name', 'role'],
3 [[1, 'Ada', 'admin'], [2, 'Linus', 'user']],
4 ['message' => 'team']
5);

measure() — one timing

1Dumpio::measure('cache warm', 42.0, ['memory' => 1_048_576, 'context' => ['keys' => 30]]);

See also the stopwatch, which produces these for you.

performance() — a metric bundle

1Dumpio::performance(
2 ['db_queries' => 12, 'cache_hits' => 30],
3 ['breakdown' => ['database' => 120.0, 'render' => 45.0], 'message' => 'request']
4);

event() — a business/domain event

1Dumpio::event('order.completed', [
2 'entity' => 'order',
3 'entity_id' => 42,
4 'actor' => ['id' => 1],
5 'data' => ['total' => 299.9, 'currency' => 'EUR'],
6]);

HTML and email previews — html() / mailable()

Capture rendered markup — a server-rendered page, a Blade/Twig fragment, or a whole email — and preview it live in the viewer. The markup rides in a dedicated html/text field (not the value serializer), so large bodies aren't clipped.

1// Any HTML fragment or full page.
2Dumpio::html($renderedBladeView, ['message' => 'Checkout page']);
3 
4// A rendered email with its envelope (subject / from / to / attachments).
5Dumpio::mailable($mailable); // Laravel Mailable: calls render()
6Dumpio::mail([ // or build the envelope yourself
7 'subject' => 'Welcome aboard',
8 'from' => 'App <no-reply@app.test>',
9 'to' => ['Ada <ada@example.com>'],
10 'html' => $html,
11 'text' => $text,
12 'attachments' => [['name' => 'invoice.pdf', 'size' => 20480, 'mime' => 'application/pdf']],
13]);
14 
15// From a Symfony Mime Email (what Laravel Mail & Symfony Mailer produce).
16Dumpio::mailFromSymfonyEmail($email);

The viewer renders the markup inside a fully sandboxed iframe (no scripts, no same-origin, no forms) and blocks all remote resources by default via a restrictive CSP — email tracking pixels and external CSS never fire silently. A per-dump Load remote resources toggle opts in; mail dumps add an envelope header and an HTML / Text / Source tab switch.

To capture outgoing mail automatically (no explicit call), enable listen_mail — see Laravel / Symfony integration below.


11. Global helper functions

These are autoloaded for convenience and are thin wrappers over Dumpio::*:

Helper Wraps Returns
dio($value, $label?, $flag?) Dumpio::make() builder
dumpio($value, $label?, $flag = 'blue') Dumpio::dump() $value
ddio(...$values) Dumpio::dd() never
dumpio_exception($e, $context = [], $opts = []) Dumpio::exception() void
dumpio_query($sql, $bindings = [], $timeMs?, $opts = []) Dumpio::query() void

Use whichever reads best. The static Dumpio::* API is always available too.


12. Laravel integration

The service provider is auto-discovered — no manual registration. It applies config('dumpio.*'), configures the static client, and (opt-in) wires automatic forwarding of queries, exceptions, models, cache, jobs and events.

Publish the config

1php artisan vendor:publish --tag=dumpio-config

config/dumpio.php:

1return [
2 'host' => env('DUMPIO_HOST', 'localhost'),
3 'port' => (int) env('DUMPIO_PORT', 21234),
4 'token' => env('DUMPIO_TOKEN', ''),
5 'enabled' => env('DUMPIO_ENABLED', env('APP_DEBUG', false)), // off in prod
6 
7 'listen_queries' => env('DUMPIO_LISTEN_QUERIES', false),
8 'listen_exceptions' => env('DUMPIO_LISTEN_EXCEPTIONS', false),
9 'intercept_dumps' => env('DUMPIO_INTERCEPT_DUMPS', false),
10 'listen_models' => env('DUMPIO_LISTEN_MODELS', false),
11 'listen_cache' => env('DUMPIO_LISTEN_CACHE', false),
12 'listen_jobs' => env('DUMPIO_LISTEN_JOBS', false),
13 'listen_events' => env('DUMPIO_LISTEN_EVENTS', false),
14 'listen_mail' => env('DUMPIO_LISTEN_MAIL', false),
15 'intercept_mail' => env('DUMPIO_INTERCEPT_MAIL', false),
16 'register_macros' => env('DUMPIO_REGISTER_MACROS', true),
17];

When enabled is false (the default in production) the provider configures the client as disabled and registers no listeners — a complete no-op.

Auto-instrumentation (opt-in)

Each switch is off by default; flip the env var to forward that signal:

Env var What it forwards
DUMPIO_LISTEN_QUERIES every executed SQL query via DB::listenquery dumps
DUMPIO_LISTEN_EXCEPTIONS reported exceptions → exception dumps
DUMPIO_INTERCEPT_DUMPS dump() / dd() routed into the viewer (via VarDumper::setHandler)
DUMPIO_LISTEN_MODELS Eloquent created/updated/deleted/restored → model dumps (models)
DUMPIO_LISTEN_CACHE cache hit/missed/written/forgotten → event dumps (cache)
DUMPIO_LISTEN_JOBS queue job processing/processed/failed → event dumps (jobs)
DUMPIO_LISTEN_EVENTS your application (non-framework) events → event dumps (events)
DUMPIO_LISTEN_MAIL every outgoing email → mail dumps with rendered preview (mail)
DUMPIO_INTERCEPT_MAIL with LISTEN_MAIL: also swallow the real send in dev (Mailpit-style)

With DUMPIO_LISTEN_MAIL=true, the provider hooks Laravel's MessageSending event and forwards every outgoing email to the viewer as a mail preview (rendered HTML/text + subject/from/to/attachments). Add DUMPIO_INTERCEPT_MAIL=true and the listener also cancels the real send — a dev-only Mailpit-style trap so your test emails land only in Dumpio and never reach a real inbox.

The facade

Auto-registered as the Dumpio alias:

1use Dumpio\Laravel\Facades\Dumpio;
2 
3Dumpio::query($sql, $bindings, $timeMs);

Chainable macros

->dio() and ->ddio() are registered on query builders and collections, so you can drop a dump mid-chain without breaking it — it ships the current state and returns $this:

1User::query()
2 ->where('active', true)
3 ->dio() // → query dump (SQL + bindings so far)
4 ->whereDate('created_at', today())
5 ->dio() // → query dump (with the extra clause)
6 ->get();
7 
8collect($users)->dio('after filter'); // → var dump, returns the collection

->ddio() is the dump-and-die variant. Disable both with DUMPIO_REGISTER_MACROS=false.


13. Symfony integration

Register the bundle (dev only is recommended):

1// config/bundles.php
2return [
3 // …
4 Dumpio\Symfony\DumpioBundle::class => ['dev' => true],
5];

It reads DUMPIO_HOST / DUMPIO_PORT / DUMPIO_TOKEN / DUMPIO_DISABLE from the environment and adds auto-instrumentation when the relevant component is present:

Condition What it forwards
always kernel.exceptionexception dumps with request context
Doctrine DBAL 4 every executed SQL → query dumps (database); SQL + timing (no bindings)
symfony/messenger Messenger sent/received/handled/failed → event dumps (messenger)
symfony/mailer + DUMPIO_LISTEN_MAIL every outgoing email (MessageEvent) → mail dumps with preview (mail)
DUMPIO_INTERCEPT_DUMPS dump() / dd() routed into the viewer (via VarDumper::setHandler)

Symfony cache pools have no built-in event system, so there is no automatic cache forwarding — call Dumpio::event('cache.…', …) yourself if you need it.

Unlike Laravel, Symfony's MessageEvent has no cancel hook, so there is no intercept_mail here — the subscriber only captures mail. To stop dev mail from actually going out, route it to a null:// transport (MAILER_DSN=null://null).

The static client and helpers work anywhere:

1\Dumpio\Dumpio::query($sql, $bindings, $timeMs);
2dumpio($entity, 'entity');

14. Recipes

Inspect one branch of a request

1dio($request->all(), 'payload')->channel('checkout')->yellow();

Count how often a condition fires

1foreach ($lines as $line) {
2 if ($line->isDiscounted()) {
3 dio($line)->count('discounted lines');
4 }
5}

Profile a block

1$sw = Dumpio::stopwatch('checkout');
2$cart = $this->buildCart($request); $sw->lap('cart built');
3$order = $this->placeOrder($cart); $sw->stop('order placed');

Debug only when a flag is present (safe in shared/staging)

1dio($internalState)->when(config('app.debug'))->channel('internals');

Tap inside an expression without changing the result

1return response()->json(dumpio($payload, 'response'));

Trace a query-builder chain

1$users = User::query()
2 ->where('team_id', $teamId)
3 ->dio() // see the SQL at this point
4 ->where('active', true)
5 ->ddio(); // see the final SQL, then stop

15. Troubleshooting

Nothing appears in the viewer.

  1. Is the Dumpio app running and listening on the configured port (21234)?
  2. Is the client enabled? In Laravel that means DUMPIO_ENABLED=true (or APP_DEBUG=true); globally it means DUMPIO_DISABLE is unset.
  3. Confirm transport independently: echo '{"message":"ping"}' | nc localhost 21234.
  4. Custom host/port? Make sure DUMPIO_HOST/DUMPIO_PORT match the viewer.

It works locally but not in Docker / a VM. localhost inside the container isn't your host. Point DUMPIO_HOST at the host (e.g. host.docker.internal), and make sure the viewer accepts non-loopback binds (it refuses remote binds unless its allowRemote security option is set).

The viewer shows a red raw entry. The payload wasn't valid JSON the viewer could parse. This is by design — unparseable input is surfaced, not dropped.

A token is configured. Set DUMPIO_TOKEN to the same value as the viewer; the client sends it as the X-Dumpio-Token header.

A dump is truncated. You hit maxDepth / maxItems / maxString. Raise them via Dumpio::configure([...]) for that run.


16. Safety & production behavior

  • Off in production by default. In Laravel, enabled derives from APP_DEBUG, so a normal production deploy sends nothing and registers no listeners. Globally, set DUMPIO_DISABLE=1 to force-disable.
  • Never throws. Every public method swallows its own errors; a missing viewer, a serialization edge case, or a network failure is silently ignored.
  • Bounded work. The serializer caps depth/items/string length, so even a huge object graph does limited work.
  • Install it as --dev and keep it disabled in production. If you must enable it on a shared environment, gate individual dumps with ->when(...).

17. API cheat sheet

1// var dumps
2dio($x)->green()->label('x')->channel('c'); // fluent builder (auto-sends)
3dumpio($x, 'x'); // returns $x (tap)
4ddio($a, $b); // dump & die
5Dumpio::dump($x, 'x', 'blue', 'channel');
6Dumpio::make($x)->red()->send();
7 
8// builder modifiers
9->red() ->yellow() ->blue() ->gray() ->purple() ->pink() ->green() ->flag('')
10->label('') ->channel('')
11->when($bool) ->unless($bool)
12->once() ->limit(5) ->count('name') ->send()
13 
14// timing
15$sw = Dumpio::stopwatch('name'); $sw->lap('split'); $sw->stop('done');
16Dumpio::measure('name', 84.2, ['memory' => 2_097_152]);
17 
18// typed messages
19Dumpio::exception($e, $context = [], $opts = []);
20Dumpio::query($sql, $bindings = [], $timeMs = null, $opts = []);
21Dumpio::http($method, $url, $status = null, $opts = []);
22Dumpio::log($level, $message, $details = [], $opts = []);
23Dumpio::model($class, $attributes, $opts = []);
24Dumpio::collection($items, $opts = []);
25Dumpio::table($columns, $rows, $opts = []);
26Dumpio::performance($metrics, $opts = []);
27Dumpio::event($event, $opts = []);
28 
29// config
30Dumpio::configure(['host' => '', 'port' => 21234, 'token' => '', 'enabled' => true]);

Flags: red yellow blue gray purple pink green. Default host/port: localhost:21234. Default channel: default.