Exceptional Code: error handling in PHP 4 and PHP 5

By Matt Zandstra

July 29, 2004

Intended Audience
Introduction
Error Handling Before PHP 5
?   Errors at Script Level
?   Returning Error Flags
Exceptions in PHP 5
?   Using the throw Keyword
?   The try-catch Statement
?   Handling Multiple Errors
?   Subclassing Exception
?   Passing the Buck
?   More Information about an Exception
Summary
About the Author

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燾lauses 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_ERRORA fatal error
E_USER_WARNINGA non-fatal error
E_USER_NOTICEA 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.

Exceptions in PHP 5

We have now discussed error handling in some detail. Although we have yet to encounter our first exception (!), the ground we have covered should go some way to illustrating the needs that exceptions meet.

The built-in Exception class includes the following methods:

__construct()The constructor. Requires a message string and an optional integer flag.
getMessage()The error message (as passed to the constructor)
getCode()The error code (as passed to the constructor)
getFile()Returns the path for the file in which the Exception was generated.
getLine()Returns the line number at which the Exception was generated
getTrace()An array that provides information about each step in the progress of an Exception
getTraceAsString()As getTrace() but in string format

As you can see, the Exception class is similar in structure to Pear_Error. When you encounter an error in your script you can create your own Exception object:

$ex = new Exception( "Could not open $this->file" );

The Exception class constructor optionally accepts an error message and an integer error code.

Using the throw Keyword

Having created an Exception object you could then return it as you might a Pear_Error object, but you shouldn't! Use the throw keyword instead. throw is used with an Exception object:

throw new Exception( "my message", 44 );

throw ends method execution abruptly, and makes the associated Exception object available to the client context. Here's our getCommandObject() method amended to use exceptions:

<?php
// PHP 5
require_once('command/Command.php');
class CommandManager {
    private
$cmdDir = "command";

    function
getCommandObject($cmd) {
        
$path = "{$this->cmdDir}/{$cmd}.php";
        if (!
file_exists($path)) {
            throw new
Exception("Cannot find $path");
        }
        require_once
$path;
        if (!
class_exists($cmd)) {
            throw new
Exception(
                
"class $cmd does not exist");
        }

        
$class = new ReflectionClass($cmd);
        if (!
$class->isSubclassOf(new ReflectionClass('Command'))) {
            throw new
Exception("$cmd is not a Command");
        }
        return new
$cmd();
    }
}
?>

We use ReflectionClass from the Reflection API to check that the given class name belongs to the Command type. Running this code with an invalid file path will result in an error like this:

Fatal error: Uncaught exception 'Exception' with message 'Cannot find command/xrealcommand.php' in /home/xyz/BasicException.php:10
Stack trace:
#0 /home/xyz/BasicException.php(26):
CommandManager->getCommandObject('xrealcommand')
#1 {main}
  thrown in /home/xyz/BasicException.php on line 10

As you can see, throwing an Exception results in a fatal error by default. This means that code that uses exceptions has safety built-in. An error flag, on the other hand, does not provide any default behavior. Failure to handle an error flag simply allows your script to continue execution using an inappropriate value.

The try-catch Statement

In order to handle an Exception at the client end, we must use a try-catch statement. This consists of a try clause and at least one catch clause. Any code that invokes a method that might throw an Exception should be wrapped in the try clause. The catch clause is used to handle the Exception should it be thrown. Here's how we might handle an error thrown from getCommandObject():

<?php
// PHP 5
try {
    
$mgr = new CommandManager();
    
$cmd = $mgr->getCommandObject('nrealcommand');
    
$cmd->execute();
} catch (
Exception $e) {
    print
$e->getMessage();
    exit();
}
?>

As you can see, the Exception object is made available to the catch clause via an argument list similar to the kind you might find in a method or function declaration. We can query the provided Exception object in order to get more information about the error. By using the throw keyword in conjunction with the try-catch statement we avoid polluting our method's return value with an error flag.

If an Exception is thrown, execution within the try clause will cease abruptly, and flow will switch immediately to the catch clause.

As we have seen, if an Exception is left uncaught, a fatal error results.

Handling Multiple Errors

Exception handling so far has not been so different from code that checks return values for error flags or objects. Let's make the CommandManager class a bit more cautious, and have it check the command directory in the constructor:

<?php
// PHP 5
require_once('command/Command.php');
class CommandManager {
    private
$cmdDir = "command";

