Intended Audience
Introduction
Error Handling Before PHP 5
•  Errors at Script Level
•  Returning Error Flags

Exceptional Code - Part 2

Intended Audience

This article is intended for experienced PHP programmers interested in learning more about PHP 5's new Exception support. You should be comfortable with the basics of object-oriented programming, including the anatomy of a class and the mechanics of inheritance.

Introduction

Most technical articles skimp on error handling. This is understandable since clauses that check for error conditions tend to obscure otherwise good, clean example code. This article goes to the other extreme. Here you will encounter plenty of error handling, and very little else.

PHP 5 introduced exceptions, a new mechanism for handling errors in an object context. As you will see, exceptions provide some significant advantages over more traditional error management techniques.

Error Handling Before PHP 5

Before the advent of PHP 5 most error handling took place on two levels. You could:

  • Return an error flag from your method or function, and perhaps set a property or global variable that could be checked later on, or
  • Generate a script-level warning or a fatal error using the trigger_error() or die() functions.

Errors at Script Level

You can use the die() pseudo-function to end script execution when there is no sensible way of continuing. You will often see this in quick and dirty script examples. Here is a simple class that attempts to load a class file from a directory:

<?php
// PHP 4
require_once('cmd_php4/Command.php');
class CommandManager {
    var
$cmdDir = "cmd_php4";

    function
getCommandObject($cmd) {
        
$path = "{$this->cmdDir}/{$cmd}.php";
        if (!
file_exists($path)) {
            die(
"Cannot find $path\n");
        }
        require_once
$path;

        if (!
class_exists($cmd)) {
            die(
"class $cmd does not exist");
        }

        
$ret = new $cmd();
        if (!
is_a($ret, 'Command')) {
            die(
"$cmd is not a Command");
        }
        return
$ret;
    }
}
?>

This is a simplified example of what is known as the Command Pattern. The client coder can save a class to a command directory ('cmd_php4' in this case). As long as the file takes the same name as the class it contains, and this class is a child of a base class called Command, our method should generate a usable Command object given a simple string. The Command base class defines an execute() method, so we know that anything returned by getCommandObject() will implement execute().

Let's look at the Command class, which we store in cmd_php4/Command.php:

<?php
// PHP 4
class Command {
    function
execute() {
        die(
"Command::execute() is an abstract method");
    }
}
?>

As you can see, Command is a PHP 4 implementation of an abstract class. When we shift over to PHP 5 later in the chapter, we will implicitly use a cleaner PHP 5 version of this (defined in command/Command.php):

<?php
// PHP 5
abstract class Command {
    abstract function
execute();
}
?>

Here's a vanilla implementation of a command class. It is called realcommand, and can also be found in the command directory: cmd_php4/realcommand.php:

<?php
// PHP 4
require_once 'cmd_php4/Command.php';
class
realcommand extends Command {
    function
execute() {
        print
"realcommand::execute() executing as ordered sah!\n";
    }
}
?>

A structure like this can make for flexible scripts. You can add new Command classes at any time, without altering the wider framework. As you can see though, you have to watch out for a number of potential show stoppers. We need to ensure that the class file exists where it should, that the class itself is present, and that it subclasses Command.

If any of our tests fail, script execution is ended abruptly. This is safe code, but it's inflexible. This extreme response is the only positive action that the method can take. It is responsible only for finding and instantiating a Command object. It has no knowledge of any steps the wider script should take to handle a failure, nor should it. If you give a method too much knowledge of the context in which it runs it will become hard to reuse in different scripts and circumstances.

Although using die() circumvents the dangers of embedding script logic in the getCommandObject() method, it nonetheless imposes a drastic error response on the script as a whole. Who says that failure to locate a command should kill the script? Perhaps a default Command should be used instead, or maybe the command string could be reprocessed.

We could perhaps make things a little more flexible by generating a user warning instead:

<?php
// PHP 4
require_once('cmd_php4/Command.php');
class CommandManager {
    var
$cmdDir = "cmd_php4";

    function
getCommandObject($cmd) {
        
$path = "{$this->cmdDir}/{$cmd}.php";
        if (!
file_exists($path)) {
            
trigger_error("Cannot find $path", E_USER_ERROR);
        }
        require_once
$path;

        if (!
class_exists($cmd)) {
            
trigger_error("class $cmd does not exist", E_USER_ERROR);
        }

        
$ret = new $cmd();
        if (!
is_a($ret, 'Command')) {
            
trigger_error("$cmd is not a Command", E_USER_ERROR);
        }
        return
$ret;
    }
}
?>

