Building A Generic Error Reporting Class In PHP

Trap script errors and generate consistent error screens in your PHP applications.

Expect The Unexpected

One of the most fundamental tasks during the development cycle for a software application involves writing code to trap and gracefully recover from exceptions.

This might seem pretty obvious, but the reality - as anyone who's ever spent time developing a robust Web application will tell you - is completely different. Most developers, especially those new to the Web paradigm, treat error handling like an unwanted stepchild, preferring instead to concentrate their efforts on implementing and delivering the application against aggressive deadlines. Error handling is usually an after-thought...if it's thought of at all.

This is a shame, because most programming languages - including PHP, which is the subject of this article - come with a full-featured error handling API, which provides you with a number of options when it comes to trapping and resolving errors. Making use of such an error-handling API in a consistent manner throughout the scripts that power your application bears rich dividends: your code is more robust, application test time is shorter (since most errors have been thought through and handled in your code), and users never get the opportunity to see ugly, incomprehensible debug messages generated from the language's internals.

Even if you develop and release a Web application without building in any error-handling routines (either through ignorance or laziness), you can be sure that your customer's going to demand a fix for it in the next release of the software. And since it's one of the few things you're likely to do over and over again when building Web applications, it's worthwhile spending a little time to make the process as painless as possible.

That's where this article comes in. Over the next few pages, I'll be attempting to build a reusable library of functions that allow you to handle script errors in a generic manner, in an attempt to save myself (and, hopefully, you) some time when the next project comes around. The end result of this experiment will be a PHP class that can be easily included in your scripts, and that provides a simple way to trap and display errors consistently. I should warn you at the outset that it may not - in fact, probably will not - meet all your needs; however, the process should be instructive, especially if you're new to object programming in PHP, and you'll have very little difficulty customizing it to your own requirements.

Let's get going!

Back To Class

Before we begin, let's just go over the basics quickly:

In PHP, a "class" is simply a set of program statements which perform a specific task. A typical class definition contains both variables and functions, and serves as the template from which to spawn specific instances of that class.

Once a class has been defined, PHP allows you to spawn as many instances of the class as you like. These instances of a class are referred to as "objects". Each of these instances is a completely independent object, with its own properties and methods, and can thus be manipulated independently of other objects.

This comes in handy in situations where you need to spawn more than one instance of an object - for example, two simultaneous database links for two simultaneous queries, or two shopping carts. Classes also help you to separate your code into independent modules, and thereby simplify code maintenance and changes.

A class definition typically looks like this:

<?php

class ShoppingCart
{
    // this is where the properties are defined

    var $items;
    var $quantities;
    var $prices;
    ...

    // this is where the methods are defined

    function validate_credit_card()
    {
    // code goes here
    }
    ...
}

?>

Once the class has been defined, an object can be spawned with the "new" keyword and assigned to a PHP variable,

<?php

$myCart = new ShoppingCart;

?>

which can then be used to access all object methods and properties.

<?php

// accessing properties
$myCart->items = array("eye of newt", "tongue of frog", "wing of bat", "hair of dog");
$myCart->quantities = array(4, 2, 11, 1);

// accessing methods
$myCart->validate_credit_card();

?>

The Bare Bones

So that's the theory. Let's now spend a few minutes discussing the rationale behind the errorReporter object I plan to build.

Consider the following script, which is representative of the way many PHP applications are coded:


Welcome!

Pick a store:
Men
Women
Children
We're having a special Thanksgiving sale, with up to 60% off on selected items. Come on in and check it out!
<?php // get 3 featured items // open connection to database $connection = mysql_connect("localhost", "user", "pass") or die ("Unable to connect!"); mysql_select_db("store") or die ("Unable to select database!"); // formulate and execute query $query = "SELECT id, label, desc, price,img_small FROM catalog LIMIT 0,3"; $result = mysql_query($query) or die ("Error in query: $query. " . mysql_error()); // iterate through result set // and print data for each item // data consists of image, description, item ID and price while(list($id, $label, $desc, $price, $img) = mysql_fetch_row($result)) { ?>
[] <?php echo $label; ?>
<?php echo $desc; ?>
Only <?php echo $price; ?>
<?php } // clean up mysql_close($connection); ?>
Visit our sponsor: <?php // get sponsor banner // open connection to database $connection = mysql_connect("localhost", "user", "pass") or die ("Unable to connect!"); mysql_select_db("clients") or die ("Unable to select database!"); // formulate and execute query $query = "SELECT bid, banner, url FROM ads WHERE status=1 LIMIT 0,1"; $result = mysql_query($query) or die ("Error in query: $query. " . mysql_error()); // use result set list($bid, $banner, $url) = mysql_fetch_row($result); // clean up mysql_close($connection); // display banner ?> []

