A Detailed Guide to PHP Debugging

Phil Vuollet Developer Tips, Tricks & Resources

If you use PHP or you find yourself “adopting” a PHP app (like I did a few years ago), you must know how to debug PHP.

In this detailed guide to PHP debugging, you’ll see some debugging techniques that apply to almost any programming language. But don’t worry. I’ll get into the specifics that apply to PHP, from the basics all the way to fully integrated debugging techniques. Let’s start with a basic technique in PHP debugging: outputting and logging values.

Outputting values

When you need a simple way to debug programs and you have no other options, you can usually output values. Sometimes this means doing a var_dump or logging a whole series of events.

It’s useful to have debug logging in your program. In PHP, you can use various loggers to log debug messages. When the program is run in debug mode or the log level is set to debug, these messages will end up in your stdout, stderr, or log files. The logs will fill up pretty quickly in “debug mode,” so you only want to turn it on temporarily. But I’m getting ahead of myself here. Let me backup to some simple ways to output values.

Dumping variables to stdout

The var_dump function is one way to see what’s happening in your PHP program. It’ll dump a variable value to stdout. There are other functions you can use for debugging through outputs. Here are a few and how they’ll help you:

  • var_dump ($var) dumps the variable type and value to stdout.
  • print_r ($var) prints the variable value in human-readable form to stdout.
  • get_defined_vars() gets all the defined variables including built-ins and custom variables (print_r to view them).
  • debug_zval_dump ($var) dumps the variable with its reference counts. This is useful when there are multiple paths to update a single reference.
  • debug_print_backtrace() prints a backtrace that shows the current function call-chain.
  • debug_backtrace() gets the backtrace. You can print_r, log it to a file, or send it to a logging endpoint asynchronously.

Here’s sample code that exercises each of these useful debugging functions:

<?php
$myVar = "hello world!";

var_dump($myVar);
print_r($myVar);

$allVars = get_defined_vars();
print_r($allVars);
debug_zval_dump($allVars);

function sayHello($hello) {
    echo $hello;
    debug_print_backtrace();
}

sayHello($myVar);
?>

These functions are a quick way to debug your PHP code. You can see them in action in this Paiza. Each function has a purpose and can be useful for debugging.

Switching error reporting level

PHP has a few ways to configure error reporting. You can use the php.ini file, if you have access to it. Otherwise, you might use the htaccess configuration. If you can’t use configuration files, you have the option of changing the values via a script. This is possible, but think about how you would change modes after deploying your application.

A combination of settings will get you the right levels of error logging. You’ll want to consider the following settings:

  • error_reporting sets the level of logging. E_NOTICE is useful during development since it will tell you about defects such as unassigned variables.
  • display_errors tells PHP if and where to display error messages.
  • display_startup_errors should only be used when debugging.
  • log_errors and error_log work together to send errors to a log file. Do this in production rather than displaying them to end users.

The PHP manual spells out these settings in more detail and provides more information I could ever fit in this section. But even with the best logging settings, you still need to monitor for errors.

Monitoring error logs

It’s one thing to log errors—that’s almost a given. It’s a whole other thing to take action when errors are logged. First, you have to know about the errors. Unless you have all day and night to hover over the logs, you will not know when something bad is happening!

The best thing you can do is send your PHP logs to a service that will handle a few key things for you:

  1. Log aggregation. You want to see all your logs in one place. If you can centralize your logs and metrics across instances, that’s even better! You’d be able to spot trouble wherever it happens.

  1. Alerting. There’s nothing better than automation. If you’re a programmer, you know what I mean! You’ll want to automate almost anything if you can. Alerting is a way to send alerts automatically to a group email (better than an individual for continuity) when there’s a problem. This can be a server issue or errors hitting your logs. It should be configurable and you should have control over the configuration.
  2. Traces in your logs. What’s a trace? It’s not just a stack dump that lets you see what was going on when an error happened. It’s also a way to track performance, which is often a sign or a cause of a bug.
  3. Deduplication of log entries. When a bug causes an error, it can fill up the logs pretty quickly. Just combing through the logs with hundreds or thousands of the same entry is a showstopper. Deduplication takes away the pain!

You can configure many of the PHP logging utilities to work with Stackify Retrace by following this guide. Retrace works with PHP, and it does all of these things for you. Plus, it automatically collects lightweight traces—and only when it should.

Sure, Retrace is a great tool for detecting bugs! But once you detect them, fix them. Often that means attaching a debugger. Let’s get into that next!

Stepping through code

Now we will talk about debugging by stepping through code. This is what many of us developers think of when we see “debugging.” It’s a common way to debug code (remove defects that cause errors). With a web app or website, debugging is often two-pronged.