If you use the trigger_error() function instead of die() when you encounter an error, you provide client code with the opportunity to handle the error. trigger_error() accepts an error message, and a constant integer, one of:

E_USER_ERROR A fatal error
E_USER_WARNING A non-fatal error
E_USER_NOTICE A report that may not represent an error

You can intercept errors generated using the trigger_error() function by associating a function with set_error_handler():

<?php
// PHP 4
function cmdErrorHandler($errnum, $errmsg, $file, $lineno) {
    if(
$errnum == E_USER_ERROR) {
        print
"error: $errmsg\n";
        print
"file: $file\n";
        print
"line: $lineno\n";
        exit();
    }
}

$handler = set_error_handler('cmdErrorHandler');
$mgr = new CommandManager();
$cmd = $mgr->getCommandObject('realcommand');
$cmd->execute();
?>

As you can see, set_error_handler() accepts a function name. If an error is triggered, the given function is invoked with four arguments: the error flag, the message, the file, and the line number at which the error was triggered. You can also set a handler method by passing an array to set_error_handler(). The first element should be a reference to the object upon which the handler will be called, and the second should be the name of the handler method.

Although you can do some useful stuff with handlers, such as logging error information, outputting debug data and so on, they remain a pretty crude way of handling errors.

Your options are limited as far as action is concerned. In catching an E_USER_ERROR error with a handler, for example, you could override the expected behavior and refuse to kill the process by calling exit() or die() if you want. If you do this, you must reconcile yourself to the fact that application flow will resume where it left off. This could cause some pretty tricky bugs in code that expects an error to end execution.

Returning Error Flags

Script level errors are crude but useful. Usually, though, more flexibility is achieved by returning an error flag directly to client code in response to an error condition. This delegates error handling to calling code, which is usually better equipped to decide how to react than the method or function in which the error occurred.

Here we amend the previous example to return an error value on failure. (false is usually a good choice.)

<?php
// PHP 4
require_once('cmd_php4/Command.php');
class CommandManager {
    var
$cmdDir = "cmd_php4";

    function
getCommandObject($cmd) {
        
$path = "{$this->cmdDir}/{$cmd}.php";
        if (!
file_exists($path)) {
            return
false;
        }
        require_once
$path;

        if (!
class_exists($cmd)) {
            return
false;
        }

        
$ret = new $cmd();
        if (!
is_a($ret, 'Command')) {
            return
false;
        }
        return
$ret;
    }
}
?>

This means that you can handle failure in different ways according to circumstances. The method might result in script failure:

<?php
// PHP 4
$mgr = new CommandManager();
$cmd = $mgr->getCommandObject('realcommand');
if (
is_bool($cmd)) {
    die(
"error getting command\n");
} else {
    
$cmd->execute();
}
?>

or just a logged error:

<?php
// PHP 4
$mgr = new CommandManager();
$cmd = $mgr->getCommandObject('realcommand');
if(
is_bool($cmd)) {
    
error_log("error getting command\n", 0);
    }
else {
    
$cmd->execute();
}
?>

One problem with error flags such as false (or -1, or 0) is that they are not very informative. You can address this by setting an error property or variable that can be queried after a failure has been reported:

<?php
// PHP 4
require_once('cmd_php4/Command.php');
class CommandManager {
    var
$cmdDir = "cmd_php4";
    var
$error_str = "";

    function
setError($method, $msg) {
        
$this->error_str  =
        
get_class($this)."::{$method}(): $msg";
    }

    function
error() {
        return
$this->error_str;
    }

    function
getCommandObject($cmd) {
        
$path = "{$this->cmdDir}/{$cmd}.php";
        if (!
file_exists($path)) {
            
$this->setError(__FUNCTION__, "Cannot find $path\n");
            return
false;
        }
        require_once
$path;

        if (!
class_exists($cmd)) {
            
$this->setError(__FUNCTION__, "class $cmd does not exist");
            return
false;
        }

        
$ret = new $cmd();
        if (!
is_a($ret, 'Command')) {
            
$this->setError(__FUNCTION__, "$cmd is not a Command");
            return
false;
        }
        return
$ret;
    }
}
?>

This simple mechanism allows methods to log error information using the setError() method. Client code can query this data via the error() method after an error has been reported. You should extract this functionality and place it in a base class that all objects in your scripts extend. If you fail to do this, client code might be forced to work with classes that implement subtly different error mechanisms. I have seen projects that contain getErrorStr(), getError(), and error() methods in different classes.

It isn't always easy to have all classes extend the same base class, however. What would you do, for example, if you want to extend a third party class? Of course, you could implement an interface, but if you are doing that, then you have access to PHP 5, and, as we shall see, PHP 5 provides a better solution altogether.

