Xdebug: The Debugging Superpower You're Probably Not Using
Xdebug: The Debugging Superpower You're Probably Not Using
In the previous posts, I walked through debugging with dd(), Log statements, and Ray. Each has its place. But there's one tool I haven't mentioned yet — the one that, once you learn it, changes how you think about debugging entirely.
Xdebug.
If you've heard of it but never set it up, you're not alone. Xdebug has a reputation for being painful to configure. And honestly, that reputation isn't entirely undeserved. But once it's running, it gives you something no other tool can: the ability to pause your code mid-execution and step through it line by line, inspecting every variable, every method call, every decision your application makes.
It's like having a time machine for your code.
What Xdebug Actually Does
Let me explain what makes Xdebug different from everything else.
When you use dd(), you're saying: "Stop here and show me this value." The request dies. You see one snapshot.
When you use Ray, you're saying: "Send me this value while the code keeps running." You see a stream of data, but you can't control the flow.
When you use Xdebug, you're saying: "Pause here. Let me look around. Let me step forward one line at a time. Let me decide when to continue."
You're not just observing your code anymore. You're controlling it.
Setting Up Xdebug (The Short Version)
I'm not going to walk through the full setup here — it varies depending on whether you're using Laravel Herd, Valet, Sail, Docker, or something else entirely. But here are the best resources I've found for each environment:
Laravel Herd:
Laravel Sail / Docker:
- Debugging Laravel Sail with Xdebug 3 in PHPStorm
- Get Xdebug Working With Docker and PHP 8.4 in One Minute
Laravel Valet:
Laracasts (Video):
- Debugging With Xdebug — Jeffrey Way walks through the setup and basic usage in PHPStorm
- Sweet PHP Debugging in Four Minutes — If you're using VS Code
Pick the one that matches your setup, spend 15-20 minutes getting it working, and then come back here. I'll wait.
Your First Debugging Session
Alright, Xdebug is installed. Your IDE is listening for connections. Now what?
Let's say I'm debugging that checkout flow from the previous post. Something's wrong with the order total, and I want to understand exactly what's happening.
First, I set a breakpoint. In PHPStorm (or VS Code), I click in the gutter next to the line number where I want execution to pause:
public function store(CheckoutRequest $request)
{
$cart = Cart::findOrFail($request->cart_id);
// I click here to set a breakpoint
$order = Order::create([ // <-- Red dot appears in the gutter
'user_id' => auth()->id(),
'total' => $cart->calculateTotal(),
]);
event(new OrderCreated($order));
return redirect()->route('orders.show', $order);
}
Then I start the debug listener in my IDE and make the request — either through the browser (with the Xdebug browser extension enabled) or by running a test.
The moment execution hits that line, everything stops. My IDE lights up with:
- The current line highlighted — showing exactly where execution paused
- The call stack — showing how we got here (which controller, which middleware, which route)
- All variables in scope —
$request,$cart, and anything else available at this point
This is where it gets powerful.
Inspecting Variables (Better Than Any dd())
In the variables panel, I can see $cart and everything inside it. But unlike dd(), I can:
- Expand nested objects — Click into
$cart->itemsand see each item, then click into each item and see its properties - Evaluate expressions — Type
$cart->calculateTotal()in the evaluate window and see what it returns, without modifying my code - Modify values on the fly — Change
$cart->discountto a different value and continue execution to see what happens
This last one is huge. Instead of changing my code, refreshing, and trying again, I can test hypotheses in real-time.
"What if the discount was zero? Would the total be correct then?"
I just change the value in the debugger, hit continue, and find out immediately.
Stepping Through Code
Here's where Xdebug truly shines. Once I'm paused at a breakpoint, I have several options:
Step Over (F8 in PHPStorm): Execute the current line and move to the next one. If the current line calls a method, it runs that method completely and stops at the next line.
Step Into (F7): If the current line calls a method, jump into that method and pause at its first line. This is how I trace exactly what calculateTotal() does.
Step Out (Shift+F8): If I'm inside a method and I've seen enough, jump back out to wherever this method was called from.
Resume (F9): Continue running until the next breakpoint (or until the request completes).
Let me show you how I'd use these to debug the checkout issue.
I'm paused at the Order::create() line. I want to know what $cart->calculateTotal() returns, and more importantly, why it returns that value.
So I Step Into the calculateTotal() method:
// Now I'm inside Cart.php
public function calculateTotal(): float
{
$subtotal = $this->items->sum(function ($item) {
return $item->price * $item->quantity;
});
$discount = $this->calculateDiscount($subtotal);
return $subtotal - $discount;
}
I can see $subtotal as it's calculated. I can step into calculateDiscount() if I suspect the issue is there. I can watch each variable change as I step through line by line.
It's like watching your code execute in slow motion.
Conditional Breakpoints
Sometimes a bug only happens under specific conditions. Maybe the checkout works fine for most orders, but breaks when there's a discount code applied.
I don't want to pause on every single checkout — I want to pause only when a discount code is present.
Right-click on the breakpoint and add a condition:
$cart->discount_code !== null
Now Xdebug will only pause when that condition is true. I can make 10 test purchases, and the debugger will only activate for the ones that matter.
This is incredibly useful for debugging issues in loops too. Instead of pausing on every iteration:
foreach ($items as $item) {
$this->processItem($item); // Breakpoint here with condition: $item->sku === 'PROBLEM-SKU'
}
I only pause on the specific item I'm interested in.
Debugging the Request Lifecycle
Remember the request lifecycle from my first post? Xdebug lets you actually see it happen.
Set a breakpoint in a middleware:
// app/Http/Middleware/LogApiRequests.php
public function handle(Request $request, Closure $next)
{
// Breakpoint here
$response = $next($request);
return $response;
}
Now when you make a request, you can:
- Pause in the middleware and inspect the request before it hits your controller
- Step Into the
$next($request)call to follow the request deeper into the pipeline - Watch as it moves through each middleware layer, then into the router, then into your controller
- See the response object as it travels back out
This is how you really learn how Laravel works. Not by reading about it — by watching it happen.
Debugging Eloquent Queries
One of my favorite uses for Xdebug is understanding what Eloquent is actually doing.
Set a breakpoint inside a query scope or a relationship method:
// app/Models/User.php
public function scopeActive($query)
{
// Breakpoint here
return $query->where('status', 'active')
->whereNotNull('email_verified_at');
}
When execution pauses, I can evaluate $query->toSql() in the debugger to see the SQL being built. I can step through and watch the query builder chain together.
For complex queries, this beats logging every time.
Debugging Jobs and Commands
This is where Xdebug saves the most time compared to other tools.
When a job runs in the queue, there's no browser. dd() would just dump to the worker process output (if you're even watching it). Ray works, but you're still just observing.
With Xdebug, I can set a breakpoint inside a job:
// app/Jobs/ProcessOrder.php
public function handle()
{
// Breakpoint here
$this->order->items->each(function ($item) {
$this->updateInventory($item);
});
$this->sendConfirmationEmail();
}
Then I run the queue worker. When the job executes, the debugger pauses, and I can step through the entire job execution — inspecting the order, watching inventory updates, seeing exactly what happens.
For Artisan commands, same thing:
// app/Console/Commands/ImportUsers.php
public function handle()
{
// Breakpoint here
$rows = $this->parseCSV();
foreach ($rows as $row) {
User::create($row);
}
}
Run php artisan import:users, and the debugger catches it.
Debugging Tests
This is where Xdebug becomes a daily tool for me.
I write a test that's failing. Instead of adding dd() calls and running it over and over, I set a breakpoint and run the test in debug mode.
public function test_order_total_includes_discount()
{
$cart = Cart::factory()
->has(CartItem::factory()->count(3))
->create(['discount_code' => 'SAVE20']);
// Breakpoint here
$response = $this->post('/checkout', [
'cart_id' => $cart->id,
]);
$response->assertStatus(200);
$order = Order::first();
$this->assertEquals(80.00, $order->total); // This is failing — why?
}
When I run this test in debug mode, I can step into the checkout process, watch the total being calculated, and see exactly where the expected 80.00 becomes something else.
No guessing. No "I wonder what this value is." I just know.
The Debugging Mindset Shift
Here's what changes once you're comfortable with Xdebug:
Before: "Let me add some dd() calls and see what's happening."
After: "Let me set a breakpoint and watch what's happening."
The difference is control. With dd() and Ray, you're collecting evidence after the fact. With Xdebug, you're present at the scene, watching everything unfold.
It takes some getting used to. The first few times, stepping through code feels slow. But once you're comfortable with the keyboard shortcuts and you develop an intuition for where to set breakpoints, it becomes the fastest way to understand complex bugs.
When to Use What
Here's how I think about it now:
| Situation | Tool |
|---|---|
| Quick "what is this value?" check | dd() |
| Watching data flow through a request | Ray |
| Understanding why something happens | Xdebug |
| Debugging complex logic with multiple branches | Xdebug |
| Debugging jobs, commands, or tests | Xdebug |
| Production debugging | Log statements |
Xdebug isn't a replacement for the other tools — it's the tool you reach for when observation isn't enough, and you need control.
Give It 30 Minutes
If you've never used Xdebug, here's my challenge: spend 30 minutes setting it up. Use one of the resources I linked above for your specific environment.
Then, the next time you have a bug that would normally take 5-6 dd() calls to track down, try using the debugger instead.
Set a breakpoint. Step through the code. Watch the variables change.