Debugging (Xdebug)
Tests catch a lot, but sooner or later something will fail — or behave strangely in production — in a way that isn't obvious from reading the code. This page walks through PHP's debugging toolkit from simplest to most capable: quick output-based checks with var_dump() and print_r(), then real step debugging with Xdebug, the extension that lets you pause execution and inspect a program's state the way a debugger would in any other language.
var_dump() and print_r() as a first pass
The fastest way to answer "what does this variable actually contain right now" is to print it. var_dump() shows both the type and value of a variable, including nested arrays and objects, which makes it more useful than echo for anything beyond a plain string. print_r() gives a more human-readable (but less precise — it doesn't distinguish "1" from 1) view of the same data.
Inspecting a value quickly
<?php
$order = [
'id' => 42,
'total' => '19.99',
'items' => ['Mug', 'Coaster'],
];
var_dump($order);array(3) {
["id"]=>
int(42)
["total"]=>
string(5) "19.99"
["items"]=>
array(2) {
[0]=>
string(3) "Mug"
[1]=>
string(7) "Coaster"
}
}Notice var_dump() reveals that total is stored as the string "19.99" rather than a float — exactly the kind of subtle type mismatch that causes bugs in comparisons or calculations, and exactly the kind of detail echo would hide from you.
Why var_dump-driven debugging doesn't scale
Sprinkling var_dump() calls through a codebase works for a bug that's one or two function calls deep. It breaks down once a bug involves following data through several layers — a controller, a service, a repository, a loop with several iterations — because each added var_dump() requires re-running the whole request, reading through a wall of dumped output, then editing the code again to move the dump somewhere else. Every dump statement is also something you have to remember to delete before committing, and it's easy to leave one behind by accident.
Installing Xdebug
Xdebug is a PHP extension, not a Composer package — it has to be compiled or installed at the PHP interpreter level. The exact install command depends on your OS and PHP version; on a typical Linux setup it's a package manager install followed by enabling the extension in php.ini.
Installing Xdebug via PECL
pecl install xdebug
php.ini additions
zend_extension=xdebug xdebug.mode=debug xdebug.client_host=127.0.0.1 xdebug.client_port=9003 xdebug.start_with_request=yes
xdebug.mode=debug turns on step debugging specifically (Xdebug also has separate modes for profiling and code coverage, which you enable independently). xdebug.start_with_request=yes tells Xdebug to try connecting to a debugger on every request, rather than only when a special cookie or query parameter is present — convenient for local development, though many teams switch this to trigger in shared environments so debugging sessions don't start unintentionally.
Step debugging with breakpoints
With Xdebug running and an editor extension installed (the PHP Debug extension for VS Code, or PHPStorm's built-in debugger), you can click in the gutter next to a line of code to set a breakpoint. When execution reaches that line, the script pauses and the editor shows you every variable in scope at that exact moment — no more guessing which var_dump() to add next.
Step over — run the current line and move to the next one, without diving into function calls it makes.
Step into — if the current line calls a function, jump inside that function and pause at its first line.
Step out — finish executing the current function and pause right after it returns to its caller.
Watch expressions — a small panel where you type an expression (like
$order->total()) and see it re-evaluated live as you step through the code.
This turns debugging from "guess where the bad value comes from, print it, repeat" into "pause exactly where it matters and look" — especially valuable for a bug in a loop where you need to see how a value changes on the third iteration specifically, not the first.
xdebug_break() for a breakpoint in code
Sometimes it's more convenient to trigger a breakpoint from the code itself rather than clicking a line number in your editor — particularly for a breakpoint that should only trigger under a specific condition.
Conditional breakpoint via code
<?php
foreach ($orders as $order) {
if ($order->total() < 0) {
xdebug_break();
}
processOrder($order);
}xdebug_break() pauses execution right there, exactly as if you'd set a breakpoint on that line, but only when the surrounding if condition is true. It's a small function, but it's often faster than configuring a conditional breakpoint through an editor's UI, and it works even when you're debugging from an environment without full IDE integration.
Profiling for performance issues
Step debugging answers "what is the code doing." Profiling answers "where is the code spending its time." Setting xdebug.mode=profile makes Xdebug write a "cachegrind" file for each request, which tools like KCachegrind, QCachegrind, or Webgrind can visualize as a call tree annotated with how much time (and how many calls) each function consumed. This is the right tool when a page is simply slow rather than wrong — Xdebug's profiler will point at the actual bottleneck (often an N+1 database query or an unexpectedly expensive loop) instead of you guessing at which function to optimize.