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
42is anintand"paid"is astring, and for objects you get the class name and which properties arepublic/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
- The Dumpio app is running with its default server (HTTP on
127.0.0.1:21234). See Installation. - PHP 8.0 or newer.
Install
composer require dumpio/clientThat'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 callDumpio::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 valueColors, 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,#checkoutdumps from#authdumps. You can filter by channel too.
dio($order) ->green() // color ->label('new order') // title in the list ->channel('checkout'); // groupThe 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()(orflag('…')). - Text:
label('…'),channel('…'). - Conditions:
when($bool)sends only if true,unless($bool)is the opposite.
dio($internalState)->when($debugMode); // sent only when $debugMode is trueHow 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 deepdio($hugePayload)->collapse(); // arrives closed — open it only when you need itdio($report)->large(); // render the detail panel bigger (->small() shrinks it)dio($token)->hide(); // ship it, but keep it masked until you click to revealsize('sm'|'md'|'lg')scales the detail panel;small()andlarge()are shorthands forsm/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 ahiddenbadge 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 timeTiming 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 blocktry { $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 statusDumpio::http('POST', 'https://api.stripe.com/v1/charges', 402, [ 'responseTime' => 240, 'body' => ['error' => 'card_declined'],]);// A quick tableDumpio::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
typethe 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()andDumpio::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. UseDumpio::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 requestdio($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 resultreturn 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?
localhostinside the container isn't your host machine. PointDUMPIO_HOSTat the host (oftenhost.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_DISABLEis set; under Laravel,enabledfollowsAPP_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.
- Is the Dumpio app running and listening on the configured port? Test it directly:
curl http://127.0.0.1:21234/healthshould return{"ok":true,…}. - Is the client enabled? Standalone:
DUMPIO_DISABLEmust be unset. Laravel:DUMPIO_ENABLED=true(orAPP_DEBUG=true). - Confirm the transport independently:
echo '{"message":"ping"}' | nc localhost 21234. - Custom host/port? Make sure
DUMPIO_HOST/DUMPIO_PORTmatch 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.