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
- How it works
- Requirements & installation
- Quick start
- Configuration
- Core concepts: dumps, flags, channels, labels
vardumps — the flagship- The fluent builder (
dio,Dumpio::make) - Flood control for loops
- Timing with the stopwatch
- Typed messages
- Global helper functions
- Laravel integration
- Symfony integration
- Recipes
- Troubleshooting
- Safety & production behavior
- 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/catchthat 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-curlis 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 # TCP2curl -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 depth10 'maxItems' => 100, // serializer: array/object children per level11 'maxString' => 2000, // serializer: string length before truncation12]);
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 dump2dio($user, 'current user'); // with a label3dio($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 $user2$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:
- Scalars —
null,bool,int,float(includingNAN/INF/-INF), andstring(truncated pastmaxString, flaggedtruncated). - Arrays — keys preserved; bounded by
maxItemsper level andmaxDepth. - Objects — class name plus every property with its visibility
(
public/protected/private). Uninitialized typed properties show asundefinedrather than erroring. - Enums — pure and backed enums carry their
name(and backingvalue), not internal machinery. - Closures / resources — rendered as their kind/type, not expanded.
- Reference cycles — broken with
refnodes (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/$castsmachinery. - 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-sends2Dumpio::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 title2->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=12dio($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 sent3 dio($row)->limit(5); // at most 5 are sent, then silence4 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$maxhits from this line.count(?string $name = null)— collapse every hit onto a single, live-updating entry in the viewer (it carries adedupeKeyand a runningcount). 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 running7 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'] // optional6);
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, // ms5]);
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::listen → query 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.php2return [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.exception → exception 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
MessageEventhas no cancel hook, so there is nointercept_mailhere — the subscriber only captures mail. To stop dev mail from actually going out, route it to anull://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 point4 ->where('active', true)5 ->ddio(); // see the final SQL, then stop
15. Troubleshooting
Nothing appears in the viewer.
- Is the Dumpio app running and listening on the configured port (
21234)? - Is the client enabled? In Laravel that means
DUMPIO_ENABLED=true(orAPP_DEBUG=true); globally it meansDUMPIO_DISABLEis unset. - Confirm transport independently:
echo '{"message":"ping"}' | nc localhost 21234. - Custom host/port? Make sure
DUMPIO_HOST/DUMPIO_PORTmatch 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,
enabledderives fromAPP_DEBUG, so a normal production deploy sends nothing and registers no listeners. Globally, setDUMPIO_DISABLE=1to 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
--devand 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// timing15$sw = Dumpio::stopwatch('name'); $sw->lap('split'); $sw->stop('done');16Dumpio::measure('name', 84.2, ['memory' => 2_097_152]);17 18// typed messages19Dumpio::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// config30Dumpio::configure(['host' => '…', 'port' => 21234, 'token' => '…', 'enabled' => true]);
Flags: red yellow blue gray purple pink green.
Default host/port: localhost:21234. Default channel: default.