Dumpio

Dokumentace

dumpio/client is the official PHP package for sending dumps to the Dumpio app. It's the richest way to use Dumpio from PHP: instead of hand-writing JSON, you call a helper like dio($user) and the package builds the whole message for you — types, colors, labels, the file and line it was called from, even HTML and email previews.

This page is written to get you productive even if you've never used a tool like this. If you use Laravel, Symfony or Nette, read this page first, then see the dedicated PHP + Laravel, PHP + Symfony and PHP + Nette pages for the framework parts.

What is a "dump"?

A dump is a snapshot of a value in your running program, sent to the Dumpio window so you can look at it. It's the same idea as PHP's var_dump() or Laravel's dd() — except the data travels over the network to a separate app instead of being printed into your page or API response.

Why that's better for debugging:

  • It doesn't break your output. You can inspect a variable in the middle of a JSON API, a queued job, or a Livewire component without corrupting the response.
  • It keeps the types. You see that 42 is an int and "paid" is a string, and for objects you get the class name and which properties are public/protected/private.
  • It never crashes your app. Every call is wrapped in a try/catch. If the Dumpio app isn't running, the dump is quietly dropped — your code runs exactly as before.

Before you start

  1. The Dumpio app is running with its default server (HTTP on 127.0.0.1:21234). See Installation.
  2. PHP 8.0 or newer.

Install

composer require dumpio/client

That's it — no configuration needed while the Dumpio app is on your machine using the default port.

Tip: install it as a dev dependency (composer require --dev dumpio/client) if you only debug locally. The package is a safe no-op in production either way (see Production safety).

Your first dump

Put this anywhere in your code — a controller, a script, a test — and load the page or run the script:

use Dumpio\Dumpio;
$user = ['id' => 7, 'name' => 'Ada', 'roles' => ['admin', 'billing']];
Dumpio::dump($user);

Switch to the Dumpio window and you'll see a new entry at the top. Click it and the value is shown as an expandable, typed tree:

Nothing showing up? Jump to Troubleshooting at the bottom of this page.

The four ways to send a value

There are four entry points. They do the same core thing — send a value — but differ in what they return and whether they stop your script. Pick whichever fits the line you're editing.

use Dumpio\Dumpio;
// 1) The plain call
Dumpio::dump($user);
// 2) dio() — the fluent builder (recommended). Lets you chain color/label/etc.
dio($user)->green()->label('current user');
// 3) dumpio() — "tap". Sends the value AND returns it unchanged.
$total = dumpio($cart->total(), 'total'); // $total still holds the number
// 4) ddio() — "dump and die". Sends every argument, then stops the script.
ddio($request->all(), $user);
I want to… Use
Quickly show a value Dumpio::dump($x) or dio($x)
Add a color / label / channel dio($x)->red()->label('…')->channel('…')
Inspect a value without breaking a chain or expression dumpio($x) (it returns $x)
Stop the script right here and look ddio($x)
Send something structured (an exception, a query, an email…) the typed helpers below

dio(), dumpio(), and ddio() are plain global functions — they're autoloaded by the package, so you can call them from anywhere without a use statement.

The "tap" trick

Because dumpio() returns its argument, you can wrap any expression with it and see the value without changing what your code does:

return dumpio($service->build($request), 'built'); // dumps, then returns the value

Colors, labels, and channels

These three things help you find the dump you care about in a busy list.

  • Flag (color): one of red, yellow, blue, green, purple, pink, gray. Use it however you like — e.g. red for problems, green for success. In the viewer you can filter by color.
  • Label: the title shown in the list. Without one, the list shows a generic title.
  • Channel: a group name shown as #name. Handy for separating, say, #checkout dumps from #auth dumps. You can filter by channel too.
dio($order)
->green() // color
->label('new order') // title in the list
->channel('checkout'); // group

The fluent builder

dio() (and Dumpio::make()) return a builder you can chain on. The dump is sent automatically when the builder is done — either when you call ->send() or when it goes out of scope — so ->send() is optional.

dio($cart)
->purple() // or ->flag('purple')
->label('cart')
->channel('checkout')
->send(); // optional
  • Colors: red(), yellow(), blue(), gray(), purple(), pink(), green() (or flag('…')).
  • Text: label('…'), channel('…').
  • Conditions: when($bool) sends only if true, unless($bool) is the opposite.
dio($internalState)->when($debugMode); // sent only when $debugMode is true

