PHPDebugging (Xdebug)

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
<?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

Bash
pecl install xdebug

php.ini additions

Text
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
<?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.

Leave profiling and debug mode off in production
Both step debugging and profiling add real overhead to every request and, in the case of debug mode, will try to connect out to a debug client on every single page load if misconfigured. Xdebug should be enabled in local development (and sometimes a dedicated staging environment) but never left active on a production server — it slows every request down and can leak internal details if a debug connection is ever reachable from outside your network.
var_dump() still has a place
Xdebug doesn't replace `var_dump()` entirely. For a quick "is this variable what I think it is" check in the middle of otherwise working code, printing a value is still faster than attaching a debugger. Reach for step debugging once the bug involves following state through multiple function calls, a loop, or conditional branches where the exact path taken isn't obvious from reading the code alone.
Tip
If Xdebug feels like it's "not connecting," check three things in order: the extension is actually loaded (`php -v` should mention Xdebug), `xdebug.client_port` matches the port your editor's debugger is listening on, and your editor is actively listening for a connection before the request is made — most IDE debuggers require you to explicitly start listening first.