Chapter 12. Debugging

Programs rarely work correctly the first time you run them. This chapter shows you some techniques for finding and fixing the problems in your programs. When you’re just learning PHP, your programs are probably simpler than the programs that PHP wizards write. The errors you get, however, generally aren’t much simpler, and you have to use the same tools and techniques to find and fix those errors.

Controlling Where Errors Appear

Many things can go wrong in your program that cause the PHP engine to generate an error message. You have a choice about where those error messages go. The messages can be sent along with other program output to the web browser. They can also be included in the web server error log.

A useful way to configure an error message display is to have the errors displayed on screen while you’re developing a PHP program, and then when you’re done with development and people are actually using the program, send error messages to the error log. While you’re working on a program, it’s helpful to see immediately that there was a parse error on a particular line, for example. But once the program is (supposedly) working and your coworkers and customers are using it, such an error message would be confusing to them.

To make error messages display in the browser, set the display_errors configuration directive to On. Set it to Off to prevent error messages from displaying in the browser. To make sure errors end up in the web server error log, keep log_errors set to On.

An error message that the PHP engine generates will fall into one of five different categories:

Parse error
A problem with the syntax of your program, such as leaving a semicolon off of the end of a statement. The engine stops running your program when it encounters a parse error.
Fatal error
A severe problem with the content of your program, such as calling a function that hasn’t been defined. The engine stops running your program when it encounters a fatal error.
Warning
An advisory from the engine that something is fishy in your program, but the engine can keep going. Using the wrong number of arguments when you call a function causes a warning.
Notice
A tip from the PHP engine playing the role of Miss Manners. For example, printing a variable without first initializing it to some value generates a notice.
Strict notices or deprecation warning
An admonishment from the PHP engine about your coding style, or that something you’re doing will stop working in a future version of PHP.

You don’t have to be notified about all the error categories. The error_reporting configuration directive controls which kinds of errors the PHP engine reports. The default value for error_reporting is E_ALL & ~E_NOTICE & ~E_DEPRECATED, which tells the engine to report all errors except notices and deprecation warnings. Appendix A explains what the & and ~ mean in configuration directive values.

PHP defines some constants you can use to set the value of error_reporting such that only errors of certain types get reported:

  • E_ALL (for all errors)
  • E_PARSE (parse errors)
  • E_ERROR (fatal errors)
  • E_WARNING (warnings)
  • E_NOTICE (notices)
  • E_STRICT (strict notices, in versions of PHP before 7.0.0)

Because strict notices were new to PHP 5, they are not included in E_ALL in versions of PHP before 5.4.0. To tell an older version of the PHP engine that you want to hear about everything that could possibly be an error, set error_reporting to E_ALL | E_STRICT.

Fixing Parse Errors

The PHP engine is really picky but not very chatty. If you leave out a necessary semicolon, or start a string with a single quote but end it with a double quote, the engine doesn’t run your program. It throws up its (virtual) hands, complains about a “parse error,” and leaves you stuck in the debugging wilderness.

This can be one of the most frustrating things about programming when you’re getting started. Everything has to be phrased and punctuated just so in order for the PHP engine to accept it. One thing that helps this process along is writing your programs in an editor that is PHP-aware. This is a program that, when you tell it that you are editing a PHP program, turns on some special features that make programming easier.

One of these special features is syntax highlighting. It changes the color of different parts of your program based on what those parts are. For example, strings would be pink, keywords such as if and while would be blue, comments would be grey, and variables would be black. Syntax highlighting makes it easier to detect things such as a string that’s missing its closing quote: the pink text continues past the line that the string is on, all the way to the end of the file (or the next quote that appears later in the program).