How the entry looks in the viewer (display modifiers)

Four more builder methods are pure presentation hints: they don't change what you send, only how the viewer shows it.

dio($config)->expand(); // arrives fully open instead of two levels deep
dio($hugePayload)->collapse(); // arrives closed — open it only when you need it
dio($report)->large(); // render the detail panel bigger (->small() shrinks it)
dio($token)->hide(); // ship it, but keep it masked until you click to reveal
  • size('sm'|'md'|'lg') scales the detail panel; small() and large() are shorthands for sm/lg. Anything else is ignored.
  • expand() / collapse() decide how deep the value tree starts open. Without them you get the viewer's default (two levels).
  • hide() still sends the value: the list row gets a hidden badge and the detail panel shows a "Hidden value" placeholder you click to reveal. Good for noisy or sensitive payloads you still want on the timeline.

They chain with everything else, and an older viewer that doesn't know them simply ignores them:

dio($order)->red()->label('order')->large()->expand();

Debugging inside loops (flood control)

A dump inside a loop can send hundreds of entries and drown the viewer. These modifiers keep that under control. They're keyed to the exact line in your code (per process), so each call site counts on its own:

foreach ($rows as $row) {
dio($row)->once(); // send only the FIRST iteration
dio($row)->limit(5); // send at most 5
dio($row)->count(); // send ONE entry that shows "×N" and counts up
}

count() is the friendliest for loops: instead of N rows you get a single live-updating entry with a ×N badge. Pass a name to share one counter across several places: dio($x)->count('processed rows').

Timing a block

A stopwatch measures how long something takes and sends it as a measure dump (with elapsed milliseconds and the memory change):

$sw = Dumpio::stopwatch('import');
// … first part …
$sw->lap('parsed'); // a split time; the stopwatch keeps running
// … second part …
$sw->stop('done'); // the final time

Timing a request as a tree (spans)

A stopwatch measures one thing. A span measures a section of work and knows what it contains, so the viewer's request timeline becomes a tree instead of a flat list:

$span = Dumpio::span('OrderController@index', 'controller');
$db = Dumpio::span('load orders', 'db');
// … queries here appear under "load orders" …
$db->end();
Dumpio::span('render', 'view', fn () => $view->render()); // closure form
$span->end();
  • The second argument is a free-form category (db, view, http, …); it colours the bar, so the same kind of work reads the same everywhere.
  • Spans opened inside another become its children automatically — nothing has to be threaded through your code.
  • Prefer the closure form: it ends the span even when the work throws, and the exception continues on its way untouched. A span that ends out of order (or never) closes everything opened after it, so one mistake can't strand the rest.
  • Anything else you dump while a span is open — a query, a log line — shows under that span in the timeline.

Spans ride on the existing measure type, so they also show up as ordinary timing entries in the list.

Interactive breakpoint

Dumpio::pause() stops execution and blocks the request until you click Continue or Stop in the viewer — a breakpoint without a debugger.

Dumpio::pause('before charging the card');

Stop ends the script (exit(1)); Continue lets it proceed. It's a safe no-op when Dumpio is disabled or the app isn't running, works on loopback servers only (a network-exposed server continues immediately), and a viewer-side timeout auto-continues so a forgotten breakpoint never hangs forever.

Update in place

Dumpio::update($id, $value) sends a keyed dump: send again with the same $id and the viewer replaces the existing entry instead of adding a row — perfect for a live counter or a value that changes across a loop. Dumpio::remove($id) drops it.

foreach ($rows as $i => $row) {
Dumpio::update('import', ['done' => $i + 1, 'total' => count($rows)]);
// …
}
Dumpio::remove('import');

Typed helpers

So far we've dumped plain values. Dumpio also has typed helpers for common debugging targets — exceptions, SQL queries, HTTP calls, and more. Each one gets its own nicely formatted view in the app (see Dump types) and picks a sensible default color. They're all static methods on Dumpio and, like everything else, never throw.

