Capture Fatal PHP Errors for Logging

When code runs without a user interface, for example a script invoked by cron, there often is not an easy way to see what errors are occuring. Another example would be php invoking another php script using exec(), for example. Here are some common fatal errors: unknown class referenced, unknown method called and parse error. Forgot a semicolon? Thats a parse error.

This post combines ideas others have used and gives an example of how to capture and log fatal php errors.

The php function set_error_handler() allows you to create a callback for handling errors. However, you cannot catch fatal errors using a callback registered with set_error_handler().

You can register a callback for php to invoke at the very end of execution via register_shutdown_function(). There is a way to see if there were any errors during script execution by calling error_get_last().

The problem is, how do we call register_shutdown_function()? If the script you want to monitor for errors has en error, like a parse error, and you put the register_shutdown_function() call in that script it will not work, because of the parse error. And using “include” or “require” will not work either because that essentially is still the same script, as far as php is concerned.

You can however use an INI file to tell php to automatically prepend a php file before invoking the script you want to monitor. By combining this with the log4php as the logging library you can create a very powerful error handler. You can read my log4php how-to if you are not familiar with the log4php library.

Imagine this is the script to be called by cron, it has a parse error. The code is saved in a file called mycron.php.

<?php
$foo = ;
?>

I put mycron.php in its own directory: /home/tom/bin.

In that same directory create php.ini telling php what files to automatically prepand and append:

; php.ini contents:
auto_prepend_file = /home/tom/bin/runtime_start.php

The contents of runtime_start.php is:

<?php

require_once 'log4php/Logger.php';

Logger::configure('logconfig.xml');

class Runtime
{
    function Runtime()
    {
        register_shutdown_function(array($this, 'shutdown'));
    }

    function shutdown()
    {
        $logger = Logger::getLogger('root');

        $e = error_get_last();

        if(is_null($e)) {
	    $logger->debug('Script ended normally');
	} else {
            $logger->error($e['message']);
        }
    }

    function finish()
    {
        $logger = Logger::getLogger('root');
        $logger->debug('Script ended normally');
    }
}

$runtime = new Runtime();
?>

One of the nice things about this approach is that the script I’m executing does not have to be altered in any way to add the error capturing and logging. You do however need to invoke it in a particular way. You have to tell php where to find the INI file:

php -c /home/tom/bin -f /home/tom/bin/mycron.php

You can get the full list of php command line invocation options using “php –help”.

When run the log file will contain a message like the following:

2011-09-15 11:08:01 PDT [ERROR] root: syntax error, unexpected ‘:’