laravel/tinker
Laravel Tinker is a powerful REPL for the Laravel framework, letting you interact with your entire application from the command line—evaluate PHP, run queries, and test code in a live environment via Artisan.
Tinker is almost certainly already installed — it ships in require-dev of the Laravel skeleton — so the first step is usually just opening the shell:
php artisan tinker
You get a PsySH prompt (>) with your entire application bootstrapped: the container, Eloquent, facades, config, and every registered service provider. A good first session:
> app()->version()
> App\Models\User::count()
> $u = App\Models\User::with('posts')->first()
> $u->toArray()
> DB::table('users')->latest()->limit(5)->get()
Note that App\Models\User resolved without a use statement — Tinker's ClassAliasAutoloader registers your config/app.php aliases (and vendor aliases) so classes are reachable by short name. The second thing to try is the non-interactive mode, which is where most day-to-day automation lives:
php artisan tinker --execute="echo App\Models\User::count();"
Where to look first when something is unclear:
Laravel\Tinker\Console\TinkerCommand for the real option surface (--execute, --include) and the artisan-command confirmation logic.Laravel\Tinker\ClassAliasAutoloader when an alias resolution surprises you.--execute output, exit codes, model hidden/appended attributes, custom casters) only exist above specific minor versions.First productive use case: use it as a data-inspection tool instead of writing throwaway routes or dd() calls. Pull a model, inspect hidden/appended attributes and relations, run the query you were about to put in a controller, and confirm it behaves before writing the code.
Interactive exploration. The highest-value pattern is question → query → answer without touching the codebase:
> $u = App\Models\User::find(1)
> $u->load('orders.items')
> $u->orders->sum('total')
> App\Models\User::whereNull('email_verified_at')->count()
> App\Models\Order::where('status','pending')->toSql()
Because it is the real container, anything you can app() is available: app(ReportBuilder::class)->build(), Mail::fake(), event(new OrderShipped($order)), dispatch(new SyncJob()), Http::get(...)->json(). It is also a fast scratchpad for Str::, Arr::, Carbon, and validation rules.
Non-interactive one-liners (--execute). Since v2.2.0 this is the scriptable path, and it is genuinely useful for smoke checks and deploy tasks:
php artisan tinker --execute="echo Cache::get('build-id');"
php artisan tinker --execute="dump(App\Models\User::first()->only(['id','email']));"
Use single quotes around the PHP so your shell doesn't eat $variables. From v2.7.3 the output is raw (no PsySH decoration), so it pipes cleanly; from v3.0.0 exceptions produce a non-zero exit code, which means you can gate shell/CI logic on it. Version 2.4.1 fixed the loader leaking past --execute, so scripts that run repeatedly in one process are safe on current releases.
Preloading with --include. Put reusable helpers or fixture setup in a PHP file and bootstrap the session with it: php artisan tinker --include=dev/tinker-bootstrap.php. This is cleaner than pasting ten lines every morning.
Transaction-scoped experiments. There is no undo. When you're about to mutate data, open a transaction first and roll back explicitly at the end of the experiment:
> DB::beginTransaction()
> $u = App\Models\User::first(); $u->name = 'Test'; $u->save()
> DB::rollBack()
Or wrap the whole experiment in DB::transaction(function () { ... }). Better still, point the shell at a scratch connection/database before you start typing writes.
Query and N+1 debugging. DB::enableQueryLog() … DB::getQueryLog() inside the shell gives you the SQL and bindings for the exact snippet you're prototyping — often faster than wiring up Telescope for a one-off question. ->dd() on a query builder dumps SQL + bindings and halts.
Custom casters for domain objects. Tinker ships casters for HtmlString (v2.1.0), Stringable (v2.6.0), and ProcessResult (v2.8.1), and shows model hidden/appended attributes (v2.6.1). For your own value objects, custom caster support landed in v2.6.2 — define a class with PsySH's static cast* methods and register it on the shell (the wiring point is TinkerCommand::getCasters(); override it in a subclass if your version's config hook isn't available). This turns dump() output from a wall of private properties into something readable, which matters when a money/address/state object is what you inspect all day.
Team/onboarding usage. Because it boots the real app, php artisan tinker --execute="..." is a neat way to verify a deployed environment without SSH-ing into code: check a config value, confirm a queue connection responds, confirm a scheduled command's query returns rows.
laravel/tinker lives in require-dev. Running composer install --no-dev in production removes it, and any deploy script relying on artisan tinker --execute will break. If you do keep it around, remember --execute bypasses the interactive confirmation guards you'd get at the prompt.use imports, and DB::enableQueryLog() are per-process. Only readline history persists (~/.config/psysh/psysh_history on modern PsySH). That history file is a real secret-leak vector: tokens, passwords, and copied API keys typed into the shell end up in plaintext there.User::truncate() is instant and permanent. Always confirm which .env/database you're attached to — config('database.default') and config('database.connections.mysql.database') — before destructive commands, and prefer DB::beginTransaction().env() can return null in Tinker. After php artisan config:cache, the .env file isn't loaded, so env('FOO') is empty while config('services.foo') works. Always read settings via config() in the shell.use statement: use App\Models\User; is fine, but importing a class under a name that's already aliased can throw a "name is already in use" error mid-session. Fall back to the FQCN, or start a fresh session.dd() kills the shell; dump() doesn't. Use dump() + exit only when you mean it. Related: Ctrl+C often just abandons the input line rather than cancelling a long-running query, so a runaway User::all()->each(...) will hold the terminal. Use lazy(), chunk(), or limit() — dumping a 50k-row collection is the most common way to make Tinker appear to hang.--execute quoting bugs are shell bugs, not PHP bugs. Wrap the code in single quotes, avoid heredocs in Makefiles without care, and prefer echo/print for scalars and dump() for structures, since raw output means there's no automatic result echo.migrate, migrate:install, optimize, clear-compiled, up/down). Don't expect a non-interactive --execute script to sail through Artisan::call('migrate:fresh') — use real artisan commands in deploy scripts instead.psy/psysh constraint if you pin that separately.TinkerCommand (options, confirmation logic, casters) and ClassAliasAutoloader (alias map) are the two classes you subclass or read when you need behaviour beyond the defaults. Everything else in the package is a thin adapter over PsySH.How can I help you explore Laravel packages today?