Method What it's for Default color
Dumpio::exception(\Throwable $e, $context = [], $opts = []) An exception, with a parsed stack trace red
Dumpio::query($sql, $bindings = [], $timeMs = null, $opts = []) A database query (with its parameters and time) purple
Dumpio::http($method, $url, $status = null, $opts = []) An HTTP request/response (headers, body, responseTime) by status
Dumpio::log($level, $message, $details = [], $opts = []) A log line by level
Dumpio::model($class, $attributes, $opts = []) One ORM record (relations, exists, connection)
Dumpio::collection($items, $opts = []) A list of items (message)
Dumpio::table($columns, $rows, $opts = []) An explicit table
Dumpio::measure($name, $timeMs, $opts = []) One timing (memory, context)
Dumpio::performance($metrics, $opts = []) A bundle of metrics (breakdown, context)
Dumpio::event($event, $opts = []) A business/domain event (entity, actor, data, …)
Dumpio::trace(?$label = null, $opts = []) The current call stack ("how did we get here?") gray
Dumpio::memory(?$label = null, $opts = []) A memory snapshot (current/peak/limit) blue
Dumpio::html($html, $opts = []) Preview an HTML fragment in a safe sandbox
Dumpio::mail($mail, $opts = []) Preview an email (html, text, subject, to, …)
Dumpio::mailable($mailable, $opts = []) Render and preview a Laravel Mailable
Dumpio::json($json, $opts = []) A JSON document (string or any encodable value) as tree + source
Dumpio::xml($xml, $opts = []) An XML document (string, DOMDocument or SimpleXMLElement) as tree + source
Dumpio::image($url, $opts = []) An image — data: URI inline, remote URL behind an opt-in
Dumpio::notify($message, $opts = []) Raise a desktop notification (title optional) yellow

Automatic colors where they help: HTTP status → null/blue, ≥500 red, ≥400 yellow, ≥300 blue, else green; log level → error red, warning yellow, info blue, else gray.

Examples

// An exception with useful context — call it in a catch block
try {
$gateway->charge($order);
} catch (\Throwable $e) {
Dumpio::exception($e, ['order_id' => $order->id, 'user' => $user->email]);
throw $e; // re-throw; you only wanted to see it
}
// A database query with its bindings and time (ms)
Dumpio::query('select * from orders where status = ?', ['paid'], 12.4);
// An outgoing HTTP call — the color comes from the status
Dumpio::http('POST', 'https://api.stripe.com/v1/charges', 402, [
'responseTime' => 240,
'body' => ['error' => 'card_declined'],
]);
// A quick table
Dumpio::table(['id', 'status'], [[1, 'paid'], [2, 'pending']], ['message' => 'Orders']);
// "How did the code even reach this line?"
Dumpio::trace('unexpected branch');

For how each of these renders (and how exception parsing works), see Dump types and Exceptions.

Short global helpers

The most common typed helpers have plain global shortcuts, autoloaded like dio():

dumpio_exception($e, ['user' => ['id' => 1]]); // = Dumpio::exception(...)
dumpio_query('select * from users', [], 1.2); // = Dumpio::query(...)
dumpio_trace('reached checkout'); // = Dumpio::trace(...)
dumpio_memory('after import'); // = Dumpio::memory(...)

Your own dump types (macros)

When a domain object comes up again and again — an invoice, a cart, a state machine — register how it should be dumped once and call it by name:

Dumpio::extend('invoice', fn (Invoice $i) => [
'type' => 'invoice',
'message' => "Invoice {$i->number}",
'total' => $i->total(),
'lines' => $i->lines->toArray(),
]);
Dumpio::invoice($invoice); // or Dumpio::custom('invoice', $invoice)
Dumpio::custom('invoice', $invoice, ['flag' => 'green', 'channel' => 'billing']);
  • The callback returns the payload array to send. Anything the wire format allows is fair game; without an explicit type the macro's name is used.
  • The viewer renders an unknown type with the generic value view, so a macro works without touching the app.
  • A name that collides with a real helper (query, html, …) is refused, so a macro can never shadow the built-ins. Dumpio::hasMacro() and Dumpio::forgetMacros() round it out.
  • Like everything else here, a macro that throws or returns nonsense is swallowed — it can't break the app it's debugging.
  • A name that isn't registered throws (Dumpio::quey($sql)BadMethodCallException), the same as calling any undefined method. A typo in a debug helper should be loud, not a dump that silently never arrives. Use Dumpio::custom($name, …) when the name comes from a variable and may legitimately be unknown — that form stays silent.

Node and Python have the same registry: extend(name, maker) + custom(name, value) (has_macro/forget_macros in Python, hasMacro/forgetMacros in Node).

Recipes

Small, real-world patterns you'll reach for most.

