Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ray Laravel Package

spatie/ray

spatie/ray sends debug output from any PHP app to Ray, Spatie's desktop debugging app. Dump arrays, HTML, queries, and Markdown, measure performance, pause execution, and inspect output with the same syntax you use across Laravel, JavaScript, and more.

View on GitHub
Deep Wiki
Context7

Getting Started

Ray is a desktop app plus a tiny PHP client. spatie/ray is the framework-agnostic client; in Laravel you normally pull in spatie/laravel-ray instead (it depends on this package and adds framework-aware helpers). Install, make sure the desktop app is running, then start dumping.

# Vanilla PHP / any framework
composer require spatie/ray --dev

# Laravel (adds query, model, event, job helpers + auto-discovery)
composer require spatie/laravel-ray --dev

Verify the app is listening (default host 127.0.0.1, port 23517) and send your first payload:

ray('hello from PHP');
ray($order->toArray());
ray($user)->green();          // colour the payload
ray($payload)->label('API response');

First use cases, in the order most developers hit them:

  1. ray($var) as a dd()/var_dump() replacement that doesn't kill the request.
  2. ray()->showQueries() in Laravel to watch SQL as the page renders.
  3. ray()->measure() around a slow block to find the hot path.
  4. ray($e) in an exception handler to inspect a throwable with its trace.

Where to look first:

  • https://myray.app/docs/php for the full method reference (the README deliberately defers to it).
  • config/ray.php after php artisan vendor:publish --tag=ray-config — that's where enable, host, port, remote_path, local_path, send_dumps_to_ray and send_log_calls live.
  • CHANGELOG.md when a method you remember isn't there — helpers like expandAll(), invade(), trace($offset, $limit) and the yellow()/grey() aliases all arrived in specific releases.

A minimal sanity check before you trust anything else:

ray()->newScreen('checkout flow');
ray(['step' => 'before charge', 'amount' => $amount]);

If that appears in the app, the transport is fine and any later problem is your code, not the plumbing.

Implementation Patterns

Keep the request alive. The core habit is replacing dd() with ray() and letting the request finish, so you can inspect several branches in one pass rather than re-running the page per variable.

ray($request->validated())->label('validated');
ray($order)->blue();
ray($totals)->green();

Use screens to organise noise. A single request that emits 40 payloads is unreadable; split it.

ray()->newScreen('Cart');          // visual separator in the app
ray($cart)->label('cart state');
ray()->clearScreen();              // wipe before the next phase

Reach for the typed payload methods when the value isn't a scalar. These are the ones that save real time in day-to-day work:

ray()->html($renderedBladeString);     // render HTML inside Ray
ray()->markdown($readmeContents);
ray()->file(storage_path('logs/laravel.log'));
ray()->image($pathToScreenshot);
ray()->table($rows, 'Recent orders');
ray()->diff($before, $after);
ray()->json($jsonString);
ray()->xml($xmlString);

Instrument timing instead of guessing.

$measure = ray()->measure();       // start
$this->expensiveReport();
$measure();                         // stop, shows duration

ray()->measure('db phase');
DB::transaction(fn () => $this->writeEverything());
ray()->measure('db phase');         // same label closes the timer

Trace with a slice when the stack is deep. trace() accepts offset and limit, so you can skip the framework frames that always appear at the top.

ray()->trace(limit: 10, offset: 3);

Pause when you actually need a breakpoint. Ray supports halting execution from inside the app; ray()->pause() is the "stop here but still let me look around" tool, and it's the closest thing to a real debugger without configuring Xdebug.

Inspect internals with invade(). Added in 1.37.0, it reaches into protected/private state without littering your class with temporary public accessors.

ray()->invade($service)->privateProperty;

Use the onBeforeSend / afterSendCallbacks hooks for cross-cutting concerns. You can register a closure that fires before each request is sent (1.40.0) and an after-send callback on sendRequest (1.48.0) — useful for stamping every payload with the current user, request ID or environment.

ray()->beforeSend(function () {
    // runs before the HTTP call to the app
});

