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

Laravel Web Tinker Laravel Package

spatie/laravel-web-tinker

Adds a browser route to your Laravel app for running Artisan tinker without the terminal. Includes light and dark modes, but executes arbitrary code, so use it only as a dev dependency and never in production or with real data.

View on GitHub
Deep Wiki
Context7

Getting Started

Install it as a dev-only dependency and publish the front-end assets — the second command is not optional, the page renders unstyled/blank without it.

composer require spatie/laravel-web-tinker --dev
php artisan web-tinker:install

Then visit /tinker on your local app. The default config gates the whole thing on env('APP_ENV') === 'local', so there is nothing else to configure for a first run.

First use case: stop reaching for dd() in a controller and reach for the browser instead. Typical day-one moves:

// Inspect a model shape before writing a resource class
App\Models\Order::latest()->first()->toArray();

// Sanity-check query output
App\Models\Order::where('status', 'pending')->whereDate('created_at', today())->count();

// Verify a relationship / scope actually does what you think
App\Models\User::find(1)->orders()->where('paid', true)->toSql();

Anything the terminal tinker accepts works here, and you get a draggable editor, autofocus, and a scrollable output pane — which makes multi-line snippets far less painful than copy/pasting into an interactive shell.

Where to look first:

  • config/web-tinker.php after php artisan vendor:publish --provider="Spatie\WebTinker\WebTinkerServiceProvider" --tag="config" — path, theme, enabled, middleware, output modifier, PsySH config.
  • php artisan route:list | grep tinker to confirm the route and its middleware in your app.
  • Spatie\WebTinker\OutputModifiers\OutputModifier — the only real extension point in the package.

Implementation Patterns

Per-environment paths. Give staging and local distinct paths so you never confuse the two, and so a bookmarked URL doesn't point at the wrong app:

// config/web-tinker.php
'path' => env('WEB_TINKER_PATH', '/tinker'),

Authorizing outside local. The package's own advice is "don't," but a sandboxed staging box is a legitimate exception. Two steps, both required:

// AuthServiceProvider::boot()
Gate::define('viewWebTinker', fn ($user = null) => $user?->isAdmin() ?? false);
// config/web-tinker.php
'enabled' => env('APP_ENV') === 'local' || env('WEB_TINKER_ENABLED', false),

The Authorize middleware is already in the default middleware stack, so the gate is the only missing piece — but see the CSRF note below before you flip enabled.

Tightening middleware for non-local use. The middleware array is fully replaceable, which is the documented escape hatch. Add real auth and CSRF rather than relying on the gate alone:

'middleware' => [
    Illuminate\Cookie\Middleware\EncryptCookies::class,
    Illuminate\Session\Middleware\StartSession::class,
    Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class,
    'auth',
    Spatie\WebTinker\Http\Middleware\Authorize::class,
],

Keep EncryptCookies and StartSession — they were added deliberately in 1.6.0 because the gate needs a session to resolve $user, and removing them silently breaks authorization.

Output modifiers as a redaction layer. This is the most underused feature. Instead of the default PrefixDateTime, write a modifier that scrubs what you don't want in your browser history, screen shares, or (if you enabled staging) logs:

namespace App\Support;

use Spatie\WebTinker\OutputModifiers\OutputModifier;

class RedactSecrets implements OutputModifier
{
    public function modify(string $output = ''): string
    {
        $output = preg_replace('/(password_hash|remember_token)"?\s*=>\s*"[^"]*"/i', '$1 => "[redacted]"', $output);

        return "[" . now()->toDateTimeString() . "]\n" . $output;
    }
}
'output_modifier' => App\Support\RedactSecrets::class,

PsySH config for a consistent shell. If you have global PsySH settings (history size, pager, formatters), point the package at a project-relative file so the browser shell behaves like your terminal:

PSYSH_CONFIG=psysh.php   # in .env.local

Day-to-day workflows that pay for the install:

  • Faking outbound side effects before reproducing a bug — Mail::fake(), Notification::fake(), Queue::fake() then firing the real job and inspecting Mail::assertSent(...). Note that fakes only live for the duration of one submission.
  • Running artisan from the browser and reading the buffer: Artisan::call('migrate:status'); Artisan::output();
  • Verifying a queued job synchronously: set QUEUE_CONNECTION=sync and dispatch it, then inspect the resulting DB row.
  • Pasting an Eloquent query straight out of a failing test to see what the database actually returns.
  • Checking config('services.x') / env() resolution without bouncing the app.

