skip to content

What is `php artisan tinker` in a Laravel application, and what should you watch out for when using it against real data?

level: juniorimportance: should knowfreq 50%

answer

  1. laravel/tinker on top of PsySH
  2. boots the whole app, same .env
  3. writes are real, nothing rolls back
  4. dispatch() helper waits for garbage collection
  5. restricted Artisan allow-list

basics

~20 s

php artisan tinker opens a PsySH-based REPL with the whole Laravel application booted, so Eloquent, facades and services work interactively. It uses the same configuration and database as the CLI, so every write is real.

solid answer

~40 s

`php artisan tinker` comes from the `laravel/tinker` package, a Laravel integration of the **PsySH** REPL. It boots the full application — container, config, facades, Eloquent — so you can type `User::where('email', $email)->first()` or call a service and see the result immediately. It connects with whatever the CLI's configuration says, so on a server it is a live console on real data: `save()`, `delete()` and `Mail::to()->send()` really happen, and nothing is wrapped in a transaction. Two Laravel-specific gotchas: the `dispatch()` helper queues the job only when its pending-dispatch object is destroyed, so in Tinker use `Bus::dispatch()` or `Queue::push()`; and only an allow-list of Artisan commands can be run inside the shell unless you extend `commands` in `config/tinker.php`. Tinker is in the skeleton's `require` section, so it is installed on production servers too.

go deeper

for a junior

Recall that php artisan tinker is a REPL with the whole application booted, and that it talks to the configured database for real.

for a middle

Explain why the dispatch() helper misbehaves in a REPL, why code edits need a restart, and how the Artisan allow-list and class aliasing work.

for a senior

Show operational judgement: treat production Tinker as privileged access, read before writing, and turn repeated data fixes into reviewed commands.

for a principal

Set a team policy for production REPL access — who may use it, how sessions are recorded, and when a fix must go through a command and review instead.

## What Tinker is **Tinker** is Laravel's interactive shell — a REPL (read-eval-print loop) where each line of PHP you type is executed immediately and its result printed. It is the `laravel/tinker` package, which wraps the general-purpose PHP REPL **PsySH** and adds Laravel integration. You start it with: ```bash php artisan tinker ``` Because it runs as an Artisan command, Tinker **boots the entire application** first: service providers, configuration, the container, facades, Eloquent models and database connections. That is what makes it useful — you can explore the real application rather than plain PHP: - query and inspect models: `App\Models\User::latest()->first()`; - call your own services: `app(App\Services\PriceCalculator::class)->quote($order)`; - check configuration or a helper's output: `config('queue.default')`, `now()->addDays(3)`; - try an expression before writing it into code. Tinker automatically **aliases** classes, so typing `User` usually resolves to `App\Models\User`; the `dont_alias` array in its configuration excludes classes you do not want aliased. ## The same configuration, the same database Tinker is not a sandbox. It uses the configuration the CLI process resolves — the same `.env` (or cached config) and the same database connection as every other Artisan command on that machine. On a developer laptop that is harmless; on a server it means: - `$user->save()`, `$order->delete()` and `DB::table('orders')->update([...])` change real rows immediately; - nothing is wrapped in a transaction or rolled back when you exit; - `Mail::to($customer)->send(...)` sends real mail if the mailer is real; - events fire and observers run exactly as they would in a request. This matters because **Tinker ships to production**: the Laravel 13 skeleton lists `laravel/tinker` under `require`, not `require-dev`, so a normal `composer install --no-dev` still installs it. Treat a Tinker session on a server as privileged production access. ## Laravel-specific gotchas 1. **The `dispatch()` helper.** `dispatch(new SendReport($id))` and `SendReport::dispatch($id)` return a pending-dispatch object that pushes the job onto the queue when the object is **destroyed**. In a script that happens at the end of the statement; in a REPL the returned value can stay referenced, so the push may happen later than you expect. The documented workaround is `Bus::dispatch(...)` or `Queue::push(...)`, which dispatch immediately. 2. **Code changes need a restart.** PHP cannot redefine a class that is already loaded in a running process, so after editing a model or service you exit and start Tinker again. 3. **Artisan inside Tinker is restricted.** Tinker keeps an allow-list of Artisan commands that may be run from the shell — by default `clear-compiled`, `down`, `env`, `inspire`, `migrate`, `migrate:install`, `up` and `optimize`. Others can be allowed through the `commands` array after publishing the configuration with `php artisan vendor:publish --provider="Laravel\Tinker\TinkerServiceProvider"`. ## Tinker versus alternatives | Need | Better tool | |---|---| | Try an expression or inspect a record | Tinker | | A repeatable data fix you will run again | a custom Artisan command, reviewed and committed | | Understanding a table's columns and indexes | `php artisan db:table <table>` | | Understanding a model's casts and relations | `php artisan model:show <Model>` | | Running it inside Sail's container | `sail tinker` | ## A worked example: checking a record on day one Imagine joining a team whose support ticket says a customer's shipment is "stuck". On a staging copy you might open Tinker and work outward from the record: - `$s = App\Models\Shipment::find(4182)` to load it; - `$s->status` and `$s->updated_at` to see where it stopped; - `$s->events()->latest()->take(5)->get()` to read the last state changes, if the model defines that relation; - `config('queue.default')` to confirm which queue connection would carry the retry. Every one of those lines only reads. The moment the fix involves changing rows, write it as a command the team can review and run the same way on every environment. ## Good habits on shared environments - Read before you write: select the rows, check the count, then change them. - Prefer a reviewed command for anything you would otherwise paste into production Tinker twice. - Keep an audit trail: note what you ran and why, because Tinker itself leaves no record in the application. - Check where you are first — `app()->environment()` and `config('database.default')` take a second to type.

  • Why do the Laravel docs tell you to use `Bus::dispatch()` or `Queue::push()` instead of the `dispatch()` helper inside Tinker?
    The helper returns a pending-dispatch object whose destructor actually pushes the job, so the job is queued when that object is garbage-collected. In a REPL the returned object can remain referenced after the line runs, so the job may be queued late. `Bus::dispatch()` and `Queue::push()` dispatch immediately and return, which makes the behaviour predictable.
  • You changed a method on `App\Models\Order` while a Tinker session was open, but Tinker still runs the old code. Why?
    The class was loaded into the running PHP process the first time you used it, and PHP cannot redefine or reload a loaded class in the same process. Exit Tinker and start it again to pick up the edited code.

Tinker is like a technician's service port on a machine that is already running: you get direct access to every part while it works, which is exactly why a wrong command changes the real machine rather than a model of it.

saying these in an interview costs you the question

  • Tinker runs in a sandbox, so changes are rolled back when you exit.
  • Tinker is a dev dependency and never exists on production servers.
  • Tinker only exposes plain PHP; Eloquent and facades are not available.
  • Any Artisan command can be called from inside a Tinker session by default.
  • Editing a class while Tinker is open takes effect on the next line you type.