Error Handling with fatals in PHP

At some point I grew frustrated with PHP not being able to catch fatal errors, so I set out to see if it was possible with the current PHP feature set. The problem is that if an error is considered fatal, php bails and leaves you with no page or a partial page. Now we aren’t going to be able to use the traditional error handler interface, but what we will do is use it to catch any errors we can, also we need to mark the output buffer, so we can empty it later. This will allow us to register a shutdown function that will look into the output buffer and see if we have an error output (we will leave php outputting errors to ensure this). If so, we’ll eat the content of the output buffer and dump our error page.

First we’ll need to set up some global variables to handle general settings

class ErrorHandler{

    public static $filePath = './Errors/';
    public static $lineEnding = "<br/>\n";
    public static $debug = false;
    public static $enableFatalCatch = true;

This function is your call hook which should probably be called right around where you setup your autoloader, and before connecting to your datasource. One call to ErrorHandler::startup(); and all your hooks are in place.

    public static function startup(){
        ob_start();
        set_error_handler(array("ErrorHandler", "error"));
        register_shutdown_function(array("ErrorHandler", "shutdown"));
    }

The shutdown function is your fatal detector, we look at the buffer and the error type and if we see fatal in both, we trigger an error and consume the page.

    public static function shutdown(){
        $error = error_get_last();
        $page = ob_get_contents();
        if(ErrorHandler::$enableFatalCatch && $error['type'] == 1
                && preg_match('~Fatal error:~', $page)){
            ErrorHandler::error(
                $error['type'],
                $error['message'],
                $error['file'],
                $error['line'],
                $errcontext, false
            );
        }else{
            ob_end_flush();
        }
    }

Here I’m just Formatting the structure of the stack trace.

    public static function buildStackTrace($indent =''){
        $backtrace = debug_backtrace();
        $result='';
        for($lcv=0; $lcv<count($backtrace)+1; $lcv++){
            $trace = $backtrace[$lcv];
            $lastTrace = $backtrace[$lcv-1];
            if(isset($lastTrace)){
                if($lastTrace['class']) $location = $lastTrace['class'].'.
                    '.$lastTrace['function'];
                else $location = $lastTrace['function'];
            }else{
                $location = 'main script block';
            }

            if(isset($trace)){
                $fileParts = explode('/', $trace['file']);
                $file = $fileParts[count($fileParts)-1].':'.$trace['line'];
            }else{
                $fileParts = explode('/', $_SERVER['SCRIPT_FILENAME']);
                $file = $fileParts[count($fileParts)-1];
            }

            if($trace['class'] != 'ErrorHandler')
                $result .= $indent.$location.'('.$file.')'.
                    ErrorHandler::$lineEnding;
        }
        return $result;
    }

This is where we actually handle the error, either passed back through the error handler, or from the shutdown function. I gather all the information together and dump it if we’re in debug mode or just simply output an error message. This would also be an ideal place to log errors, though you should probably do a 2 stage error handler in that case (which I’ll cover in a later post).

    public static function error($errno, $errstr, $errfile,
            $errline, $errcontext, $exitOnFinish=true){
        switch ($errno){
            case E_USER_WARNING:
            case E_USER_NOTICE:
            case E_WARNING:
            case E_NOTICE:
            case E_CORE_WARNING:
            case E_COMPILE_WARNING:
             break;
            case E_USER_ERROR:
            case E_ERROR:
            case E_PARSE:
            case E_CORE_ERROR:
            case E_COMPILE_ERROR:
            case E_RECOVERABLE_ERROR:

                if (eregi('^(sql)$', $errstr)) {
                    $MYSQL_ERRNO = mysql_errno();
                    $MYSQL_ERROR = mysql_error();
                    $mysqlerror = "Additionally MySQL reported error#".
                        " $MYSQL_ERRNO : $MYSQL_ERROR";
                }

                $fileParts = explode('/', $errfile);
                $file = $fileParts[count($fileParts)-1].':'.$trace['line'];

                $message = "Error #".$errno.": '".$errstr."' in "
                    .$file." on line ".$errline.ErrorHandler::$lineEnding;
                if($mysqlerror)$message .= $mysqlerror.ErrorHandler::$lineEnding;

                $message .= 'Trace:<ul>'.ErrorHandler::buildStackTrace('<li>').
                    '</ul>'.ErrorHandler::$lineEnding;
                $hash = md5($message);

                $log = ErrorHandler::getErrorLogText();
                dBug::$bufferOutput = true;
                //this is a 3rd party data dump
                $debugger = new dBug($errcontext, '', true);
                $backtrace = debug_backtrace();
                unset($backtrace[0]);
                $trace_debugger = new dBug($backtrace, '', true);
                $scope = 'Context:'.$debugger->buffer."<br/>\n".
                     'Backtrace:'.$trace_debugger->buffer;


                $bufferContent = ob_get_contents();
                ob_clean();
                if(!ErrorHandler::$debug){
                    echo "<html>
<head>
<title>An Error Has Occurred</title>
</head>
<body>
Error!
<br /><br />
Error Code: ".$hashText."
".$additionalError."</body></html>";
                }else{
                    echo($message);
                    echo("[Error Log]*****<br/>\n");
                    echo($log);
                    echo("[Error Scope]*****<br/>\n");
                    echo($scope);
                    echo("[Page Content]*****<br/>\n");
                    echo($bufferContent);
                }
                if($exitOnFinish) exit();
            default:
             break;
        }
    }
}

That’s it! Now we can catch any error PHP throws at us.

Leave a Reply