Another feature is quote and bracket matching, which helps to make sure that your pairs of quotes and brackets are balanced. When you type a closing delimiter such as }, the editor highlights the opening { that it matches. Different editors do this in different ways, but typical methods are to flash the cursor at the location of the opening {, or to bold the { } pair for a short time. This behavior is helpful for pairs of punctuation that go together: single and double quotes that delimit strings, parentheses, square brackets, and curly braces.

These editors also show the line numbers of your program files. When you get an error message from the PHP engine complaining about a parse error in line 35 in your program, you know where to look for your error.

Table 12-1 lists some PHP-aware editors. Prices are in USD and accurate at the time of writing.

Table 12-1. PHP-aware text editors
Name URL Cost
PhpStorm https://www.jetbrains.com/phpstorm $89
NetBeans https://netbeans.org Free
Zend Studio http://www.zend.com/en/products/studio $89
Eclipse + PDT http://www.eclipse.org/pdt Free
Sublime Text http://www.sublimetext.com $70
Emacs http://ergoemacs.org/emacs/which_emacs.html Free
Vim http://vim.wikia.com/wiki/Where_to_download_Vim Free

PhpStorm, NetBeans, Zend Studio, and Eclipse + PDT are more like traditional integrated development environments (IDEs), whereas Sublime Text, Emacs, and Vim are more like traditional text editors—though they can easily be customized with plugins that help them understand PHP. PhpStorm and Zend Studio are the most PHP-specific of these editors, while the others are made to work with many other programming languages as well. All of the non-free editors in Table 12-1 have free evaluation periods, so you can try them out to see which one is most comfortable for you.

Parse errors happen when the PHP engine comes upon something unexpected in your program. Consider the broken program in Example 12-1.

Example 12-1. A parse error
<?php
if $logged_in) {
        print "Welcome, user.";
    }
?>

When told to run the code in Example 12-1, the PHP engine produces the following error message:

PHP Parse error:  syntax error, unexpected '$logged_in' (T_VARIABLE), 
expecting '(' in welcome.php on line 2

That error message means that in line 2 of the file, the PHP engine was expecting to see an open parenthesis but instead encountered $logged_in, which it thinks is something called a T_VARIABLE. The T_VARIABLE is a token. Tokens are the PHP engine’s way of expressing different fundamental parts of programs. When the engine reads in a program, it translates what you’ve written into a list of tokens. Wherever you put a variable in your program, there is a T_VARIABLE token in the engine’s list.

So, what the PHP engine is trying to tell you with the error message is “I was reading line 2 and saw a variable named $logged_in where I was expecting an open parenthesis.” Looking at line 2 of Example 12-1, you can see why this is so: the open parenthesis that should start the if() test expression is missing. After seeing if, PHP expects a ( to start the test expression. Since that’s not there, it sees $logged_in, a variable, instead.

A list of all the tokens that the PHP engine uses (and therefore that may show up in an error message) can be found in the online PHP Manual.

The insidious thing about parse errors, though, is that the line number reported in the error message is often not the line where the error actually is. Example 12-2 has such an error in it.

Example 12-2. A trickier parse error
<?php
$first_name = "David';
if ($logged_in) {
    print "Welcome, $first_name";
} else {
    print "Howdy, Stranger.";
}
?>

When it tries to run the code in Example 12-2, the PHP engine says:

PHP Parse error:  syntax error, unexpected 'Welcome' (T_STRING) 
in trickier.php on line 4

That error makes it seem like line 4 contains a string (Welcome) in a place where it shouldn’t. But you can scrutinize line 4 all you want to find a problem with it, and you still won’t find one. That line, print "Welcome, $first_name";, is perfectly correct—the string is correctly delimited with double quotes and the line appropriately ends with a semicolon.

The real problem in Example 12-2 is in line 2. The string being assigned to $first_name starts with a double quote but “ends” with a single quote. As the PHP engine reads line 2, it sees the double quote and thinks, “OK, here comes a string. I’ll read everything until the next (unescaped) double quote as the contents of this string.” That makes the engine fly right over the single quote in line 2 and keep going all the way until the first double quote in line 4. When it sees that double quote, the engine thinks it’s found the end of the string. So then it considers what happens after the double quote to be a new command or statement. But what’s after the double quote is Welcome, $first_name";. This doesn’t make any sense to the engine. It’s expecting an immediate semicolon to end a statement, or maybe a period to concatenate the just-defined string with another string. But Welcome, $first_name"; is just an undelimited string sitting where it doesn’t belong. So the engine gives up and shouts out a parse error.

Imagine you’re running down the streets of Manhattan at supersonic speed. The sidewalk on 35th Street has some cracks in it, so you trip. But you’re going so fast that you land on 39th Street and dirty the pavement with your blood and guts. Then a traffic safety officer comes over and says, “Hey! There’s a problem with 39th Street! Someone’s soiled the sidewalk with their innards!”

That’s what the PHP engine is doing in this case. The line number in the parse error is where the engine sees something it doesn’t expect, which is not always the same as the line number where the actual error is.

When you get a parse error from the engine, first take a look at the line reported in the parse error. Check for the basics, such as making sure that you’ve got a semicolon at the end of the statement. If the line seems OK, work your way forward and back a few lines in the program to hunt down the actual error. Pay special attention to punctuation that goes in pairs: single or double quotes that delimit strings, parentheses in function calls or test expressions, square brackets in array elements, and curly braces in code blocks. Count that the number of opening punctuation marks (such as (, [, and {) matches the number of closing punctuation marks (such as ), ], and }).

Situations such as this one are where a PHP-aware editor is really helpful. With syntax highlighting or bracket matching, the editor can tell you about the problem without making you have to hunt around for it. For example, if you’re reading a digital version of this book, the syntax highlighting and color coding of Example 12-2 probably made it very easy to spot the error.

Inspecting Program Data

Once you clear the parse error hurdle, you still may have some work to do before you reach the finish line. A program can be syntactically correct but logically flawed. Just as the sentence “The tugboat chewed apoplectically with six subtle buffaloes” is grammatically correct but meaningless nonsense, you can write a program that the PHP engine doesn’t find any problems with but that doesn’t do what you expect.

Finding and fixing parts of a programs that don’t behave as you expect is a big part of programming. The specifics of how you’d diagnose and explore particular situations vary greatly depending on what you’re trying to fix. This section shows you two techniques for investigating what’s going on in your PHP program. The first, adding debugging output, is easy, but requires modifying your program and may not be suitable for a production environment where regular users can also see the output. The second, using a debugger, requires more work to set up properly, but gives you more runtime flexibility as to how you inspect the running program.

Adding Debug Output

If your program is acting funny, add some checkpoints that display the values of variables. That way, you can see where the program’s behavior diverges from your expectations. Example 12-3 shows a program that incorrectly attempts to calculate the total cost of a few items.

Example 12-3. A broken program
$prices = array(5.95, 3.00, 12.50);
$total_price = 0;
$tax_rate = 1.08; // 8% tax

foreach ($prices as $price) {
    $total_price = $price * $tax_rate;
}

printf('Total price (with tax): $%.2f', $total_price);

Example 12-3 doesn’t do the right thing. It prints:

Total price (with tax): $13.50

The total price of the items should be at least $20. What’s wrong with Example 12-3? One way you can try to find out is to insert a line in the foreach() loop that prints the value of $total_price before and after it changes. That should provide some insight into why the math is wrong. Example 12-4 annotates Example 12-3 with some diagnostic print statements.

Example 12-4. A broken program with debugging output
$prices = array(5.95, 3.00, 12.50);
$total_price = 0;
$tax_rate = 1.08; // 8% tax

foreach ($prices as $price) {
    print "[before: $total_price]";
    $total_price = $price * $tax_rate;
    print "[after: $total_price]";
}

printf('Total price (with tax): $%.2f', $total_price);

Example 12-4 prints:

[before: 0][after: 6.426][before: 6.426][after: 3.24][before: 3.24]
[after: 13.5]Total price (with tax): $13.50

From analyzing the debugging output from Example 12-4, you can see that $total_price isn’t increasing on each trip through the foreach() loop. Scrutinizing the code further leads you to the conclusion that the line:

$total_price = $price * $tax_rate;

should be:

$total_price += $price * $tax_rate;

Instead of the assignment operator (=), the code needs the increment-and-assign operator (+=).

Example 12-5. Printing all submitted form parameters with var_dump()
print '<pre>';
var_dump($_POST);
print '</pre>';

To include an array in debugging output, use var_dump(). It prints all the elements in an array. Surround the output of var_dump() with HTML <pre> and </pre> tags to have it nicely formatted in your web browser. Example 12-5 prints the contents of all submitted form parameters with var_dump().

Debugging messages are informative but can be confusing or disruptive when mixed in with the regular page output. To send debugging messages to the web server error log instead of the web browser, use the error_log() function instead of print. Example 12-6 shows the program from Example 12-4 but uses error_log() to send the diagnostic messages to the web server error log.

Example 12-6. A broken program with error log debugging output
$prices = array(5.95, 3.00, 12.50);
$total_price = 0;
$tax_rate = 1.08; // 8% tax

foreach ($prices as $price) {
    error_log("[before: $total_price]");
    $total_price = $price * $tax_rate;
    error_log("[after: $total_price]");
}

printf('Total price (with tax): $%.2f', $total_price);

Example 12-6 prints just the total price line:

Total price (with tax): $13.50

However, it sends lines to the web server error log that look like this:

[before: 0]
[after: 6.426]
[before: 6.426]
[after: 3.24]
[before: 3.24]
[after: 13.5]

The exact location of your web server error log varies based on how your web server is configured. If you’re using Apache, the error log location is specified by the ErrorLog Apache configuration setting.

Because the var_dump() function itself prints information, you need to do a little fancy footwork to send its output to the error log, similar to the output buffering functionality discussed at the end of “Why setcookie() and session_start() Want to Be at the Top of the Page”. You surround the call to var_dump() with functions that temporarily suspend output, as shown in Example 12-7.

Example 12-7. Sending all submitted form parameters to the error log with var_dump()
// Capture output instead of printing it
ob_start();
// Call var_dump() as usual
var_dump($_POST);
// Store in $output the output generated since calling ob_start()
$output = ob_get_contents();
// Go back to regular printing of output
ob_end_clean();
// Send $output to the error log
error_log($output);

The ob_start(), ob_get_contents(), and ob_end_clean() functions in Example 12-7 manipulate how the PHP engine generates output. The ob_start() function tells the engine, “Don’t print anything from now on. Just accumulate anything you would print in an internal buffer.” When var_dump() is called, the engine is under the spell of ob_start(), so the output goes into that internal buffer. The ob_get_contents() function returns the contents of the internal buffer. Since var_dump() is the only thing that generated output since ob_start() was called, this puts the output of var_dump() into $output. The ob_end_clean() function undoes the work of ob_start(): it tells the PHP engine to go back to its regular behavior with regard to printing. Finally, error_log() sends $output (which holds what var_dump() “printed”) to the web server error log.

Using a Debugger

The printing and logging approach described in the previous section is easy to use. But because it requires modifying your program, you can’t use it in a production environment where regular users might see the debugging output. Also, you need to decide what information you want to print or log before you start running your program. If you haven’t added any code to print a value you’re interested in, you have to modify your program again and rerun it.

Examining your program with a debugger solves these problems. A debugger lets you inspect your program while it is running so you can see the values of variables and which functions call which other functions. It doesn’t require any changes to your program, but it does require some separate setup.

There are a few debuggers that work with PHP, and many of the editors listed in Table 12-1 integrate well with a debugger to allow you to inspect a running PHP program from within your editor. This section shows program inspection with the phpdbg debugger, which comes with PHP.

Tip

The phpdbg debugger is part of PHP versions 5.6 and later, but your installation of the PHP engine may not be configured to include it. If you don’t have a phpdbg program on your system that you can run, check (or ask your system administrator to check) that your PHP installation was built with the --enable-phpdbg option.

The Xdebug debugger is powerful and full-featured. It can communicate with editors and IDEs using a protocol but does not come with an easy-to-use client on its own. Xdebug is free.

The Zend Debugger is part of Zend Studio. It uses its own protocol to communicate with Zend Studio, but some other IDEs, such as PhpStorm, work with it as well.

To start a debugging session with phpdbg, run the phpdbg program with a -e argument indicating what program you want to debug:

phpdbg -e broken.php

phpdbg responds with:

Welcome to phpdbg, the interactive PHP debugger, v0.4.0]
To get help using phpdbg type "help" and press enter
[Please report bugs to <http://github.com/krakjoe/phpdbg/issues>]
[Successful compilation of broken.php] 

This means that phpdbg has read broken.php, has digested the commands in it, and is ready to run it for you. First, we’re going to set a breakpoint. This tells phpdbg to pause whenever it reaches a certain place in the program. When phpdbg pauses, we can inspect the program’s innards. Line 7 is the line where $total_price gets its value assigned within the loop, so let’s break there:

prompt> break 7

The prompt> part is not something to type. phpdbg prints that on its own as a prompt telling you it is ready for a command. The break 7 command tells phpdbg to pause execution when it reaches line 7 of the program. phpdbg responds with:

[Breakpoint #0 added at broken.php:7]

We’re ready to go, so tell phpdbg to start running the program:

prompt> run

It starts from the beginning, running each line of the program until it gets to the breakpoint at line 7. At that point, phpdbg says:

[Breakpoint #0 at broken.php:7, hits: 1]
>00007:     $total_price = $price * $tax_rate;
 00008: }
 00009:

Now we can add a watch point for $total_price. This tells phpdbg to pause program execution each time the value of $total_price changes. This is exactly what we need to diagnose our problem, since it’s the value of $total_price that’s not getting set to what we expect. The watch command adds a watch point:

prompt> watch $total_price

phpdbg responds with:

[Set watchpoint on $total_price]

Now that we have our watch point, we don’t need the breakpoint on line 7 any more. The break del command deletes a breakpoint:

prompt> break del 0

This tells phpdbg to remove the first breakpoint we set (like PHP with array elements, phpdbg starts numbering things with 0, not 1). phpdbg acknowledges the breakpoint deletion with:

[Deleted breakpoint #0]

We are all set to continue running the program and have it pause whenever the value of $total_price changes. The continue command tells phpdbg to keep going:

prompt> continue

phpdbg starts running the program. The first commands that now get executed are the ones in line 7, which change the value of $total_price. So right away program execution is paused, and phpdbg says:

[Breaking on watchpoint $total_price]
Old value: 0
New value: 6.426
>00007:     $total_price = $price * $tax_rate;
 00008: }
 00009:

This is useful—we see that the code is changing $total_price from 0 to 6.426. Let’s see what happens next. The continue command tells phpdbg to get things going again:

prompt> continue

And then the program stops again:

[Breaking on watchpoint $total_price]
Old value: 6.426
New value: 3.24
>00007:     $total_price = $price * $tax_rate;
 00008: }
 00009:

Back again on line 7 in the loop, $total_price goes from 6.426 to 3.24. That definitely doesn’t look right—$total_price should be increasing! Let’s keep going:

prompt> continue

One last time, the value of $total_price gets changed:

[Breaking on watchpoint $total_price]
Old value: 3.24
New value: 13.5
>00007:     $total_price = $price * $tax_rate;
 00008: }
 00009:

This time it increases to 13.5. And a final continue to finish out the program:

prompt> continue

phpdbg keeps running the program, and we get the actual program output:

Total price (with tax): $13.50
[$total_price was removed, removing watchpoint]
[Script ended normally]

The second time phpdbg pauses at the watch point, it is clear that there is a problem with how the value of $total_price is being calculated. This is the same conclusion that the debugging output introduced in the previous section shows.

The specific syntax to type (or places to click in a GUI) may vary with a different debugger or IDE, but the basic idea is the same: the debugger runs your program with special oversight. You get to pause your program execution in the places of your choosing and inspect the program’s guts when it pauses.

Handling Uncaught Exceptions

“Indicating a Problem with Exceptions” explained the basics of how PHP uses exceptions, and Example 6-8 showed what happens if an exception is thrown but not caught: your PHP program stops running and the PHP engine prints out error information and a stack trace (the list of functions that have called one another at the point where the program has stopped).

While you should always include try/catch blocks around any code that might throw an exception, in practice it can be difficult to meet that goal perfectly. You might be using a third-party library and unaware of exceptions it throws, or you might just make a mistake and forget a situation where your own code can throw an exception. For these situations, PHP gives you a way to specify a special exception handler that will get called if your code doesn’t handle an exception. This exception handler is a good place to log information about the exception and present information to your program’s user that is friendlier than a stack trace.

To use a custom exception handler for otherwise uncaught exceptions, do two things:

  1. Write a function that will handle the exception. It takes one argument: the exception.
  2. Use set_exception_handler() to tell the PHP engine about your function.

Example 12-8 sets up an exception handler that prints a nice error message for a user and logs more detailed information about the exception.

Example 12-8. Setting up a custom exception handler
function niceExceptionHandler($ex) {
    // Tell the user something unthreatening
    print "Sorry! Something unexpected happened. Please try again later.";
    // Log more detailed information for a sysadmin to review
    error_log("{$ex->getMessage()} in {$ex->getFile()} @ {$ex->getLine()}");
    error_log($ex->getTraceAsString());
}

set_exception_handler('niceExceptionHandler');

print "I'm about to connect to a made up, pretend, broken database!\n";

// The DSN given to the PDO constructor does not specify a valid database
// or connection parameters, so the constructor will throw an exception
$db = new PDO('garbage:this is obviously not going to work!');

print "This is not going to get printed.";

In Example 12-8, the niceExceptionHandler() function uses print to give the user a simple message and error_log(), along with methods on the Exception object, to log more detailed technical information for review. The call to set_exception_handler() with the niceExceptionHandler argument (as a string) tells the PHP engine to give any unhandled exceptions to that function.

The output from Example 12-8 is:

I'm about to connect to a made up, pretend, broken database!
Sorry! Something unexpected happened. Please try again later.

And the logged error information is:

could not find driver in exception-handler.php @ 17
#0 exception-handler.php(17): PDO->__construct('garbage:this is...')
#1 {main}

This prevents the user from seeing confusing technical details that could potentially leak secure information (such as database credentials or file paths) but stores that information in the error log for review.

A custom exception handler doesn’t prevent your program from stopping after the exception is handled. After the exception handler runs, your program is done. That’s why, in Example 12-8, the This is not going to get printed. line is never printed.

Chapter Summary

This chapter covered:

  • Configuring error display for a web browser, a web server error log, or both
  • Configuring the PHP engine’s error-reporting level
  • Getting the benefits of a PHP-aware text editor
  • Deciphering parse error messages
  • Finding and fixing parse errors
  • Printing debugging information with print, var_dump(), and error_log()
  • Sending var_dump() output to the error log with output buffering functions
  • Inspecting a running program with a debugger
  • Handling exceptions not caught by any other code

Exercises

  1. This program has a syntax error in it:

    <?php
    $name = 'Umberto';
    function say_hello() {
        print 'Hello, ';
        print global $name;
    }
    say_hello();
    ?>

    Without running the program through the PHP engine, figure out what the parse error that gets printed when the engine tries to run the program looks like. What change must you make to the program to get it to run properly and print Hello, Umberto?

  2. Modify the validate_form() function in your answer to Exercise 3 in Chapter 7 (see “Exercise 3”) so that it prints in the web server error log the names and values of all of the submitted form parameters.
  3. Modify your answer to Exercise 4 in Chapter 8 (see “Exercise 4”) to use a custom database error-handling function that prints out different messages in the web browser and in the web server error log. The error-handling function should make the program exit after it prints the error messages.
  4. The following program is supposed to print out an alphabetical list of all the customers in the table from Exercise 4 in Chapter 8 (see “Exercise 4”). Find and fix the errors in it.

    <?php
    // Connect to the database
    try {
        $db = new PDO('sqlite::/tmp/restaurant.db');
    } catch ($e) {
        die("Can't connect: " . $e->getMessage());
    }
    // Set up exception error handling
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    // Set up fetch mode: rows as arrays
    $db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
    // Get the array of dish names from the database
    $dish_names = array();
    $res = $db->query('SELECT dish_id,dish_name FROM dishes');
    foreach ($res->fetchAll() as $row) {
        $dish_names[ $row['dish_id']]] = $row['dish_name'];
    }
    $res = $db->query('SELECT ** FROM customers ORDER BY phone DESC');
    $customers = $res->fetchAll();
    if (count($customers) = 0) {
        print "No customers.";
    } else {
        print '<table>';
        print '<tr><th>ID</th><th>Name</th><th>Phone</th>
        <th>Favorite Dish</th></tr>';
        foreach ($customers as $customer) {
            printf("<tr><td>%d</td><td>%s</td><td>%f</td><td>%s</td></tr>\n",
                   $customer['customer_id'],
                   htmlentities($customer['customer_name']),
                   $customer['phone'],
                   $customer['favorite_dish_id']);
        }
        print '</table>';
    ?>