// Inspect one branch of a request
dio($request->all(), 'payload')->channel('checkout')->yellow();
// Count how often a condition fires (one live-updating "×N" entry)
foreach ($lines as $line) {
if ($line->isDiscounted()) {
dio($line)->count('discounted lines');
}
}
// Profile a block with splits
$sw = Dumpio::stopwatch('checkout');
$cart = $this->buildCart($request); $sw->lap('cart built');
$order = $this->placeOrder($cart); $sw->stop('order placed');
// Debug only when a flag is present (safe on shared/staging)
dio($internalState)->when(config('app.debug'))->channel('internals');
// Tap inside an expression without changing the result
return response()->json(dumpio($payload, 'response'));

Configuration

You usually don't need to configure anything. When you do, either set environment variables or call Dumpio::configure([...]) once during boot:

Option Env var Default Meaning
host DUMPIO_HOST localhost Where the Dumpio app listens
port DUMPIO_PORT 21234 The port it listens on
token DUMPIO_TOKEN "" Shared token, if the app requires one
enabled DUMPIO_DISABLE (set it ⇒ off) true Master on/off switch
timeoutMs 1500 Network timeout in milliseconds
maxDepth / maxItems / maxString 6 / 100 / 2000 How deep/wide/long the value tree may get
Dumpio::configure([
'host' => '127.0.0.1',
'port' => 21234,
'token' => 'Dio-…', // only if the app has a token set
]);

The token is sent as the X-Dumpio-Token header. The package talks HTTP only.

Running your app in Docker or a VM? localhost inside the container isn't your host machine. Point DUMPIO_HOST at the host (often host.docker.internal) and make sure the Dumpio server accepts non-loopback connections.

What actually gets sent (the wire format)

You rarely need this, but it helps to know there's no magic. Each dump is a small JSON object with a standard envelope plus type-specific fields:

{
"timestamp": 1749900000000,
"flag": "blue",
"channel": "default",
"type": "var",
"language": "php",
"label": "current user",
"caller": { "file": "/app/Http/UserController.php", "line": 21, "function": "show" },
"value": {
"kind": "array",
"children": [
{ "kind": "int", "value": 7, "key": "id" },
{ "kind": "string", "value": "Ada", "key": "name" }
]
}
}

The serializer understands scalars, arrays, objects (with class and property visibility), enums, DateTime, Eloquent models, Laravel and Doctrine collections, Symfony UID (Uuid/Ulid), SplObjectStorage, and Stringable value objects (their __toString() is shown alongside the real properties), detects cyclic references, and trims by maxDepth/maxItems/maxString. The framework-aware shapes are detected by class name, so they stay inert when the framework isn't installed.

Production safety

The package is built to be safe to leave installed:

  • Off in production by default. The standalone client is enabled unless DUMPIO_DISABLE is set; under Laravel, enabled follows APP_DEBUG. A normal production deploy sends nothing.
  • Never throws. Every public method swallows its own errors — a missing app, a serialization edge case, or a network hiccup is silently ignored.
  • Bounded work. The serializer caps depth/items/string length, so even a huge object graph does limited work.

Using it with a framework

The framework integrations add automatic capture — queries, exceptions, emails, jobs, and more — with zero code changes, plus conveniences like chainable macros. They have their own pages:

  • PHP + Laravel — auto-discovered service provider, config/dumpio.php, the ->dio() macros, Monolog handler, Livewire, and more.
  • PHP + Symfony — the Dumpio bundle, Doctrine/Messenger/Mailer capture, and the exception subscriber.
  • PHP + Nette — the Dumpio DI extension, Nette Database queries, the Tracy logger bridge, mail/cache/Latte capture.

Troubleshooting

Nothing shows up.

  1. Is the Dumpio app running and listening on the configured port? Test it directly: curl http://127.0.0.1:21234/health should return {"ok":true,…}.
  2. Is the client enabled? Standalone: DUMPIO_DISABLE must be unset. Laravel: DUMPIO_ENABLED=true (or APP_DEBUG=true).
  3. Confirm the transport independently: echo '{"message":"ping"}' | nc localhost 21234.
  4. Custom host/port? Make sure DUMPIO_HOST/DUMPIO_PORT match the app.

A token is set. Give the client the same value with DUMPIO_TOKEN; it's sent as the X-Dumpio-Token header.

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

For app-side issues (403/413/429, the red raw entry, filters hiding dumps), see the app's Troubleshooting page.