    function
__construct() {
        if (!
is_dir($this->cmdDir)) {
            throw new
Exception(
            
"directory error: $this->cmdDir");
        }
    }

    function
getCommandObject($cmd) {
        
$path = "{$this->cmdDir}/{$cmd}.php";
        if (!
file_exists($path)) {
            throw new
Exception("Cannot find $path");
        }
        require_once
$path;
        if (!
class_exists($cmd)) {
            throw new
Exception("class $cmd does not exist");
        }

        
$class = new ReflectionClass($cmd);
        if (!
$class->isSubclassOf(new ReflectionClass('Command'))) {
            throw new
Exception("$cmd is not a Command");
        }
        return new
$cmd();
    }
}
?>

There are now two invocations that might cause an error. However, we don't need to change our client code at all. You can include as many statements as you want in a try clause, and still handle all errors in one place. If the CommandManager object's constructor throws an exception then execution in the try clause stops, and the catch clause is invoked with the relevant Exception object. The same is true of the getCommandObject() invocation. So we have two potential causes of error in one place, and a single clause for managing any problems in another. This allows you to write cleaner code without sacrificing error handling.

<?php
// PHP 5
try {
    
$mgr = new CommandManager(); // potential error
    
$cmd = $mgr->getCommandObject('realcommand');
                                
// another potential error
    
$cmd->execute();
} catch (
Exception $e) {
    
// handle either error here
    
print $e->getMessage();
    exit();
}
?>

There is one problem that we haven't yet addressed. How do we distinguish between different types of error? For example, we may wish to deal with a missing directory in one way, and an illegal command class in another.

The Exception class accepts an optional integer error flag, which is one way of distinguishing between error types in your catch clause.

<?php
// PHP 5
require_once('command/Command.php');
class CommandManager {
    private
$cmdDir = "command";
    const
CMDMAN_GENERAL_ERROR = 1;
    const
CMDMAN_ILLEGALCLASS_ERROR = 2;

    function
__construct() {
        if (!
is_dir($this->cmdDir)) {
            throw new
Exception("directory error: $this->cmdDir", self::CMDMAN_GENERAL_ERROR);
        }
    }

    function
getCommandObject($cmd) {
        
$path = "{$this->cmdDir}/{$cmd}.php";
        if (!
file_exists($path)) {
            throw new
Exception("Cannot find $path", self::CMDMAN_ILLEGALCLASS_ERROR);
        }
        require_once
$path;
        if (!
class_exists($cmd)) {
            throw new
Exception("class $cmd does not exist", self::CMDMAN_ILLEGALCLASS_ERROR);
        }

        
$class = new ReflectionClass($cmd);
        if (!
$class->isSubclassOf(new ReflectionClass('Command'))) {
            throw new
Exception("$cmd is not a Command", self::CMDMAN_ILLEGALCLASS_ERROR);
        }
        return
$class->newInstance();
    }
}
?>

By passing one of CMDMAN_ILLEGALCLASS_ERROR or CMDMAN_GENERAL_ERROR to any Exception object that we throw, we make it possible for client code to recognize different error categories, and define distinct strategies for handling the problem.

<?php
// PHP 5
try {
    
$mgr = new CommandManager();
    
$cmd = $mgr->getCommandObject('realcommand');
    
$cmd->execute();
} catch (
Exception $e) {
    if (
$e->getCode() == CommandManager::CMDMAN_GENERAL_ERROR) {
        
// no way of recovering
        
die($e->getMessage());
    } else if (
$e->getCode() == CommandManager::CMDMAN_ILLEGALCLASS_ERROR) {
        
error_log($e->getMessage());
        print
"attempting recovery/n";
        
// perhaps attempt to invoke a default command?
    
}
}
?>

We can achieve a similar effect by throwing and catching distinct Exception subclasses.

Subclassing Exception

There are two clear reasons why you might want to subclass Exception. These are:

  • To provide specialized functionality within your subclass
  • To distinguish one error type from another as a service to client code.

Let's look at the second instance. We might work with two kinds of error in the CommandManager class: a general error category (covering a missing command directory, for example), and a set of errors associated with the failure to locate or generate Command objects.

We might define two Exception subtypes for these cases:

<?php
// PHP 5
require_once('command/Command.php');
class CommandManagerException extends Exception{}
class
IllegalCommandException extends Exception{}

