Introduction to using PEAR_ErrorStack for advanced error handling

Introduction to using PEAR_ErrorStack for advanced error handling -- Using PEAR_ErrorStack to do both simple and advanced error handling

Synopsis

Introduction to the usage of PEAR_ErrorStack

Introduction

This class is available as part of the PEAR package. Features include:

PEAR_ErrorStack implements error raising and handling using a stack pattern. This has tremendous advantages over the PEAR_Error Implementation. PEAR_Error centralizes all error creation and handling in the constructor of the PEAR_Error object. Once an object has been created, all handling must have been completed, either through checking the return value of a method, or through a single global callback. In addition, it is nearly impossible to determine the source of an error, and the baggage of all of the PEAR base class's bulky, slow methods accompanies every error creation.

<?php
// traditional PEAR_Error usage
require_once 'PEAR.php';
class myobj
{
    // there is no way to know where $err comes from
    function errorCallback($err)
    {
        $this->display($err->getMessage());
        $this->log($err->getMessage());
    }
    
    function log($msg)
    {
        error_log($msg, 3, 'somefile.log')
    }

    function display($msg)
    {
        echo $msg . '<br />';
    }
}

$myobj = new myobj;

// using a callback
PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, array(&$myobj, 'errorCallback'));

$ret = SomePackage::doSomething();
if (PEAR::isError($ret)) {
    // do some handling - this error is also displayed and logged
}
PEAR::pushErrorHandling(PEAR_ERROR_RETURN);

$ret = SomePackage::doSomething();
if (PEAR::isError($ret)) {
    // do some handling - this error is not displayed or logged
}
PEAR::popErrorHandling();
?>

The PEAR_ErrorStack class has built in knowledge of the Log package, can easily differentiate and even automatically re-package errors with little to no difficulty.

<?php
// PEAR_ErrorStack error handling
require_once 'PEAR/ErrorStack.php';
require_once 'Log.php';
define('MYPACKAGE_ERROR_DBERROR', 1);
class myobj
{
    var $_stack;
    function myobj()
    {
        $this->_stack = &PEAR_ErrorStack::singleton('MyPackage');
    }

    function errorCallback($err)
    {
        switch($err['package']){
            case 'MyPackage': 
                // tell the error stack to log the error only
                // it will not be pushed onto the stack
                return PEAR_ERRORSTACK_LOG;
                break;
            case 'InternalDbPackage': 
                // re-package these errors as a mypackage error fit
                // for enduser consumption
                $this->_stack->push(MYPACKAGE_ERROR_DBERROR, 'error',
                    array('dbmessage' => $err['message'],
                          'dbcode' => $err['code'],
                          'We are having Connection problems, please' .
                          'try again in a few moments'),
                    '', $err); // include the error as re-packaged
                // tell the internal DB error stack to ignore this error,
                // as if it never happened
                return PEAR_ERRORSTACK_IGNORE;
                break;
        } // switch
    }
}

$myobj = &new myobj;
// separate error stacks for my package, and the internal DB package
$dbstack = &PEAR_ErrorStack::singleton('InternalDbPackage');
$mystack = &PEAR_ErrorStack::singleton('MyPackage');
// set up a file log using PEAR::Log
$log = &Log::Factory('file', 'somefile.log', 'MyPackage error log');
$mystack->setLogger($log);
// set up a default log to use for all error stacks
PEAR_ErrorStack::setDefaultLogger($log);

// any errors returned by MyPackage are logged
$ret = SomePackage::doSomething();