As you can see, this script dynamically builds an HTML page, using PHP to generate some components of the data that appears on the page. The PHP business logic is closely intertwined with the HTML interface code, and is executed sequentially, line by line, as the PHP engine parses the document.

Now, consider what happens if an error occurs during execution of the PHP functions near the middle of the script - say, for example, the database server does down. Here's what the resulting output might look like:

Ugly, huh?

Obviously, if you're in the business of building professional Web applications, an incomplete page like the one above opens the door to a minefield of problems - possible further errors if the user attempts to invoke a command on the incomplete page, support calls if the error message is particularly difficult to understand (or absent), and so on. Therefore, it makes sense to ensure that such errors, if they occur, are handled in a graceful manner, with a consistent error screen displayed to the user and no potential to further compound the mistake.

Further, it is possible to classify the errors that occur within an application into two basic categories: serious ("fatal") errors that require the script to terminate immediately, such as a broken database connection or a file I/O error, and less serious ("non-fatal") errors that do not require the script to terminate immediately, such as missing or incorrectly-typed form data.

Keeping this in mind, it's possible to create a simple class that handles error reporting gracefully, producing consistent error messages so that users are protected from the situation described a few pages back. This errorReporter class would consist of the following three components:

  1. Methods that allows the developer to raise, or trigger, errors whenever required, and to add these errors to an "error stack". This error stack is merely a PHP structure (here, an associative array) that holds a list of all the errors (both fatal and non-fatal) encountered.

  2. Methods that may be called by the developer to clear the screen of previously-generated data and re-draw it with an error message (this is necessary to avoid the display of incomplete pages, such as the one above). Fatal errors are reported immediately; reporting of non-fatal errors may be controlled by the developer.

  3. Utility methods to manipulate the error stack.

As you will see, these three basic components make it possible to build a very simple (and yet very useful) errorReporter object.

How Things Work

Now, before proceeding further, I need to decide how this class is going to work. Here's how I plan to use it:

<?php
include_once("errorReporter.class.php");
$e = new errorReporter();

// errors in this section would be considered fatal
// open connection
$connection = mysql_connect("localhost", "user", "pass") or $e->raiseError(102, "LOCALHOST");
mysql_select_db("store") or $e->raiseError(104, "STORE");

// formulate and execute query
$query = "SELECT id, label, desc, price,img_small FROM catalog LIMIT 0,3";
$result = mysql_query($query) or $e->raiseError(101, $query . mysql_error());

// iterate through result set
// and print data for each item
// data consists of image, description, item ID and price
while(list($id, $label, $desc, $price, $img) = mysql_fetch_row($result))
{
?>
<tr>
    <td>
    <img src="<?php echo $img; ?>">
    </td>
    <td>
    <a href="details.php?id=<?php echo $id; ?>"><?php echo $label; ?></a>
    <br>
    <?php echo $desc; ?>
    <br>
    <i>Only <?php echo $price; ?></i>
    </td>
    </tr>
<?php
}

// clean up
mysql_close($connection);

// check for missing form variables
// errors in this section would be considered non-fatal
if (!$sku) { $e->raiseError(1101, "SKU"); }
if (!$price) { $e->raiseError(1101, "PRICE"); }

// display non-fatal errors (if any)
if ($e->numErrors() > 0)
{
    $e->displayNonFatalErrors();
}

// if we get this far, it means the script encountered
// zero fatals and zero non-fatals
// in other words, the script was successfully executed
// display success page

include("success.tmpl");

?>

As you can see, I would like to wrap each function call in my script with a call to the errorReporter class. If a function call fails, an error will be generated via a unique error code and added to the error stack. If the error is evaluated as fatal, an appropriate error message will be immediately displayed to the user; if the error is evaluated as non-fatal, script processing will continue and the developer has the option to throw up an error screen at a later date containing a list of non-fatal errors.

Once the basic functionality of the class is clear, it's a good idea to spend some time listing the important methods, together with their purpose. Here's my initial cut:

raiseError($code, $data) - raise an error using a pre-defined error code and optional debug data;

numErrors() - check the number of errors currently held in the error stack;

displayFatalError($code, $data) - display the fatal error screen;