Keep your reusable snippets in a versioned scratch file (e.g. database/tinker-scratch.php) rather than relying on the browser — see the persistence gotcha below.

Gotchas and Tips

Variables do not persist between submissions. Each execution is a fresh shell instance, so $user = User::find(1); on one line and $user->orders; on the next will not work. Write each snippet as a single self-contained block, or use ;-separated statements in one submission. This is the single most common source of "why is it undefined" confusion.

dd() kills the response. It terminates the request and you get nothing back in the pane. Use dump() — or just let the last expression be the return value, which the shell renders for you.

Never let this reach production, and mean it. It executes arbitrary PHP. composer require --dev plus the APP_ENV === 'local' default is the whole safety model. If you override enabled, do it via a dedicated env var that is absent from production .env files, not by hardcoding true.

Config caching can bake the "enabled" flag. Because enabled is computed with env() at config-load time, a config:cache run on a machine where APP_ENV=local writes 'enabled' => true into the cached config, and that file may then be deployed. Treat bootstrap/cache/config.php as a secret when this package is installed, and always run php artisan config:clear after changing path, theme, or enabled.

No CSRF middleware by default. The shipped stack is deliberately minimal (cookies, session, authorize) and omits VerifyCsrfToken and any auth middleware. On localhost that's fine; the moment you widen access, add both — see the middleware pattern above.

Publish assets again after upgrading. web-tinker:install copies the compiled JS/CSS into public/vendor/web-tinker. If you upgrade and the UI looks broken or output isn't styled as <pre>, you're running stale assets. Re-run the install command and hard-refresh.

Pin a recent version. Several fixes are directly relevant to daily use: 1.10.2/1.10.3 fixed hanging requests in Docker (custom execution loop replaced with PsySH's built-in Shell::execute()), 1.10.4 forces non-interactive mode and properly writes return values and exceptions, 1.9.1 and 1.10.0 addressed HTML injection and client-side output sanitizing. ^1.10.4 is a sane floor. Check the changelog before assuming a weird output is your code.

Comments are stripped before execution. Since 1.3.1 the input is cleaned of comments before being handed to PsySH. Usually harmless, but a // or # sequence inside a string or a heredoc can bite you — if a snippet behaves differently here than in a test, remove the comments first.

PsySH write-permission errors. "Writing to /var/www/.config/psysh is not allowed" appears when HOME isn't writable (Docker, CI-like local containers). Fixed in 1.4.2 for the default path, but reintroduced if you set PSYSH_CONFIG to an unwritable location. Point it at something inside the project, or ensure the container user owns $HOME.

Large outputs are a footgun. User::all() in the browser dumps PII into your history, your screen share, and possibly your reverse-proxy logs on staging. Always ->take(5) or ->select('id','email'), and lean on the output modifier as a backstop.

Exceptions inside an output modifier break the whole pane. The modifier returns string; if it throws (bad regex, null from a helper), you lose the output entirely rather than seeing a partial result. Keep modifiers dumb and side-effect-free.

Changing path needs no asset republish — the path is passed to the front end at render time (fixed in 1.4.5) — but it does need config:clear if you're cached. Also confirm subdirectory installs work; the Vue layer had a subfolder fix in 1.7.2 and path handling remains the most fragile config key.

Extension points, in order of usefulness: OutputModifier (redaction, timestamping, logging output), the middleware array (auth/CSRF/throttling), PsySH config_file (shell behavior), then path/theme. The theme (auto/light/dark) uses prefers-color-scheme; auto was broken pre-1.3.2, so don't debug your browser if the toggle misbehaves on an old install.

Treat it as a scratchpad, not a stateful REPL. The lack of cross-request state is a design consequence of HTTP execution, not a bug — plan snippets accordingly and keep anything worth keeping in a file.

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.
hotmeteor/eco-env
mvaliolahi/hydrate
wearede/tbcpay-php
parsnick/steak
thibaud-dauce/mattermost-php
multilinguals/apollo-client
antaresbr/collection
kr0lik/phpstan-rules
loco/loco
starfederation/datastar-php
yupmin/modern-phystrix
stephenjude/extended-artisan-commands
lumphp/laravel-macroable
lumphp/laravel-collections
lumphp/laravel-contracts
sermepa/sermepa
arkecosystem/fortify
arkecosystem/ui
owowagency/laravel-resources
gtjamesa/php-standards