nunomaduro/dig
Dig is a lightweight PHP package by Nuno Maduro that helps you dig into and inspect values in your applications, providing a simple API for clearer debugging and development workflows.
Settle what the package actually is before you install it. The registry title says "a beautiful debug tool for the command line"; the package documentation describes something different — a tiny, framework-agnostic utility for digging into values, including private and protected object members and nested structures. Those are two different products. Until you reconcile them, everything downstream is guesswork. The source is small enough to read in one sitting, so read it:
composer require --dev nunomaduro/dig
composer dump-autoload
composer show nunomaduro/dig
ls vendor/nunomaduro/dig/src
Open vendor/nunomaduro/dig/composer.json first. Look for the files autoload entry that registers a global function, and note the PHP constraint (the docs say PHP 8+). Then read the entry point — with a package this size there is exactly one, and it defines the entire API surface you will be living with.
Pin the constraint. "nunomaduro/dig": "^x.y" in require-dev. 43 stars, zero dependents, one maintainer — treat every minor as potentially breaking and let Composer tell you.
First use case — stop writing reflection boilerplate in tests. The day-to-day win for a Laravel developer is reaching into an object's internals that were deliberately not exposed, without ReflectionProperty::setAccessible() noise:
// shape of the call — confirm exact signature against vendor/nunomaduro/dig/src
$state = dig($this->service, 'somePrivateProperty');
$this->assertSame('expected', $state);
If you find yourself writing a public function getInternalState() getter purely so a test can assert on it, that is the exact moment this package is supposed to pay for itself.
Use it as a test-only inspection layer, not as application code. The package's own framing — "ideal for unit tests and debugging" — is the correct scope. The pattern that holds up is: exercise the public API, then dig into the private state to assert what the public API does not expose.
public function test_import_batches_are_chunked(): void
{
$importer = app(Importer::class);
$importer->handle($this->fixture());
$this->assertCount(4, dig($importer, 'batches'));
}
Reach into third-party and vendor objects on purpose, briefly. A GuzzleHttp\Client's resolved config, a Illuminate\Queue\Worker's internal state mid-job, a package manager's private registry — these are the cases where you would otherwise write five lines of reflection. Dig it, log the answer in a scratch test, delete the test. Do not leave it in the suite.
Wrap it behind one project-local helper. Even dev tooling needs a single point of removal and a single grep target:
// tests/Support/inspect.php
function inspect(object $target, string $path): mixed
{
return dig($target, $path);
}
Now composer remove nunomaduro/dig plus deleting one file is the entire reversal. Without the wrapper, adoption is scattered across dozens of test files.
In artisan tinker, use it for exploration, not assertion. Tinker is the natural home for a non-halting dig — you are already in a REPL, you already have a container resolved, and you can inspect a live object graph without writing a throwaway command. Combine with data_get() for arrays and dig() for objects rather than fighting one tool into the other's job.
Prefer it over adding getters, and prefer assertions over dumps. The temptation with any inspection tool is to print things. In a test suite, printing is noise. Dig, assertSame(), move on. If you want to see the value mid-run, use Pest's expect()->dd() in a scratch run and delete it before commit.
If it genuinely is a terminal renderer, keep it out of the request lifecycle. Everything Ignition, Telescope, Clockwork, and Ray do for HTTP requests is a solved problem by more mature tools. Any value here is on the surfaces they cannot see: a four-minute php artisan import:run, a queue worker, a Laravel Zero binary you are building. If you find yourself adding it to a controller, stop.
No service provider, no config, no publish step. The package is framework-agnostic and dependency-free by design. There is nothing to register in AppServiceProvider and nothing to add to config/. Keep it that way — the moment you write an integration layer, you have taken on maintenance of a package you were only trialing.
The identity contradiction is the number one risk. The description says "beautiful debug tool for the command line"; the documentation describes value digging into private and protected members. Before you scope a spike, an estimate, or a usage pattern, resolve this against the source. If it turns out to be a terminal renderer, half of the patterns above do not apply; if it turns out to be a value digger, the terminal claims do not. Either way, treat the inconsistency as a maturity signal.
Private member access is not a contract. Every dig($obj, 'somePrivate') is a bet that a vendor or teammate will not rename that property. Laravel's own internals change between minor releases. Pin Laravel, pin the package, and accept that some digs will silently return null after an upgrade — write the assertions tightly enough that a null fails loudly.
Know which layer you are looking at with Eloquent. Digging into a model almost certainly bypasses accessors, mutators, and casts and hands you raw $attributes. That is sometimes exactly what you want and sometimes actively misleading. If you need the cast value, ask the model; if you need the storage truth, dig.
Watch for magic and lazy loading. __get overloads, uninitialized typed properties, readonly properties, and lazy-loading proxies all behave differently under direct inspection than through the public API. A dig that returns null may mean "the property exists and is uninitialized," not "the property does not exist." Verify before you file a bug against the package.
Global helper collisions are a real failure mode. If the package registers a files-autoloaded function, it loads in every process of the app, unconditionally. Grep before installing:
rg -n 'function dig\b' app/ tests/ database/ config/
A redeclaration fatal that only appears in dev, in one branch, is a miserable afternoon.
Never point it at secrets, tokens, or PII. Nothing in the documentation suggests redaction. Whatever comes out lands in scrollback, shared screens, screen-shares, and CI logs. Assume everything printed is public.
Keep it out of production builds and gate CI. composer install --no-dev in production plus a grep gate scoped to application code — not tests, where legitimate use lives:
rg -n '\bdig\(' app/ routes/ config/ database/ && exit 1 || exit 0
If it renders to a terminal, non-TTY contexts will bite you before anything else. Piped output, CI runners, Docker without -t, some SSH sessions, and Windows terminals that mishandle ANSI or box-drawing. If output is captured under ob_start() or inside an Artisan command holding its own buffer, the probe may render nowhere. Test your actual Docker and remote-SSH setup during the spike — garbled output is a hard blocker, not a nuisance.
When something does not work, debug in this order: is the package in vendor/ → did composer dump-autoload register the files entry → is output buffered or the context non-TTY → is there a helper collision. That sequence covers nearly the entire failure surface of a package this small.
Plan the exit while you are still in the honeymoon. No support contract, no security-response process, no LTS, one maintainer, zero dependents. The removal cost is composer remove nunomaduro/dig plus deleting your wrapper — measured in minutes, which is precisely why a time-boxed trial is cheaper than a debate. Set a revisit trigger (a stable 1.x, a second maintainer, a real dependent count) rather than standardizing on it now, and keep dump() / dd() / VarDumper as the named fallback in the same sentence where you document this tool.
How can I help you explore Laravel packages today?