Once notified about an error that’s been logged, we can debug if needed. With enough detail in the logs, this should be easy. We might not even have to use a debugger. Often, the less use one, the better. But if you do, here’s how to tackle that!

PHP debugging tools

You can debug PHP using one of many debugging tools to attach a debugger client. PhpStorm works with debug utilities like Xdebug and ZendDebugger.

Being a polyglot, I need an IDE that supports multiple languages, so I’m opting for VS Code these days. I’ve used Xdebug with Visual Studio in the past, so let’s see how we can set it up with VS Code.

The debug server setup is the same, but each client (IDE or CLI) will have a slightly different setup. See, the debug server (a Zend extension) opens a port, and the client communicates with the server through that port. It’s just a matter of configuration and installing the right components.

Here are the steps I’m taking on this journey:

  1. Check for PHP extensions in VS Code. There are plenty! The top of the list looks like this:
    And in case you’re missing it, PHP IntelliSense is on top with over six million downloads, followed by PHP Debug, with over three million.
  2. Install the PHP Debug extension.
  3. Click “reload” to reload VS Code.
  4. Install Xdebug. The PHP Debug extension for VS Code is only an integration to Xdebug. I have PHP 7.0 installed, so I must get the right version of Xdebug from the download page. The following script will help:
<?php
phpinfo();
?>

I’m looking for the PHP version, compiler version, architecture, and PHP Extension Build so I can download the correct version. I have x86, VC14, NTS (Non-thread-safe). This version came with WordPress for IIS. I’m working on a Windows machine. If you’re on a Linux box, you won’t face these same issues… just compile the source code for Xdebug!

  1. Now I have the right version, I will put it in the PHP/ext directory.
  2. Next, I need to configure PHP to use the extension and allow remote debugging. I’ll add the following configuration to the php.ini file that’s listed in PHP Info:
; set the extension path
zend_extension="C:/Program Files (x86)/PHP/v7.0/ext/php_xdebug-2.6.1-7.0-vc14-nts.dll"
; allow remote debugging
[XDebug]
xdebug.remote_enable = 1
xdebug.remote_autostart = 1

This’ll set up the PHP server to use XDebug. The steps here are the same no matter what IDE you use. Xdebug opens an HTTP port so that your debugger can attach. The client still needs to be configured to attach and use the debugging protocol. I will run through that part now.

  1. Finally, I will configure VS Code to connect to Xdebug. There are a few simple steps and then attaching is automatic. I’ll break this out into its own section since there are a few steps.

PHP Retrace

Configuring your IDE

After installing Xdebug, you still need to configure your IDE to attach to the debugger. In VS Code, this means adding a debug configuration. Fortunately, this is automatic at this point. It’s just a few simple steps:

  1. Switch to the debug view.
  2. Click the gear to bring up the languages menu.
  3. Select PHP. VS Code will generate the default configuration.
  4. Reload the PHP server. I’ve installed another extension called “PHP Server” that makes this simple. Use the context menu (right-click) to control the PHP server as shown in the screenshot below.

This should put our IDE in a state that’s ready to attach to Xdebug. Communications with the debugger happen through a TCP port on the debug server. Xdebug uses the DBGp protocol through port 9000 by default.

Now we’re configured, let’s look at the mechanics of a debug session. Next, we’ll cover how to get into a debug session, how to set breakpoints, and how to step through, into, and over functions. It’s the best part!

Attaching and stepping into debugging

And now, the moment you’ve been waiting for: step-through debugging. We’ve already configured our environment for debugging. We’ve installed a debugging extension (specifically Xdebug), and we’ve configured our IDE (VS Code, in my case). Now it’s time to attach to the debugger.

Attaching a debugger

The PHP Debug extension for VS Code generated a launch.json file. That file goes into a .vscode directory in the root of the project. Here’s what it generated:

{
    // 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": 9000
        },
        {
            "name": "Launch currently open script",
            "type": "php",
            "request": "launch",
            "program": "${file}",
            "cwd": "${fileDirname}",
            "port": 9000
        }
    ]
}

It’s adding two launch configurations. Those are available in the debug view. We can either attach to a running server or launch a new one with the current script. Since I have phpinfo running already, I’ll start there by choosing Listen for XDebug to attach to that server.

Once you’re attached, you will see the debug toolbar.

Most debuggers have a similar control mechanism. This allows you to start, stop, step, and restart your debugger. Since we see a stop and pause icon, we’re attached and ready, so let’s get stepping!

Stepping through code

Stepping through code is as much an art as it is science. The first thing you need to do is set up a breakpoint where you think you have an issue. I’ll usually plug one in just before so I can see what’s happening as we step into the problem code. Let’s set one in the phpinfo script just to get things rolling.