class
CommandManager {
    private
$cmdDir = "command";

    function
__construct() {
        if (!
is_dir($this->cmdDir)) {
            throw new
CommandManagerException("directory error: $this->cmdDir");
        }
    }

    function
getCommandObject($cmd) {
        
$path = "{$this->cmdDir}/{$cmd}.php";
        if (!
file_exists($path)) {
            throw new
IllegalCommandException("Cannot find $path");
        }
        require_once
$path;
        if (!
class_exists($cmd)) {
            throw new
IllegalCommandException("class $cmd does not exist");
        }

        
$class = new ReflectionClass($cmd);
        if (!
$class->isSubclassOf(new ReflectionClass('Command'))) {
            throw new
IllegalCommandException("$cmd is not a Command");
        }
        return
$class->newInstance();
    }
}
?>

When our class fails to find a command directory it throws a CommandManagerException. When it encounters difficulties in generating a Command object, the getCommandObject() method throws an IllegalCommandException. Note that there are a number of different reasons why an IllegalCommandException might be thrown. We could combine the two previous examples here and provide an error code constant in the IllegalCommandException for each potential cause.

Now that the CommandManager class can fail in these novel ways, we can add new catch clauses to match the different error types:

<?php
// PHP 5
try {
    
$mgr = new CommandManager();
    
$cmd = $mgr->getCommandObject('realcommand');
    
$cmd->execute();
} catch (
CommandManagerException $e) {
    die(
$e->getMessage());
} catch (
IllegalCommandException $e) {
    
error_log($e->getMessage());
    print
"attempting recovery/n";
    
// perhaps attempt to invoke a default command?
} catch (Exception $e) {
    print
"Unexpected exception/n";
    die(
$e->getMessage());
}
?>

If the CommandManager object above throws a CommandManagerException, then the corresponding catch is executed. This is not a given, however. The argument portion of each catch clause acts like a test. The first match is the one executed. For this reason, you should always organize your catch clauses from the most specific to the most general. If we were to reorganize our catch clauses like this:

<?php
// PHP 5
try {
    
$mgr = new CommandManager();
    
$cmd = $mgr->getCommandObject('realcommand');
    
$cmd->execute();
} catch (
Exception $e) {
    print
"Unexpected exception/n";
    die(
$e->getMessage());
} catch (
CommandManagerException $e) {
    die(
$e->getMessage());
} catch (
IllegalCommandException $e) {
    
error_log($e->getMessage());
    print
"attempting recovery/n";
    
// perhaps attempt to invoke a default command?
}
?>

the first clause would always be executed when an exception was thrown. This is because every exception belongs to the Exception type and the first clause will therefore always make a match.

If you are catching specific Exception subtypes in your catch clauses, it is a good idea to implement a final clause that catches the Exception type. This then acts as a catch-all. Of course, you may wish to pass the buck along to the method that called the client code. This is another feature of PHP's exception functionality that should be discussed.

Passing the Buck

We have already established that some errors cannot be handled at the point at which they occur. A good solution to this can't-see-the-wood-for-the-trees scenario is to pass responsibility back up to the code that called the current method. What happens, though, if the calling code does not itself have the perspective to deal with the problem? Well, we can always re-throw the error. Let's expand the client code we have been working with so that it forms part of a simple class:

<?php
// PHP 5
class RequestHelper {
    private
$request = array();
    private
$defaultcmd = 'defaultcmd';
    private
$cmdstr;

    function
__construct($request_array=null) {
        if (!
is_array($this->request = $request_array)) {
            
$this->request=$_REQUEST;
        }
    }

    function
getCommandString() {
        return (
$this->cmdstr ? $this->cmdstr : ($this->cmdstr=$this->request['cmd']));
    }

    function
runCommand() {
        
$cmdstr = $this->getCommandString();
        try {
            
$mgr = new CommandManager();
            
$cmd = $mgr->getCommandObject($cmdstr);
            
$cmd->execute();
        } catch (
IllegalCommandException $e) {
            
error_log($e->getMessage());
            if (
$cmdstr != $this->defaultcmd) {
                
$this->cmdstr = $this->defaultcmd;
                
$this->runCommand();
            } else {
                throw
$e;
            }
        } catch (
Exception $e) {
            throw
$e;
        }
    }
}

$helper = new RequestHelper(array(cmd=>'realcommand'));
$helper->runCommand();
?>