Guard instrumentation in shared code. ray() should never be a hard dependency of a code path that runs in CI or production.

if (app()->isLocal() && config('ray.enable')) {
    ray($payload)->label('webhook');
}

Customise the client from an array rather than hard-coding config, e.g. in a service provider or a test bootstrap:

use Spatie\Ray\Settings\SettingsFactory;

$settings = SettingsFactory::createFromArray([
    'enable' => true,
    'host'   => 'host.docker.internal',
    'port'   => 23517,
]);

Clean up mechanically, not by memory. Two static-analysis helpers exist specifically for this:

  • A Rector rule (RemoveRayCallRector, improved in 1.44.1 to strip chained calls) that removes ray() calls across the codebase before merging.
  • A PHPStan rule that flags remaining ray() calls so they fail analysis instead of shipping.

Wire both into CI and the "debug statement leaked to main" class of bug disappears.

Gotchas and Tips

A silent no-op is the default failure mode. If the Ray app isn't running, or the port is wrong, ray() does nothing and prints nothing. Don't debug your code — check the app first, then the socket:

curl -X POST http://127.0.0.1:23517 -d '{"uuid":"test","payloads":[]}'

If that hangs or refuses, it's firewall/port/host config, not the package.

Docker, WSL and VMs break path mapping. Payloads arrive fine but "open file" links point at paths that don't exist on the host. Fix it in config/ray.php with remote_path / local_path, and set host to host.docker.internal (or the gateway IP on Linux) instead of 127.0.0.1.

Trial mode caps you at 20 messages per session. You will hit this mid-debug and mistake it for a crash. Licensed installs have no cap.

Loops will flood the app and slow the request. Each ray() call is an HTTP request (with cURL handle caching since 1.40.1, but still). Inside a loop, use a counter or gate on the iteration:

if ($i % 100 === 0) {
    ray($i)->label('progress');
}

ray()->count() is handy for "did this branch run at all?" without emitting a payload per iteration.

Large and recursive structures need care. Recursion handling and clipboard serialisation have both had bug fixes (1.37.5, 1.37.7, 1.41.3). Very deep objects can still truncate or bloat. Send a targeted slice ($model->only([...]), $collection->take(20)) rather than the whole graph.

send_dumps_to_ray and send_log_calls are double-edged. Redirecting dump() and Log:: into Ray is excellent for legacy code you can't edit — but it also silently changes what "no output" means, and can spam the app with framework log noise. Enable them deliberately, per-session.

Version skew between spatie/ray and spatie/laravel-ray causes confusing missing-method errors. The Laravel helpers (showQueries, showDuplicateQueries, model/event displays) live in laravel-ray; the base client doesn't have them. If a method "doesn't exist", you're probably looking at the wrong package's docs — or you need to composer update because it landed in a recent minor (showDuplicateQueries arrived in 1.40.1, for example).

Config changes are cached. In Laravel, a stale config:cache means your edited ray.php host/port is ignored. Clear it before chasing a connection problem.

Treat ray() as disposable from the first commit. Add the PHPStan rule on day one, run the Rector cleanup in the pre-merge checklist, and never let a ray() call into a --no-dev install path (--dev in composer require is the cheapest defence). Ray is a superb tool precisely because it's throwaway; the cost is entirely in forgetting to throw it away.

SettingsFactory::createFromArray is the extension point worth knowing. It's how you get a configured client outside Laravel's container — in a test suite bootstrap, a CLI script, or a package of your own that wants to send diagnostics without depending on the framework.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
laravel2580/idehelpers
delatbabel/viewpages
anavel/gettext
muhamadrezaar/highcharts
delatbabel/keylists
frankkessler/salesforce-laravel-oauth2-rest
anavel/foundation
wdmtech/enviricons
eii/laravel-captcha
muka/shape-reader
tdt/json
refinery29/php-cs-fixer-config
hotmeteor/eco-env
mvaliolahi/hydrate
wearede/tbcpay-php
parsnick/steak
thibaud-dauce/mattermost-php
multilinguals/apollo-client
antaresbr/collection
kr0lik/phpstan-rules