Setting breakpoints

Usually, clicking in the left margin will set a breakpoint on the nearest line. You can also set your cursor on the line and hit F9. If you’ve got multiple functions calls on the same line, that’s a way to ensure the breakpoint is on the right one. A red dot should appear in the left margin. This shows a breakpoint. It should also be listed in the “breakpoints” component. Here’s an image to clarify:

Note: we’re still in the Debug view. I set a single breakpoint. Now, when I right-click the red breakpoint circle in the margin next to the code, you can choose Edit breakpoint… to set up conditions if you need to. Conditions are useful, especially if you have an entire collection but only one element is causing an issue. I use conditionals all the time!

Besides conditional breakpoints, you have the option to log a message and break after a certain number of hits. The latter is useful when you have code that repeats without a specific unique value to trigger a break on. For example, you might have code to render components in a collection of components. If the 13th component causes a catastrophe, you can just set the hit count to 13. I’ve had to count manually too many times to see the value in this feature!

With at least one breakpoint set, you’re ready to step through your code.

Stepping through code

Stepping through code is a complex operation. It’s simple to control, but there’s a lot going on. The debugger will evaluate variables, you can set watches on variables, and you have access to the call stack. Once the debugger is paused on a breakpoint (or by manually hitting the pause button/pressing F6) you’re ready to step through the code.

Use this script to follow along:

<?php
phpinfo();

function step_over_me() {
    echo 'stepping over me';
}

function step_into_me() {
    step_over_me();
}

for ($i=0; $i < 100; $i++) {
    step_into_me();
}

?>

You can step into functions (F11), step out of functions (Shift + F11), and step over functions (F10). You should get used to using the function keys to drive your debugging sessions; they’ll go a lot smoother if you do!

Some languages allow you to go backward in time. PHP doesn’t, but that’s OK. It’s a scripting language, and that would cause issues with declarations, anyway. Let’s step through this script by using F10.

Place a breakpoint on the phpinfo line. With your PHP server serving the page and your debugger attached, reload the web page. If all goes well, your debugger will light up and pause on that line.

Congratulations! You now have an active debug session. Follow these steps to move along through the code:

  1. Press F10 a few times and watch the debugger step through the code.
  2. When you get into the loop, step into a method with F11.
  3. Once you’re inside the method, you can use Shift + F11 to step back out. All the code will still run—it’s just that your debugger won’t follow those parts.
  4. You can use F5 to get out of the loop and continue to the end (or the next breakpoint, if there is one).

TIP: Browser debuggers use F8 to continue since F5 is already mapped to a page reload. This little mismatch can trip you up when you switch between browser tools and your IDE.

Skipping a loop

It’s easy to skip a loop. Just set a breakpoint just past the loop and use F5 to skip ahead to that point.

Often, you want to step into the loop at least once before you pass it by. Use F11 to step into it. Wrap loops in functions so you can step out with Shift + F11 and not lose a beat!

There’s another option, if it’s available to you. It’s run to cursor, and it does what it sounds like. Just place your cursor somewhere past the loop and use this option to jump to that point.

The debugger will run from its current position to the cursor when I select this option. Notice that there’s no keyboard shortcut for it, unfortunately.

Checking variables

One key benefit of debugging is that you can check variables as you step through the code. Most IDEs include a way to check local and global variables. Also, you can watch variables so you don’t have to hunt them down in Locals or Superglobals.

You can also watch expressions! This is great when you want to see how a complex expression inside an if statement will evaluate. Just copy the expression into a watch and you can track the result as values change.

I’ve added the following watch expression: $path == “blah”. In VS Code, you can highlight an expression in the code and open the context menu to watch the expression.

I’ve highlighted $i < 100 to add it to my watch expressions. You can also evaluate any expression in the debug console!

Evaluating in debug console

You can evaluate expressions in the debug console too. If you need to check something once, it’s better to evaluate it in the console.

Open the debug console and enter the expression.

You can also check the context menu for an option to evaluate the selected expression. Here are things you can do in the debug console:

  • Set new variables.
  • Change variable values.
  • Execute functions (in most cases).
  • Evaluate expressions (e.g., $i == 55).

These frequently come in handy for debugging!

Wrapping things up

We’ve covered a lot in this post. It’s enough to get you started with PHP debugging. This is a highly in-depth topic, but the best way to learn is by doing. Practice debugging until you’ve mastered the technique, and you’ll earn a reputation as a bug exterminator extraordinaire!

About Phil Vuollet

Phil Vuollet uses software to automate processes to improve efficiency and repeatability. He writes about topics relevant to technology and business, occasionally gives talks on the same topics, and is a family man who enjoys playing soccer and board games with his children.