0x55aa
← Back to Blog

Laravel Tinker: Stop Writing Test Controllers Just to Run One Query šŸ”®

•5 min read

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 users table with one bad command. Use DB::beginTransaction() before anything destructive, and DB::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

  1. New feature? Boot Tinker, query the real DB, understand the data shape first.
  2. Bug report? Tinker to reproduce the exact scenario without touching the UI.
  3. Data question from the client? Tinker to get the number in 30 seconds, not 30 minutes.
  4. 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 tinker boots 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
  • wtf shows 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. šŸ™