You can see another approach to error handling in the PEAR packages. When an error is encountered PEAR packages return a Pear_Error object (or a derivative). Client code can then test the returned value with a static method: PEAR::isError(). If an error has been encountered, then the returned Pear_Error object provides all the information you might need including:

PEAR::getMessage() - the error message
PEAR::getType() - the Pear_Error subtype
PEAR::getUserInfo() - additional information about the error or its context
PEAR::getCode() - the error code (if any)

Here we alter the getCommandObject() method so that it returns a Pear_Error object when things go wrong:

<?php
// PHP 4
require_once("PEAR.php");
require_once('cmd_php4/Command.php');

class
CommandManager {
    var
$cmdDir = "cmd_php4";

    function
getCommandObject($cmd) {
        
$path = "{$this->cmdDir}/{$cmd}.php";
        if (!
file_exists($path)) {
            return
PEAR::RaiseError("Cannot find $path");
        }
        require_once
$path;

        if (!
class_exists($cmd)) {
            return
            
PEAR::RaiseError("class $cmd does not exist");
        }

        
$ret = new $cmd();
        if (!
is_a($ret, 'Command')) {
            return
            
PEAR::RaiseError("$cmd is not a Command");
        }
        return
$ret;
    }
}
?>

Pear_Error is neat for client code because it both signals that an error has taken place, and contains information about the nature of the error.

<?php
// PHP 4
$mgr = new CommandManager();
$cmd = $mgr->getCommandObject('realcommand');
if (
PEAR::isError($cmd)) {
    print
$cmd->getMessage()."\n";
    exit;
}
$cmd->execute();
?>

Although returning an error value allows you to respond to problems flexibly, it has the side effect of polluting your interface.

PHP does not allow you to dictate the type of value that a method or function should return, in practice, though it is convenient to be able to rely upon consistent behavior. The getCommandObject() method returns either a Command object or a Pear_Error object. If you intend to work with the method's return value you will be forced to test its type every time you call the method. A cautious script can become a tangle of error check conditionals, as every return type is tested.

Consider this PEAR::DB client code presented without error checking:

<?php
// PHP 4
require_once("DB.php");
$db = "errors.db";
unlink($db);
$dsn = "sqlite://./$db";
$db = DB::connect($dsn);
$create_result = $db->query("CREATE TABLE records(name varchar(255))");
$insert_result = $db->query("INSERT INTO records values('OK Computer')");
$query_result = $db->query("SELECT * FROM records");
$row = $query_result->fetchRow(DB_FETCHMODE_ASSOC);
print
$row['name']."\n";
$drop_result = $db->query("drop TABLE records");
$db->disconnect();
?>

The code should be readable at a glance. We open a database, create a table, insert a row, extract the row, and drop the table. Look what happens when we code defensively:

<?php
// PHP 4
require_once("DB.php");
$db = "errors.db";
unlink($db);
$dsn = "sqlite://./$db";

$db = DB::connect($dsn);
if (
DB::isError($db)) {
    die (
$db->getMessage());
}

$create_result = $db->query("CREATE TABLE records (name varchar(255))");
if (
DB::isError($create_result)) {
    die (
$create_result->getMessage());
}

$insert_result = $db->query("INSERT INTO records values('OK Computer')");
if (
DB::isError($insert_result)) {
    die (
$insert_result->getMessage());
}

$query_result = $db->query("SELECT * FROM records");
if (
DB::isError($query_result)) {
    die (
$query_result->getMessage());
}

$row = $query_result->fetchRow(DB_FETCHMODE_ASSOC);
print
$row['name']."\n";

$drop_result = $db->query("drop TABLE records");
if (
DB::isError($drop_result)) {
    die (
$drop_result->getMessage());
}

$db->disconnect();
?>

Admittedly, we might be a little less paranoid than this in real-world code, but this should illustrate the tangle that can result from inline error checking.

So what we need is an error management mechanism that:

  • Allows a method to delegate error handling to client code that is better placed to make application decisions
  • Provides detailed information about the problem
  • Lets you handle multiple error conditions in one place, separating the flow of your code from failure reports and recovery strategies
  • Does not colonize the return value of a method
PHP 5's exception handling scores on all these points.



Exceptional Code - Part 2


Matt Zandstra is a writer and consultant specializing in server programming and training. With his business partner, Max Guglielmino, he runs Corrosive, a technical agency that provides open source/open standards training and plans, designs and builds Internet applications.

Matt is the author of SAMS Teach Yourself PHP in 24 Hours. He is currently working on a book about object-oriented PHP.