GZICP.com   
 
    返回首页
    联系我们
 
 
     

Exceptional Code

www.gzicp.com   2004年8月22日 01:24:50
 

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.

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
  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

 Matt Zandstra

相关文章
·PHP SOAP Extension(2004年08月22日)
·SQLite Introduction(2004年08月22日)
·XML in PHP 5 - What's New(2004年08月22日)
·Zend Engine II - PHP's OO Evolution(2004年08月22日)
·What's New in PHP 5(2004年08月22日)
最新文章
·无法加载gd动态链接库的解决办法  (2005年04月29日)
·PHP:风雨欲来 路在何方?  (2005年04月13日)
·用PHP实现验证码功能  (2005年03月19日)
·利用gettext来实现PHP 的国际化编程  (2005年03月14日)
·PHP的编译选项说明  (2005年03月13日)
·通过对php服务器端的配置加强安全  (2005年03月13日)





 
 
Copyright © 1999-2008 GZICP.com All Rights Reserved