We have wrapped our client code in a class called RequestHelper. The RequestHelper class is responsible for managing user-provided data. In the constructor we optionally accept a debug array. If no such array is forthcoming, the class uses the superglobal $_REQUEST array. Whichever array is used, it is assigned to a property called $request. Client code signals the command it wishes to execute by providing a 'cmd' element in the request array. The getCommandString() method tests a property called $cmdstr. If it is empty (which it is, to start with) then the method assigns the contents of the $request property's 'cmd' element to $cmdstr, returning the result. If it is not empty, the method simply returns the $cmdstr property. Through this mechanism, the command string can be overridden within the RequestHelper class.

RequestHelper has a relatively narrow remit, so for all exceptions but IllegalCommandException objects we defer any error handling to a higher level class. We do this in the final catch clause by manually throwing the caught Exception class:

} catch (Exception $e) {
    throw
$e;
}

If we catch an IllegalCommandException, however, we first attempt to invoke a default command. We do this by setting the $cmdstr property to the same value as $defaultcmd and then invoking the runCommand method recursively. If the $cmdstr and $defaultcmd strings are already equivalent, there is no further action we can take, and we re-throw the exception.

In fact Zend Engine 2 will automatically re-throw any exceptions that you don't catch yourself, so we could omit the final catch clause altogether with no change in script functionality. (We have already taken advantage of this feature in our example.) Here is the final line of the CommandManager::getCommandObject() method:

return $class->newInstance();

We beg a couple of questions here.

Firstly, we have assumed that the constructor requires no arguments. In this article we will not deal with cases where this is not so.

Secondly, we have assumed that the command can be instantiated. If the constructor was declared private, for example, this statement would throw a ReflectionException object. If we don't handle this in RequestHelper, then the exception will be passed up to the code that invoked RequestHelper, and so on. Where an exception may be thrown implicitly it is a good idea to signal as much in your documentation, or even to manually throw the exception so that other programmers are forewarned to handle this eventuality.

More Information about an Exception

Here is some code that formats Exception information.

<?php
// PHP 5
class Front {
    static function
main() {
        try {
            
$helper = new RequestHelper(array(cmd=>'realcommand'));
            
$helper->runCommand();
        } catch (
Exception $e) {
            print
"<h1>".get_class($e)."</h1>/n";
            print
"<h2>{$e->getMessage()}
                ({$e->getCode()})</h2>/n/n"
;
            print
"file: {$e->getFile()}<br />/n";
            print
"line: {$e->getLine()}<br />/n";
            print
$e->getTraceAsString();
            die;
        }
    }
}
Front::main();
?>

If you make the realcommand class impossible to instantiate (by declaring its constructor private) and run this code, you will see this output:

<h1>ReflectionException</h1>
<h2>Access to non-public constructor of class realcommand (0)</h2>

file: /home/xyz/TraceStackException.php<br />
line: 30<br />
#0 /home/xyz/TraceStackException.php(53): CommandManager->getCommandObject()
#1 /home/xyz/TraceStackException.php(73): RequestHelper->runCommand('realcommand')
#2 /home/xyz/TraceStackException.php(85): Front::main()
#3 {main}

As you can see, getFile() and getLine() do just what you would expect: they return the file name and line number for the exception. The getStackAsString() method returns details about each layer of invocation that led to the exception's generation.

You can also get at this information in array format using the getTrace() method. getTrace() returns a multi-dimensional array. As you'd expect, the first element contains information about the location at which the exception is generated, the next element details the outer method call, and so on until the topmost level is reached. Each element in this array is itself an array with the following fields:

fileThe file in which the method invocation was made
lineThe line number of the offending method call
functionThe name of the offending method
classThe class on which the invocation was made
typeThe kind of call, '::' for static, or '->' for instance invocations
args The arguments for the method call

Summary

Exceptions provide some key benefits.

By grouping error handling code in catch statements you can separate your error handling from your application flow. This can make your code easier to read, and your coding practice more pleasant. I often begin with a strict policy of catching all exceptions in a method and killing script execution. I then introduce additional flexibility as needed. This draconian policy gets me up and running with safe and easy exception management straight away.

Exceptions flow from the low level to the high. That is, exceptions are passed back to the code that is best placed to decide how to handle an error. At first it may seem to defy common sense, but it is usually the case that the point at which an error occurs is the worst place to decide what to do about the error.

The throw/catch mechanism provided by exceptions sidesteps the problems associated with returning error values from methods. The return value of a method can be determined entirely by the logic of your class. Client code can expect a particular return type, without the wearisome task of continual testing.

About the Author

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.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值