displayNonFatalErrors() - display the non-fatal error screen;

flushErrors() - reset the error stack.

These are the essential methods; there may be more, which I will add as development progresses.

The Number Game

Right. So I now know how the class is supposed to work, plus I have a fairly good idea of what the methods encapsulated within it will look like. This is a good time to start writing some code.

You might find it helpful to download the complete class code at this point, so that you can refer to it over the next few pages.

Download: errorReporter.zip

Let's begin by setting up the class definition:

// errorReporter.class.php
// class to capture and display errors in a consistent manner

class errorReporter
{

    // snip

}

Now, since I plan to build some basic error-handling routines into this class, I need to add a variable to hold this information.

<?php

class errorReporter
{

    //
    // private variables
    //
    var $_errors = array();

    // snip
}

Note the underscore (_) prefixed to this variable; I've done this in order to provide a convenient way to visually distinguish between private and public class variables and functions.

You'll remember, from my explanation on the previous page, that the errorReporter class allows developers to manually trigger two types of errors - fatal and non-fatal - using a unique error code. Let's define this list of error codes next:

<?php

class errorReporter
{

    // define main error types (add to this list as per your requirements)
    // < 1000 are fatal (script stops immediately)
    // > 1000 are non-fatal (script stops once all non-fatals have been processed)
    var $_errorTypes = array(
    100 => "Database error",
    200 => "File I/O error",
    300 => "Authentication error",
    400 => "Script error",

    1100 => "Form error",
    1200 => "URL error"
    );

    // snip
}

As you can see, I've defined six main error categories, each with a unique identifying code. I've also further classified these error categories into "fatal" and "non-fatal" - that is, errors which should cause immediate script termination, such as a failed database connection or a bad file operation, and errors which are not so serious, such as missing form data (more on this later). Categories with error codes below 1000 are considered fatal, the remaining categories are considered non-fatal.

This list is not exhaustive - you should edit it as per your own needs. The categories above have been created on the basis of my own past experience with Web application errors.

With this broad categorization in mind, it's possible to go down one level further, and define more specific error messages within each category. Take a look:

<?php

class errorReporter
{

    // error subtypes
    // linked to above via primary type
    var $_errorsubTypes = array(
    101 => "Error in SQL query",
    102 => "Could not connect to database server",
    103 => "Database already exists",
    104 => "Could not select database",

    201 => "Could not create directory",
    202 => "Could not copy file(s)",
    203 => "Could not execute command",
    204 => "Could not read configuration file",
    205 => "Could not delete file(s)",
    206 => "Could not read file(s)",
    207 => "Could not write file(s)",
    208 => "Could not open file(s)",
    209 => "File already exists",
    210 => "Could not delete directory",

    301 => "Session expired",

    401 => "An internal script error occurred",

    1101 => "Required field empty",
    1102 => "Invalid data in field",
    1103 => "File upload unsuccessful",

    1201 => "Missing record identifier(s)"
    );

    // snip
}

This pre-defined list of error codes and messages covers the most common errors developers experience when building Web applications. Since this list is defined once, and indexed by a unique numeric code, it may be reused over and over, in different scripts, to display consistent error messages. And in the event that a particular error message is deemed confusing or difficult to understand, all you need to do is update this master list, and the change will be immediately reflected across all error screens thrown up by the application.

Running On Empty

Let's now begin constructing the class methods. I'll start with the constructor:

<?php

class errorReporter
{
    // constructor
    function errorReporter()
    {

        // initialize error stack
        $this->flushErrors();

        // start output buffering
        ob_start();
    }

    // snip

}

The first thing the constructor does is initialize the error stack, via the flushErrors() method. Here's what this method looks like:

<?php

class errorReporter
{
    // clean stack
    function flushErrors()
    {
        $this->_errors = array();
        return $this->_errors;
    }

    // snip

}

Once that's done, the constructor uses the ob_start() function to define a special output buffer which stores all the output generated by the script during its lifetime. When I do this, the output of the script is never seen by the user unless I explicitly make the contents of this buffer visible via a call to PHP's output control API.

Once a buffer has been defined, the script proceeds to execute as usual. When you've decided that it's time to display the contents of the buffer to the user, you can simultaneously end output buffering and send the contents of the current buffer to the browser. Alternative, you can also clear the contents of this buffer at any time via a call to ob_clean() (as you will see on the next page, this is the function I will be using to clear and re-draw the screen when an error occurs).

Raising An Alarm

Next, the real meat of this class - the method used to raise errors and add them to the error stack:

<?php

class errorReporter
{
    // manually raise an error
    function raiseError($subtype=NULL, $data="No additional information available")
    {
        $this->_errors[] = array("subtype" => $subtype, "data" => $data);

        if ($this->_getErrorType($subtype) < 1000)
        {
            // fatal
            $this->displayFatalError($subtype, $data);
        }

        return $this->_errors;
    }

    // snip

}

The raiseError() method is invoked by the developer whenever a need arises to capture an error in a script; it must be sent an error code identifying the error type, together with an optional explanatory message or debug data. The error thus raised is added to the $_errors array, for further processing as and when needed.

In the event that the error raised is a fatal error, the raiseError() class also invokes the displayFatalError() method. The determination of whether or not an error is fatal is handled by the _getErrorType() private method, which merely checks the error code to see if it's above or below 1000.

<?php

class errorReporter
{
    // internal function to map subtype to primary type
    function _getErrorType($subtype)
    {
        return floor($subtype/10)*10;
    }

    // snip

}

If an error is fatal, the displayFatalError() method is invoked. Here's what it looks like:

<?php

class errorReporter
{
    // fatal error handler
    // clear previously-drawn screen
    // display error template with message
    // stop script processing and die()
    function displayFatalError($subtype, $data)
    {
        ob_clean();
        $errorType = $this->_errorTypes[$this->_getErrorType($subtype)];
        $errorCode = $subtype;
        $errorMsg = $this->_errorsubTypes[$subtype];
        $addInfo = $data;
        include("error-fatal.php");
        die;
    }

    // snip

}

This method receives the error code and debug data as input, and has to perform a number of important tasks.

  1. First, it needs to clear the output buffer of all previously generated content, so as to avoid the mangled screen I showed you near the beginning of this article. In order to do this, it uses the ob_clean() function discussed previously.

  2. Next, it needs to display an error template, and insert appropriate content into it reflecting the nature of the error. Using the error code passed to it as argument, this function looks up the $_errorsubTypes array defined at the top of the class, and maps the error code to the corresponding human-readable error message. This data is then inserted into the error template, which contains placeholders for the error type, error message and debug data and takes care of providing the consistent error handling earlier stated as one of the design goals for this class.

  3. Finally, once the error screen has been displayed, it has to terminate further script processing via a call to die().

The error template to be displayed here is contained within a separate file, named "error-fatal.php", and include()-d where needed by displayFatalError(). This template can be customized to your specific requirements - here's my bare-bones version:


Error!

The following fatal error occurred:

<?=$errorType?> (error code <?=$errorCode?>)
<?=$errorMsg?> (<?=$addInfo?>)

Wanna see how it works? Here's an example of how you could use the errorReporter in a script to trap fatal errors,

<?php
// include class
include_once("errorReporter.class.php");

// initialize error reporter
$e = new errorReporter();

echo "Beginning work...";

// write to file
$filename = "data.txt";
$fd = fopen ($filename, "wb") or $e->raiseError(208, $filename);
$ret = fwrite ($fd, "aloha!") or $e->raiseError(207, $filename);
fclose ($fd);

// zero errors?
// display success code
echo "Success!";
?>

and here's an example of the error screen displayed if an error occurs during script execution:

Note the manner in which the output generated by the script above is first cleared from the output buffer by the errorReporter class, and is then completely replaced with the template containing an appropriate error message. No more mangled screens, no more confused users!

A Well-Formed Idea

Unlike fatal errors, which immediately cause script termination when they are raised, non-fatal errors merely get registered in the error stack - the developer is free to display these non-fatal errors at any time (or even ignore them altogether).

Here's what the displayNonFatalErrors() method looks like:

<?php

class errorReporter
{
    // non-fatal error handler
    // clear previously-drawn screen
    // build an array of non-fatal errors
    // and then die()
    function displayNonFatalErrors()
    {
        ob_clean();

        $errorList = array();
        for($x=0; $x<$this->numErrors(); $x++)
        {
            $errorType = $this->_errorTypes[$this->_getErrorType($this->_errors[$x]["subtype"])];
            $errorCode = $this->_errors[$x]["subtype"];
            $errorMsg = $this->_errorsubTypes[$this->_errors[$x]["subtype"]];
            $addInfo = $this->_errors[$x]["data"];
            $errorList[] = array($errorType, $errorCode, $errorMsg, $addInfo);
        }
        include("error-non-fatal.php");
        die;
    }

    // snip

}

