You add dd($user) inside a Livewire action to see what is happening, and the whole round-trip dies with a raw dump where your component used to be. Swap it for dump() and now your JSON API response has an array printed above it, breaking the payload. Both tools fight the request. Spatie Ray fixes this for Laravel by sending debug output to a separate desktop app: your page keeps rendering, your API keeps returning clean JSON, and every query, event and model lands in a scrollable window next to your editor.
Why dd() and dump() break Livewire and API responses#
dd() halts execution and dumps to the response body. dump() is gentler but writes into the same output buffer as your page. In a plain Blade controller that is survivable. Inside a Livewire round-trip it is not: Livewire sends back an HTML diff over an AJAX request, and a dump() in the middle corrupts that payload, so the component silently stops updating. JSON APIs have the same problem — anything echoed before response()->json() makes the body invalid JSON.
Ray sidesteps all of it by shipping the payload out of band to a desktop app over a local socket. If you have reached for Telescope, Debugbar or Pulse before, Ray lives in the same space but answers a different question: not "what did my whole app do over the last hour" but "what is this variable, right now, on this request".
Install Spatie Ray and send your first payload#
Install the package with Composer:
composer require spatie/laravel-ray
Installing it as a normal dependency (no --dev) is deliberate: the package only transmits when your environment is local/dev, so a ray() call you forget to delete will not break production. Add --dev only if you are disciplined about stripping calls before deploy — more on that in the gotchas.
Now send anything to the app. ray() accepts strings, arrays, objects, models — whatever you throw at it:
ray('Checkout reached'); // a breadcrumb
ray($user); // full model, attributes and relations
ray($cart, $request->all()); // multiple arguments become separate messages
Download the desktop app, keep it open, and those payloads appear instantly without touching your page.
See every Eloquent query Ray executes#
This is where Ray earns its place. Call showQueries() and every query that runs afterwards streams into the app with its bindings and execution time:
ray()->showQueries();
// This query — and its bindings — show up in Ray.
User::firstWhere('email', 'john@example.com');
Turning on queries globally is noisy. Pass a closure to scope it to exactly the code you care about, and add a return type if you want the result handed back:
$users = ray()->showQueries(function (): \Illuminate\Support\Collection {
return User::with('orders')->get(); // only this block is logged
});
Seeing the query count for a block is the fastest way to catch an N+1 problem — if a loop fires 50 queries, Ray shows all 50. Once you have spotted it, ray()->countQueries(fn () => ...) gives you a total and a runtime, and ray()->showSlowQueries(100) flags only queries over 100ms. For a permanent fix rather than a one-off look, pair this with Eloquent strict mode to fail loudly on lazy loading. You can also tag a single builder inline:
User::query()
->where('email', 'john@example.com')
->ray() // dumps the SQL as it is built
->first();
Debug Livewire components with Spatie Ray#
Livewire is where the "don't break the response" property matters most. Drop ray($this) into a component's render() method and you get the entire component state — every public property, its wire model bindings, the lot — on each render, without disturbing the returned HTML:
public function render()
{
ray($this); // full component state, every round-trip
return view('livewire.search', [
'quotes' => $this->getQuotes(),
]);
}
Send a single property instead and Ray updates live as you type, because render() re-runs on every interaction:
public function render()
{
ray()->clearScreen(); // wipe the previous render's output
ray($this->searchingFor); // watch the value change in real time
return view('livewire.search', ['quotes' => $this->getQuotes()]);
}
clearScreen() keeps the window showing only the latest state rather than a scroll of every keystroke. If you would rather not touch the class, the @ray Blade directive does the same from the view: @ray($searchingFor). Combine this with showQueries() and you can watch a search component's queries fire as the user types — the moment to reach for computed properties with persisted caching to stop re-running heavy queries.
Make a Ray session readable: color, label, pause#
Once you are sending real volume, a wall of identical grey messages is useless. Ray's fluent methods make a session skimmable. Label and colour payloads so the important ones stand out:
ray($order)->label('Checkout')->red();
ray($response)->label('Gateway')->green();
A few more that pull their weight day to day:
ray()->measure(); // start a timer
$this->runExpensiveThing();
ray()->measure(); // logs elapsed time + peak memory since the last call
ray()->pause(); // halt execution; resume from the Ray app
ray($value)->once(); // send only the first iteration inside a loop
ray()->count(); // how many times this line ran
pause() is the civilised replacement for a dd() breakpoint: execution stops, you inspect, then hit continue in the app instead of re-running the whole request. For tailing what is happening server-side at the same time, Ray pairs nicely with real-time log tailing via Laravel Pail — Ray for structured payloads, Pail for the log stream.
Gotchas and edge cases#
The big one is the install mode. With composer require spatie/laravel-ray --dev, the ray() function does not exist in a --no-dev production build, so any call you left behind throws a fatal error on deploy. Either install it as a normal dependency (the package stays silent outside dev) or wire a check into CI to catch stray ray() calls before they merge.
The free desktop app caps you at 20 messages per session, so a showQueries() on a request that runs hundreds of queries will hit the ceiling fast. Scope query logging to a closure, or use Ray::rateLimiter()->max(10) to cap output when debugging tight loops.
The query, event, job and model helpers (showQueries(), showEvents(), model()) come from spatie/laravel-ray specifically, not the framework-agnostic core — if you are debugging inside an Orchestra/Testbench package test suite you have to register Spatie\LaravelRay\RayServiceProvider yourself before they work. And Ray only transmits while the desktop app is running; a closed app means calls quietly no-op, which is the behaviour you want but can confuse you when "nothing shows up".
Wrapping up#
Install Ray as a normal dependency, keep the desktop app open, and reach for showQueries() and ray($this) the next time a Livewire component or an API endpoint misbehaves — you get the visibility of dd() without killing the request. Start with query logging on your slowest page; it usually surfaces an N+1 within seconds.
From here, wire stray-call detection into your pipeline with a one-command Duster, Pint, PHPStan and Rector setup, and when a bug does make it to a stack trace, lean on PHP 8.5's fatal error backtraces to read what actually blew up.
FAQ#
What is Spatie Ray and how does it work with Laravel?
Ray is a desktop debugging app from Spatie paired with the spatie/laravel-ray package. Instead of printing debug data into your HTTP response, the ray() helper sends it over a local socket to the standalone app, where it is displayed in a clean, scrollable, colour-coded window. In Laravel it hooks into the framework to also log queries, events, jobs, cache activity and model state on demand.
How is Ray different from dd() and dump()?
dd() halts the request and dump() writes into the response body, so both interfere with the page you are debugging — fatal inside Livewire round-trips and JSON APIs. Ray sends output to a separate app instead, so the response is never touched and your component keeps updating or your API keeps returning valid JSON. You also get history: previous payloads stay on screen rather than vanishing on the next request.
How do I log Eloquent queries to Ray?
Call ray()->showQueries() and every subsequent query appears in Ray with its bindings and execution time. To avoid noise, pass a closure so only the queries inside that block are logged, and add a return type to have the result returned. For performance work, ray()->countQueries() totals the queries in a block and ray()->showSlowQueries(100) reports only those over a threshold in milliseconds.
Can Ray debug Livewire components?
Yes. Add ray($this) inside a component's render() method to dump the full component state on every render, or send a single property like ray($this->searchingFor) to watch it change live as the user interacts. Because it never writes to the response, it does not break Livewire's AJAX round-trip the way dump() does. You can also use the @ray Blade directive to send values straight from the component's view.
Is it safe to leave ray() calls in my code?
It depends how you installed it. As a normal (non --dev) dependency, the package only transmits in your local environment and stays silent in production, so a forgotten call will not break anything. If you installed it with --dev, the ray() function is absent from a --no-dev production build and any leftover call throws a fatal error — so remove them before deploying, ideally with an automated check in CI.