// Note that $ret need not be checked for any error condition - errors are
// totally separate from code
if ($dbstack->hasErrors()) {
    var_dump($dbstack->getErrors();
}

// use the callback
$dbstack->setCallback(array(&$myobj, 'errorCallback'));

// any db errors are transparently repackaged as
// user-friendly MyPackage errors now
$ret = SomePackage::doSomething();

?>

Why write a new error-handling routine when PEAR_Error already exists? There are several problems with PEAR_Error. Although an error message is present in an error class, processing this error message automatically is excessively difficult for computers. In addition, the error message cannot easily be translated once it has been placed into the PEAR_Error. There is also no standard facility for storing error-related data in the error class. On top of error message-related issues, there is no way to automatically determine which package a PEAR_Error object comes from, or the severity of an error. Fatal errors look exactly the same as non-fatal errors.

The largest flaw with PEAR_Error object is the single-error type design. Every PEAR_Error object is just a PEAR_Error object. There is no differentiating between the severity of an error, or its origin. The only way to determine the severity is to use PEAR_ERROR_TRIGGER and E_USER_NOTICE/E_USER_WARNING/E_USER_ERROR constants from php's http://www.php.net/trigger_error. But using this functionality does not justify 900 lines of code, simply because trigger_error() is built into PHP itself!

Now, to start using your newly created error objects, change all of your PEAR::raiseError() or PEAR::throwError() calls from this...

<?php
require_once 'PEAR.php';
// old way:
$error_specific_info = 'bad';
$e = PEAR::raiseError("error message - very " . $error_specific_info .
    " way to do things", MYPACKAGE_ERROR_FOO);
// another old way:
$e = PEAR::throwError("error message - very " . $error_specific_info .
    " way to do things", MYPACKAGE_ERROR_FOO);
?>

...to something like this:

<?php
require_once 'PEAR/ErrorStack.php';
// new way
// version 1: stack instance access
$stack = &PEAR_ErrorStack::singleton('MyPackage');
$stack->push(MYPACKAGE_ERROR_DBERROR, 'error',
    array('query' => $query, 'dsn' => $dsn),
    'Critical Database Error: Contact Administrator immediately');
// version 2: static singleton access (slightly slower)
PEAR_ErrorStack::staticPush('MyPackage', MYPACKAGE_ERROR_DBERROR, 'error',
    array('query' => $query, 'dsn' => $dsn),
    'Critical Database Error: Contact Administrator immediately');
?>

For basic use, this is all that is needed to use the PEAR_ErrorStack package in place of PEAR_Error.

Advanced Features

Error Context Display

In some cases, you may want to customize error generation. For instance, for many exceptions, it is useful to include file, line number, and class/function context information in order to trace an error. A default option is available which will be sufficient for most cases, and that is PEAR_ErrorStack::getFileLine().

Not all package errors occur in the PHP source file. For instance, compiling template engines errors can occur in the template source files. Database errors can occur in the text of a query, or internal to the database server. Internet package errors can occur on another server. All of this information can be included in an error message using a context grabbing callback.

<?php
require_once 'PEAR/ErrorStack.php';
class DatabaseClass
{
    var $_dbError;
    var $_dbErrorMsg;
    var $_dbQuery;
    var $_dbPos;
    /**
     * Context grabber for the Database package
     * @param integer Error Code
     * @param array   Error parameters passed into {@link PEAR_ErrorStack::push()}
     * @param array   Output of debug_backtrace() (not used in this callback)
     */
    function getErrorContext($code, $params, $backtrace)
    {
        $context = array(
            'errorcode' => $this->_dbError,
            'errormsg' => $this->_dbErrorMsg,
            'query' => $this->_dbQuery,
            'pos' => $this->_dbPos,
        );
        return $context;
    }
}
$db = new DatabaseClass;
PEAR_ErrorStack::staticSetContextCallback('Database', array(&$db, 'getErrorContext'));
?>

The context information is formatted to be easily processed by an external application. If you wish context information to be in the error message, the error message callback should be used to add the information in a human-readable format to the error message, as described in the next section.

Custom Error Message Generation

There are three methods of PEAR_ErrorStack designed for use with generating error messages efficiently. To use them, you must do one of two things:

Controlling error generation

There are many scenarios in which fine-grained control over error raising is absolutely necessary. A generic error handling callback means that every single error raised will be handled in the same error callback. Although PEAR_ErrorStack is designed to operate with independent callbacks for each package, generic error handling is possible through the PEAR_ErrorStack::staticPushCallback() method. This is no different from PEAR_Error's PEAR_ERROR_CALLBACK error handling mode.

PEAR_ErrorStack's real strength comes from the callback itself. PEAR_Error's callback has no actual effect on the error message - all error handling must happen in the callback method or function itself. PEAR_ErrorStack's callback can influence the error through the use of three constants:

PEAR_ERRORSTACK_IGNORE informs the stack to ignore the error, as if it never occurred. The error will be neither logged, nor pushed on the stack. It will, however, be returned from PEAR_ErrorStack::push()

PEAR_ERRORSTACK_PUSH informs the stack to push the error onto the error stack, but not to log the error.

PEAR_ERRORSTACK_LOG informs the stack not to push the error onto the error stack, but only to log the error.

<?php
define('ERROR_CODE_ONE',1);
define('ERROR_CODE_TWO',2);
define('ERROR_CODE_THREE',3);
require_once 'PEAR/ErrorStack.php';
require_once 'Log.php';
function somecallback($err)
{
    switch($err['code']){
        case ERROR_CODE_ONE: 
                return PEAR_ERRORSTACK_IGNORE;
                break;
        case ERROR_CODE_TWO: 
                return PEAR_ERRORSTACK_PUSH;
                break;
        case ERROR_CODE_THREE: 
                return PEAR_ERRORSTACK_LOG;
                break;
    } // switch
}
$log = &Log::factory('display');
$stack = &PEAR_ErrorStack::singleton('mypackage');
$stack->setLogger($log);
$stack->pushCallback('somecallback');
$stack->push(ERROR_CODE_ONE);
$stack->push(ERROR_CODE_TWO);
$stack->push(ERROR_CODE_THREE);
var_dump(PEAR_ErrorStack::staticGetErrors());

// simulate PEAR_ERROR_CALLBACK, with specific callback for mypackage
// every other package will only log errors, only mypackage's errors
// are pushed on the stack, conditionally
class myclass {
    function acallback($err)
    {
        return PEAR_ERRORSTACK_LOG;
    }
}
$stack2 = PEAR_ErrorStack::singleton('anotherpackage');
$stack3 = &PEAR_ErrorStack::singleton('thirdpackage');
PEAR_ErrorStack::setDefaultCallback(array('myclass', 'acallback'));
?>

Repackaging errors from one package to another

The most obvious usage for an error callback involves a common scenario in many user-level applications that use system-level packages. If you write a Content Management System (CMS) with the PEAR DB package, it is usually a bad idea to display database-level errors when a user clicks on a link to add a message to a forum. PEAR_ErrorStack can be used to repackage this error as a MyPackage error.

<?php
define('MYPACKAGE_ERROR_DBDOWN',1);
require_once 'PEAR/ErrorStack.php';
function repackage($err)
{
    if ($err['package'] == 'DB') {
        $mystack = &PEAR_ErrorStack::singleton('mypackage');
        $mystack->push(MYPACKAGE_ERROR_DBDOWN, 'error', array('olderror' => $err));
        // ignore the DB error, but save it in the mypackage error, for logging
        return PEAR_ERRORSTACK_IGNORE;
    }
}
?>

Emulating the @ operator

One of the difficult-to-use strengths of PEAR_Error involves the PEAR::expectError() method. With regular PHP errors, it is possible to silence them using the @ operator like so:

<?php
@file_get_contents();
?>

Emulating this behavior with PEAR_ErrorStack is simple.

<?php
define('ERROR_CODE_SOMETHING', 1);
require_once 'PEAR/ErrorStack.php';
function silence($err)
{
    // ignore all errors
    return PEAR_ERRORSTACK_IGNORE;
}
$stack = &PEAR_ErrorStack::singleton('mypackage');
$stack->pushCallback('silence');
$stack->push(ERROR_CODE_SOMETHING);
?>

PEAR_ErrorStack can take this one step further, and only log errors or only put errors on the error stack, using the other two constants. Finally, particular errors can be singled out, and all others ignored.

<?php
define('SOMEPACKAGE_ERROR_THING', 1);
require_once 'PEAR/ErrorStack.php';
function silenceSome($err)
{
    if ($err['package'] != 'somepackage') {
        // ignore all errors from other packages
        return PEAR_ERROR_IGNORE;
    }
    if ($err['code'] != SOMEPACKAGE_ERROR_THING) {
        // ignore all other error codes
        return PEAR_ERRORSTACK_IGNORE;
    }
}
$stack = &PEAR_ErrorStack::singleton('mypackage');
$stack->pushCallback('silenceSome');
$stack->push(ERROR_CODE_SOMETHING);
?>

Backwards compatibility with PEAR_Error, Forward compatibility with PHP 5 Exception

PEAR_ErrorStack can also be programmed to automatically raise a PEAR_Error using PEAR::raiseError(), simply pass in true to the PEAR_Error compatibility like so:

<?php
require_once 'PEAR/ErrorStack.php';
$stack = &PEAR_ErrorStack::singleton('mypackage', false, null, 'PEAR_ErrorStack', true);
?>

PEAR_ErrorStack will return an exception automatically in PHP 5. You can set the exception class name that will be returned using the following code:

<?php
require_once 'PEAR/ErrorStack.php';
$stack = &PEAR_ErrorStack::singleton('mypackage', false, null, 'PEAR_ErrorStack', false, 'MyException');
?>