Laravel Tinker: Stop Writing Test Controllers Just to Run One Query š®
Laravel Tinker: Stop Writing Test Controllers Just to Run One Query š®
Confession time.
I once had a production codebase with a controller literally named DebugController. It had a testQuery method. It was behind no middleware. It was accessible from the browser. It had been there for eight months.
I was a junior dev at the time. I didn't know better. But I've seen senior devs do the same thing in 2025. There's no excuse anymore ā Tinker exists.
What Even Is Tinker? š¤
Tinker is Laravel's interactive REPL (Read-Eval-Print Loop). Think of it as a PHP shell that boots your entire Laravel application ā models, services, config, database connections, the whole shebang ā and lets you poke around live.
php artisan tinker
That's it. One command. You're now inside your app.
> User::count()
= 1847
> User::where('is_premium', true)->count()
= 312
> User::latest()->first()->email
= "[email protected]"
No browser. No routes. No controllers. No more dd($user) commits you accidentally push to main (we've all been there š
).
Real Talk: The Test Controller Phase š¬
In production systems I've built, I've seen this pattern more times than I can count:
// routes/web.php
Route::get('/debug-temp-delete-this', [DebugController::class, 'check']);
// app/Http/Controllers/DebugController.php
public function check() {
$result = Order::where('status', 'pending')
->where('created_at', '<', now()->subHours(2))
->get();
dd($result); // totally temporary, I promise
}
"Totally temporary." That route is still there in 2026. It's been deployed to prod. The dd() breaks the checkout flow once a month when someone accidentally hits it.
Tinker kills this entire antipattern dead. šŖ¦
The Stuff You'll Actually Use š ļø
Running Eloquent queries:
> $orders = Order::where('status', 'pending')->with('user')->get()
> $orders->count()
= 47
> $orders->first()->user->email
= "[email protected]"
Testing your relationships without spinning up a browser:
> $user = User::find(1)
> $user->orders()->where('total', '>', 100)->count()
= 12
Triggering jobs or events to test them:
> dispatch(new ProcessRefundJob($order))
> event(new OrderShipped($order))
As a Technical Lead, I've learned that this is 10x faster than writing a test, seeding data, making an HTTP request, and reading logs. Just fire it up, poke the thing, see what happens.
Testing your factories:
> User::factory()->make()
= App\Models\User {
name: "Bart Doe",
email: "[email protected]",
...
}
No database write. Just see what your factory produces. Beautiful.
Pro Tip: Exit Without Ceremony šŖ
Ctrl+D or type exit. Don't be the person who Googles "how to exit tinker" (it's okay, I did it once in 2018).
The Hidden Gem: Tinker in Production š
A pattern that saved us in a real project ā we had a data migration that needed to run on live data but was too risky to put in a migration file. Wrong data types in 40,000+ rows.
Instead of writing a one-off Artisan command, deploying it, running it, and deleting it... we just SSH'd into the server and ran Tinker:
> Product::where('legacy_price', null)
->chunk(500, function($products) {
$products->each(fn($p) => $p->update(['legacy_price' => $p->price * 100]));
})
Done. Live data. Safe chunking. No deployment required.
ā ļø Real Talk: Yes, this is powerful. Yes, that means you can also accidentally delete your entire
userstable with one bad command. UseDB::beginTransaction()before anything destructive, andDB::rollBack()if you panic. We learned this the fun way in staging.
Tinker + PsySH = Supercharged š
Tinker runs on PsySH under the hood, which means you get some extra goodies:
> show(User::class) // See the class source
> doc User::find // See PHPDoc for a method
> ls $user // List all properties and methods
> wtf // Show last exception (yes, the command is really "wtf")
The wtf command alone is worth knowing. When you get a cryptic exception in Tinker, just type wtf and it shows the full stack trace. I love that they kept the name.
Bonus Tips Section šÆ
Run Tinker in a specific environment:
APP_ENV=staging php artisan tinker
Pipe a file into Tinker for batch operations:
php artisan tinker < fix-data-script.php
Use --execute for one-liners:
php artisan tinker --execute="echo User::count();"
This last one is great for deployment scripts that need to verify data after a migration ran.
The Workflow I Use Every Day
- New feature? Boot Tinker, query the real DB, understand the data shape first.
- Bug report? Tinker to reproduce the exact scenario without touching the UI.
- Data question from the client? Tinker to get the number in 30 seconds, not 30 minutes.
- Testing a notification?
$user->notify(new SomeNotification())ā done.
As a Technical Lead, I've made Tinker part of our team's muscle memory. New dev joins? First thing I show them: "Stop writing debug routes, learn Tinker."
TL;DR ā
php artisan tinkerboots your entire app in an interactive shell- Query Eloquent models, fire events, dispatch jobs ā all without a browser
- Way safer and faster than creating debug routes or controllers
wtfshows your last exception (it's a real command, I promise)- Use
DB::beginTransaction()before destructive operations in production
The DebugController era is over. Long live Tinker. š®
Found a use case I missed? Connect on LinkedIn ā I'm always collecting cursed debug patterns from the Laravel community.
Want to see more tricks like this? Star the repo on GitHub ā it genuinely motivates me to keep writing. š