The only difference between the internals of this method, and those of the displayFatalErrors() method discussed on the previous page, is that this method builds and returns an array of one or more non-fatal errors rather than a single error. The output buffer is cleared of previously-generated data, and then replaced by a non-fatal error template, which takes care of formatting and neatly displaying the list of errors. Script execution is then terminated.

Here's what my non-fatal error template looks like:


Error!

The following non-fatal errors occurred:
<?php foreach ($errorList as $e) { ?>
<?=$e[0]?> (<?=$e[1]?>)
<?=$e[2]?> (<?=$e[3]?>) <?php } ?>

Pretty simple, this - this script accepts the $errorList array created by the displayNonFatalErrors() method and iterates through it to create a bulleted list of errors.

It should be noted that I added this type of error primarily to handle form validation tasks - if you have a large or complex form to validate, it's inconvenient to terminate script processing every time a validation routine fails (as would happen if I only used fatal errors). Non-fatal errors make it possible to perform a series of validation routines on form data, store the errors that occur in an array, and then display all the errors at one time for the user to view and correct.

Here's an example of how you could use the errorReporter's non-fatal errors while performing form validation:

<?php if (!$_POST['submit']) { ?> Name:

Favourite sandwich filling:

Favourite ice-cream flavour:

<?php } else { // include class include_once("errorReporter.class.php"); // initialize error reporter $e = new errorReporter(); // perform form validation on data entered by user if (empty($_POST['name'])) { $e->raiseError(1101, "NAME"); } if (empty($_POST['filling'])) { $e->raiseError(1101, "FILLING"); } if (empty($_POST['flavour'])) { $e->raiseError(1101, "FLAVOUR"); } // check to see if any errors // display if so if ($e->numErrors() > 0) { $e->displayNonFatalErrors(); } // if we get this far, it means // there were no errors in the form data // now let's insert this data into a database $name = $_POST['name']; $filling = $_POST['filling']; $flavour = $_POST['flavour']; $connection = mysql_connect("localhost", "user", "pass") or $e->raiseError(102, "LOCALHOST"); mysql_select_db("users") or $e->raiseError(104, "USERS"); $query = "INSERT INTO profile (name, filling, flavour) VALUES ('$name', '$filling', '$flavour')"; $result = mysql_query($query) or $e->raiseError(101, $query . mysql_error()); // all done? // display success code echo "Success!"; } ?>

And here's an example of the error screen you might see:

Going To The Source

Of course, this is just my first stab at a generic error reporting class. It's designed for very simple requirements, and may be way too primitive for your needs. If this is the case, you have two basic options:

  1. File the results of my efforts in the trash can and write your own, much-cooler, does-everything-but-make-toast class;

  2. Pick up a free, open-source PHP class which offers a more powerful feature set.

If you picked door one, you don't really need my help any more. You can stop reading right now and get to work. Have fun, and remember to send me a Christmas card if you sell your software for a million bucks.

If, on the other hand, you're lazy and figured that door two was more promising, you'll be happy to hear that the Web has a huge number of powerful error handling classes floating around it, many of them extremely powerful. Here are two that looked particularly interesting:

Gyozo Papp's ErrorHandler class, at http://gremlins.mirrors.phpclasses.org/browse.html/package/345.html

Lennart Groetzbach's debugHelper class, at http://gremlins.mirrors.phpclasses.org/browse.html/package/891.html

You can also read more about the material discussed in this article, at the following links:

PHP output control functions, at http://www.php.net/manual/en/ref.outcontrol.php

PHP error handling functions, at http://www.php.net/manual/en/ref.errorfunc.php

Classes in PHP, at http://www.php.net/manual/en/language.oop.php

And that's about all for the moment. In this article, you expanded your knowledge of PHP's OOP capabilities by actually using all that theory to build something useful - an error reporting widget which can be wrapped around your PHP scripts to provide consistent and reusable error messages without breaking your Web pages.

If you're a stressed-out Web developer working on a Web site or application, you might find this object a handy tool in your next development effort. If you're a novice programmer struggling to understand how OOP can make your life easier, I hope this article offered some pointers, as well as some illustration of how object-oriented programming works. And if you don't fit into either of those categories - well, I hope you found it interesting and informative, anyway.

See you soon!

Note: All examples in this article have been tested on Linux/i586 with PHP 4.2.3. Examples are illustrative only, and are not meant for a production environment. Melonfire provides no warranties or support for the source code described in this article. YMMV!

This article was first published on02 Dec 2002.