Imagine you’ve got 20 lines of PHP code. There’s an UPDATE SQL statement deep within some nested logic statements. That UPDATE is just not working, even though you expect it to. You know that because you check the database, and the values in your table are not being updated.

The code looks like this:

<?php
...
if ($blah == 0) {
    ...
    $banana = !empty($_POST['banana']) ? $_POST['banana'] : '';
    $coconut = $_POST['coconut'];
    ...
    if (!empty($banana)) {
        $res = $db->query("SELECT ... SOME SQL THING");
        ...
        if ($res->num_rows > 0) {
            // avoid duplicates
            exit();
        } else {
            $insert = $db->query("INSERT INTO .... MORE SQL");

            if ($insert && !empty($coconut)) {
                $update = $db->query("UPDATE ... ANOTHER TABLE");
                // Gnash!! The UPDATE does not take place as expected!
                ...
// etc.
?>

How do you debug something like this? Here’s my process.

First, I look at the PHP logs. If error logging is enabled, it may be that some error is being logged which points to the problem right away. In that case, I might not have to do any further debugging. If I’m lucky, the fix is quick and easy! Maybe I’ll see some SQL error, like Undefined index or foreign key constraint fails, that makes the problem clear.

But suppose error logging is disabled. Then I enable it, at least for this script.

To do that, I add error_reporting(E_ALL); to the top of the script. Then, as a good test, I also add error_log('bananas'); to the top of the script, run the script, and check to make sure that my comment, “bananas”, was logged in the expected error log file. If it wasn’t, I’ll need to hunt down the file where errors are being logged! I won’t go into that here, though. Let’s assume I’ve found my error log, and I can print statements to it.

If I don’t see any errors in the log, I add a bunch of logging statements to figure out exactly where in the code things go wrong. Look at the code above, again. There are so many possible ways that the UPDATE statement might not have run – every logic branch needs to be tested to see where execution stops. If I expect that my UPDATE statement is definitely being run, I can log a short comment just before that, run the code, and check to see if that comment appears in the logs.

If my framework already has some form of logging enabled, great, I might just use that to add debugging comments. If not, that’s okay. As mentioned above, PHP comes with built-in error logging. I might add something like error_log("before UPDATE"), run the script, and check to see if my comment appears in the error log. If I don’t see “before UPDATE” in the logs, I’d probably add a bunch of error_log statements immediately after every branch in the logic: after each if and else statement in the code above.

Usually, by adding logging statements, I can quickly figure out where the problem occurs. Where does execution stop? Then, I add more debug statements to figure out why, maybe printing out variables.

A word of caution: before adding logging, I often copy the file that I’m going to be working on to a safe place. I do that something like this: cp -p myfile.php myfile.php.2019-10-21 (the -p option to cp preserves the file’s mode, permissions, and timestamps). This is true even if I’m working on code that is checked into a version control system such as git. It’s just a really quick way to be able to revert my changes, if needed.

There are other things that I might do to figure out what’s causing this bug, too. If the source code has a test framework in place, it can be super helpful to add tests which exercise the code. That way, I can isolate the buggy code outside of the website, and test if it works on its own. Usually, when a bug is reported, I do like to add at least one test which exhibits the error by failing, and which then passes when the bug is fixed. But I don’t always have the luxury of working with source code that has a test framework set up.

Note: I’m usually working in a Linux environment, so YMMV with any commands (cp) mentioned above.