How to Use a Debugger in PHP

How do you debug your PHP code? If you use `echo`, `var_dump`, and `print_r`, there's a better way: use a debugger! Join Mauro Chojrin for an exploration of three world-class debuggers available for PHP.

Bugs are annoying. They are by far the most time-consuming part of any software development project, and despite what any developer could desire, there's no way to eliminate them for good.

What you can do as a developer is leverage existing tools and methodologies that help reduce the time devoted to hunting down and fixing them.

Unfortunately for PHP developers, the language's built-in tools are mediocre, and this is probably an overstatement. Think var_dump, print_r, and the dynamic duo of dump and die.

In this article, you'll learn exactly what a debugger is, what options you have within the PHP ecosystem, and how to install and use one.

Let's get started.

What is a Debugger

A debugger, in general, is a tool that helps to find the causes of bugs. in particular, every tool has its own feature set, but its purpose is always to perform debugging.

Don't get confused; a Debugger won't fix problems for you (yet), but it'll certainly help you pinpoint the issues.

Before we dig any deeper, let's quickly review the tools you have out of the box:

  • Error messages
  • Log files

These are certainly very important, but don’t tend to be very informative. More importantly, you only get information after the script has completely executed, which makes it difficult to get the context of what was happening when the error occurred. This means that you have to re-create the dynamic flow of the program in your head.

A debugger will allow you to see the contents of variables while the program is running rather than after the last line has run.

Another great advantage of using a tool like this is the ability to inspect runtime values without adding code.

I want to stress this last point, as many developers don't consider it to be very important. It's not uncommon that in the heat of battle, you put a debugging message that the end users are not supposed to see, such as the following:

<?php

if ($var === INVALID_VALUE) {
    echo 'Arrrrrrrrgghhh!!! $var = INVALID_VALUE! Oh god why oh why???';
}

Then, after hours of thoroughly examining the code, you finally understand it, fix it, and call it a day. However, when demo time comes, you find yourself with a screen like this:

Screen containing debugging message

Or much worse...

I hope you realize now that the ability to see what's going on without tampering with your code is extremely valuable.

In practical terms, a debugger is a plugin or addon for an integrated development environment (IDE). In the case of PHP, it's a little trickier since PHP is an interpreted language, which usually runs on top of a web server. Basically, there are two pieces to every PHP debugger: a server (the debugger itself) and a client (the IDE).

Available Debuggers for PHP

There are a few serious contestants in the PHP debug landscape:

PHPDbg

PHPDbg is an interactive php interpreter with built-in debugging capabilities. Since version 5.6, it has been bundled with every PHP installation. It could come in handy in a situation where you don't have access to a proper graphical editor, such as a remote server.

To get it started, all you need to do is issue a command like the following:

phpdbg -e /path/to/your/php/script

Will which show you a prompt like this:

[Welcome to phpdbg, the interactive PHP debugger, v8.0.12]
To get help using phpdbg type "help" and press enter
[Please report bugs to <http://bugs.php.net/report.php>]
[Successful compilation of /path/to/your/php/script]
prompt>

From there, you can step through your program, set breakpoints, etc. For instance, let’s say you have a php script that looks like this:

<?php

try {
    $conn = new PDO('sqlite:dt.sq3');
} catch (PDOException $exception) {
    die($exception->getMessage());
}

$sql = "SELECT * FROM products";
$st = $conn
    ->query($sql);

if ($st) {
    $rs = $st->fetchAll(PDO::FETCH_FUNC, fn($id, $name, $price) => [$id, $name, $price] );

    echo json_encode([
        'data' => $rs,
    ]);
} else {
    var_dump($conn->errorInfo());
    die;
}

And you want to see the contents of $rs before the json encoding takes place. You could start the debugging session with this command:

phpdbg -e get_data.php

Then, place a breakpoint on line 14:

prompt> b 14
[Breakpoint #0 added at get_data.php:14]

Next, have the script run to that point using this command:

prompt> run
[Breakpoint #0 at get_data.php:14, hits: 1]
>00014:     $rs = $st->fetchAll(PDO::FETCH_FUNC, fn($id, $name, $price) => [$id, $name, $price] );
 00015: 
 00016:     echo json_encode([

Take one further step:

prompt> step
[L14      0x7fa306058d80 INIT_METHOD_CALL<2>     $st                  "fetchAll"                                get_data.php]
[L14      0x7fa306058da0 SEND_VAL_EX             10                   1                                         get_data.php]
[L14      0x7fa306058dc0 DECLARE_LAMBDA_FUNCTION<64> "\000{closure}/hom"+                      ~7                   get_data.php]
[L14      0x7fa306058de0 SEND_VAL_EX             ~7                   2                                         get_data.php]
[L14      0x7fa306058e00 EXT_FCALL_BEGIN                                                                        get_data.php]
[L14      0x7fa306058e20 DO_FCALL                                                          @8                   get_data.php]
[L14      0x7fa306092060 EXT_STMT                                                                               get_data.php]
[L14      0x7fa306092080 INIT_ARRAY<12>          $id                  NEXT                 ~0                   get_data.php]
[L14      0x7fa3060920a0 ADD_ARRAY_ELEMENT       $name                NEXT                 ~0                   get_data.php]
[L14      0x7fa3060920c0 ADD_ARRAY_ELEMENT       $price               NEXT                 ~0                   get_data.php]
[L14      0x7fa3060920e0 RETURN                  ~0                                                             get_data.php]
[L14      0x7fa306092060 EXT_STMT                                                                               get_data.php]
[L14      0x7fa306092080 INIT_ARRAY<12>          $id                  NEXT                 ~0                   get_data.php]
[L14      0x7fa3060920a0 ADD_ARRAY_ELEMENT       $name                NEXT                 ~0                   get_data.php]
[L14      0x7fa3060920c0 ADD_ARRAY_ELEMENT       $price               NEXT                 ~0                   get_data.php]
[L14      0x7fa3060920e0 RETURN                  ~0                                                             get_data.php]
[L14      0x7fa306092060 EXT_STMT                                                                               get_data.php]
[L14      0x7fa306092080 INIT_ARRAY<12>          $id                  NEXT                 ~0                   get_data.php]
[L14      0x7fa3060920a0 ADD_ARRAY_ELEMENT       $name                NEXT                 ~0                   get_data.php]
[L14      0x7fa3060920c0 ADD_ARRAY_ELEMENT       $price               NEXT                 ~0                   get_data.php]
[L14      0x7fa3060920e0 RETURN                  ~0                                                             get_data.php]
[L14      0x7fa306058e40 EXT_FCALL_END                                                                          get_data.php]
[L14      0x7fa306058e60 ASSIGN                  $rs                  @8                                        get_data.php]
[L16      0x7fa306058e80 EXT_STMT                                                                               get_data.php]
>00016:     echo json_encode([
 00017:         'data' => $rs,
 00018:     ]);

Then, evaluate the result for yourself:

prompt> ev $rs 
Array
(
    [0] => Array
        (
            [0] => 1
            [1] => Chair
            [2] => 20.0
        )

    [1] => Array
        (
            [0] => 2
            [1] => Shoe
            [2] => 1.0
        )

    [2] => Array
        (
            [0] => 3
            [1] => Candle
            [2] => 0.75
        )

)

This is certainly nice, but not very practical if you ask me.

ZendDebugger

The second tool I want to tell you about today is ZendDebugger. ZendDebugger is a PHP extension designed to be used in conjunction with ZendStudio, an IDE developed and maintained by Zend.

ZendStudio is a commercial product based on Eclipse PDT.

On the plus side, it has the support of Zend, the company behind PHP, so chances are that if you work with their products, you'll be able to opt for good and timely support. However, it being a commercial product means you can run into licensing issues.

As for the specifics of ZendDebugger, it can be used outside ZendStudio, and it can work well, but it's intended use is as part of the Zend package.

XDebug

Last, but definitely not least, is XDebug. XDebug is also a PHP extension that has to be installed on the development server to enable debugging capabilities. It was created, and is maintained to this day, by Derick Rethans.

The main criticism of XDebug used to be it's cumbersome installation and configuration process. However, since version 3.0, that's all in the past, as you'll see in the following sections.

How to Install XDebug

XDebug's installation is, of course, dependent on your particular platform. However, despite its differences, it's pretty straight forward. If you're using some flavor of Ubuntu or similar, all it takes is for you to run the following

sudo apt-get install php-xdebug

If you don't have the opportunity to use a package manager, you can still install it easily via PECL:

pecl install xdebug

Alternatively, you can get the source code directly from Git and compile it yourself:

git clone git://github.com/xdebug/xdebug.git

If you want to take this road, at least have this on hand. Believe it or not, this was the only way to get it done not so long ago.

Once the binaries are in place, you need to enable xdebug through the php.ini file. The exact file you need to change depends on your particular configuration. There might a single giant php.ini or one for each extension available.

If you don't know which one to choose for your use case, you can find out with a simple command:

php --ini

Once you've located the file you're looking for (ideally, one like xdebug.ini), this is what you should put into it:

zend_extension = xdebug

Next, to make use of XDebug, you need to enable it by at least establishing an execution mode:

xdebug.mode = develop,debug,trace

Now, restart your webserver, and you're ready to start debugging!

There's quite some nuance when it comes to XDebug's modes. If you want to learn more, read this article and watch this video.

Example: Configuring VSCode for XDebug

As I mentioned earlier, there are two sides to this process:

  • The server
  • The client

So far, you’ve learned how to install and configure the server, but if that's all you do, I'm afraid nothing will really change for you.

Any IDE can probably be used to debug PHP with XDebug, but since it’s free and popular, I'll use VSCode as an example.

If you don't have VSCode installed yet, you can get it here.

When you first start your IDE, you'll see a screen like this:

Initial VS Code screen.

If you click on the debug icon, you’ll see the following:

A triangle pointing to the right with a bug on top of it

You'll also see a screen like this:

A screen prompting to configure the IDE for debugging

To start debugging, you’ll need to create a debug configuration. Click on "create a launch.json file" to start creating one. You'll see a dropdown similar to the following:

Options to select a debugging environment

The best way to move forward at this time is to select "Install extension", where you'll be sent to a screen like this:

List of debugging extensions available for VS Code

If you type php next to @category:debuggers, the option list will be reduced to only the plugins that apply to PHP development. There are still many options available. As a good rule of thumb, the most downloaded one is probably your best shot. In fact, the list is sorted by this criteria, so if you pick the first one, you'll be on the right track.

Click install:

Blue button that reads "Install" next to the first extension's description

Once it's done, you'll see a screen similar to the following:

PHP Debug configuration screen

From there, you can see many details of the extension and some installation instructions for the server side.

Now, go back to the debugging screen, and once again click "create a launch.json file". You'll see the same pop-up, but this time, there's a new option available:

Menu showing "PHP" as a new option

Select this option, and you'll be editing a file launch.json, which should look like this:

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Listen for Xdebug",
      "type": "php",
      "request": "launch",
      "port": 9003
    },
    {
      "name": "Launch currently open script",
      "type": "php",
      "request": "launch",
      "program": "${file}",
      "cwd": "${fileDirname}",
      "port": 0,
      "runtimeArgs": [
        "-dxdebug.start_with_request=yes"
      ],
      "env": {
        "XDEBUG_MODE": "debug,develop",
        "XDEBUG_CONFIG": "client_port=${port}"
      }
    },
    {
      "name": "Launch Built-in web server",
      "type": "php",
      "request": "launch",
      "runtimeArgs": [
        "-dxdebug.mode=debug",
        "-dxdebug.start_with_request=yes",
        "-S",
        "localhost:0"
      ],
      "program": "",
      "cwd": "${workspaceRoot}",
      "port": 9003,
      "serverReadyAction": {
        "pattern": "Development Server \\(http://localhost:([0-9]+)\\) started",
        "uriFormat": "http://localhost:%s",
        "action": "openExternally"
      }
    }
  ]
}

This configuration should be enough for now, but if you need more nuanced settings, you can always use the "Add Configuration..." button at the bottom right of this screen and make the changes you require.

Now that we've gone through XDebug's setup, let's see it in action.

Example Usages

In this section, you'll get to see two of the most typical usages of XDebug: debugging a web application and debugging a CLI application. For demonstration purposes, I'll be using the code located in this repository

Debugging a PHP Web Application with XDebug and VS Code

Start by going back to the debugging screen. At the top left, you'll see a dropdown "RUN AND DEBUG":

Run and debug options dropdown

From there, select "Launch Built-in web server":

Launch Built-in web server

You'll immediately be taken to a browser. Not many surprises so far, right? Hold on. Things are about to get exciting.

Go back to VS Code and open the file get_data.php:

Code for the file get_data.php

If you move your mouse right to the left of the line numbers, you'll see a small red circle appearing. Click on it at line 9:

A small red circle next to the number 9

Congratulations. You just established your first breakpoint.

To check it out, go back to your browser and refresh the page. You'll be automatically sent back to your IDE, but this time, you'll find the following screen:

Execution stopped at line 9

What happened? The script execution is on hold, waiting for your action. If you switch to your browser, you'll see that the screen isn't completely drawn. This is a consequence of the Ajax call not being finished. Now you can take all the time you need to examine what was going on at the server right before completing this request.

For instance, if you look at the top left panel (Variables), you'll see the values of every variable available to your script up to this point: after the execution of lines 1-8 but before the execution of line 9:

A panel showing the variable contents

Right above it, you have a few buttons to continue the script execution as you see fit:

Buttons to continue the program execution

In particular, there three options available:

  • Step Over
  • Step Into
  • Step Out

Using any of these, you'll be able to move the execution one step further, effectively allowing the step-by-step execution of the whole script. As you move forward, the variable values shown in the top left panel will be updated according to the dynamic flow of your program. This means no more var_dump all over the place unless you choose to do so.

Debugging a PHP CLI Application with XDebug and VS Code

When it comes to debugging a CLI application, things are really similar to what we just did.

Let's say you want to run get_data.php as a CLI script instead of through a web server. To properly debug it, all you need to change from the previously explored workflow is the debug configuration you'll be using.

Go to the debugging screen and select "Launch currently open script" from the configurations dropdown:

Dropdown option "Launch currently open script" selected

Then, if you hit the play button, you'll immediately see how the execution stopped at line 9. From there, you can keep it going at your own pace.

When running CLI applications, it's very common to have some parameters be fed to it via command line arguments. You can specify such arguments by adding an args key in the launch.json file, like this:

{
  "name": "Launch currently open script",
  "type": "php",
  "request": "launch",
  "program": "${file}",
  "cwd": "${fileDirname}",
  "port": 0,
  "runtimeArgs": [
    "-dxdebug.start_with_request=yes"
  ],
  "env": {
    "XDEBUG_MODE": "debug,develop",
    "XDEBUG_CONFIG": "client_port=${port}"
  },
  "args": [
    "argOne"
  ]
}

Similarly, you can add environment variable definitions via the env key. This could be a reason to have several launch configurations.

XDebug's Advanced Usages

So far, I've shown you the very basic usages of XDebug. In reality, it is a fairly complex and powerful tool. Before closing up, I'd like to take a moment to tell you about a few more advanced features you can take advantage of:

  • Conditional breakpoints: the ability to break execution at a certain point in the code only if a condition is met. This condition is expressed as a simple PHP Boolean expression.
  • Watches: real-time evaluation of complex expressions. You could think of the variables panel as the simplest form of watches.
  • Profiling: execution time and bottlenecks analysis. This is such a broad topic that it should have its own article. I just wanted to mention it so that you know it's possible.

Conclusion

The old, dark days of using echo, var_dump, print_r and such for debugging purposes are over. For any PHP developers who want to take their craft seriously and reduce their bug-hunting time to the absolute minimum, there are great tools available.

The question is whether you will adopt one.

Honeybadger has your back when it counts.

We combine error tracking, uptime monitoring, and cron & heartbeat monitoring into a simple, easy-to-use platform. Our mission: to tame production and make you a better, more productive developer.

Learn more
author photo

Mauro Chojrin

Mauro has been working in IT since 1997, mostly teaching programming. He has many years of experience working with PHP and Symfony. He currently works as an independent consultant and trainer.

More articles by Mauro Chojrin
“We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release.” 
Michael Smith
Try Honeybadger Free for 15 Days
Are you using Sentry, Rollbar, Bugsnag, or Airbrake for your monitoring? Honeybadger includes error tracking with a whole suite of amazing monitoring tools — all for probably less than you're paying now. Discover why so many companies are switching to Honeybadger here.
Try Honeybadger Free for 15 Days
Stop digging through chat logs to find the bug-fix someone mentioned last month. Honeybadger's built-in issue tracker keeps discussion central to each error, so that if it pops up again you'll be able to pick up right where you left off.
Try Honeybadger Free for 15 Days
"Wow — Customers are blown away that I email them so quickly after an error."
Chris Patton
Try Honeybadger Free for 15 Days