If you get errors on your site, if you can help it you don't want to have to be reviewing log files or waiting for users to tell you. You want to catch these errors and be notified. You can try set_error_handler('our_error_handler'); but that will only be called for runtime errors such as E_ERROR. The other serious errors E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR occur in the target script before execution and are serious enough to prevent the script even entering runtime.

So you can see a lot of code try to use set_error_handler() or register_shutdown_function() (which we will actually use for our solution) to catch all the errors but they often place the assignments themselves in runtime code so critically they won't work.

But the assignment can be made before runtime in a file set as the auto_prepend_file PHP setting. You can set the "auto_prepend_file" in a few ways:
  • Either in the main php.ini with auto_prepend_file = our_error_.php
  • Or in php.ini files in each and every dir you want it to apply to because the setting is PHP_INI_PERDIR 'changeable' (which means that the setting does not cascade down the subdirectories, you need to set in each one)
  • Or in an .htaccess file with php_value auto_prepend_file our_error_.php, where it does recurse so put in the document root if you want it to apply to all pages (but you need AllowOverride Options to be set in Apache's httpd.conf (or equivalent) so the setting is allowed).
Note that "The file is included as if it was called with the require() function, so include_path is used."

In this auto_prepend_file you can place the assignment for the register_shutdown_function() which will then be called every time a page ends, either normally, or due to *any* error. register_shutdown_function('our_error_finder');

Remember that this will execute before PHP enters runtime so even if there is a critical parse error of your page where normally nothing at all happens, this will still have executed and you will find yourself in our_error_finder() where we can ask PHP if there was an error and respond.

Inside the error handler you don't know what the state is so you can't rely on anything having been loaded or anything. Which means that you should only use what you have in the prepend file. This is good anyway, the more code you try to use the more chance your handler has of failing, then you're really stuffed. This includes broken references to images in any output you show to the user. (Note that in some installations the cwd changes during shutdown, for example to "/". So including a relative file will probably fail, so use absolute paths).

So, along with a few extras:
  • To discover if the error happened before runtime so a full HTML page can be printed
  • An attempt at email
  • Writes some context of the error to the log file
The following is code for the file to prepend:
// our_error_.php
function our_error_finder () {
if (null !== $e = error_get_last())
return our_error_response($e);
return null;
responds to an error.
if this happened before runtime this shows a full page (in runtime there may have been page content already).
$err is an array with keys type, message, file, line.
function our_error_response ($err) {
//if the error is not handled here, return false so that if this fn is called as a part of an error handler it will indicate the default handler should continue.
if (!in_array($err['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR)))
return false;
$email_addrs = bl_error_email_addrs();
$log_fp = fopen(ini_get("error_log"), "a");
$message = "server-name [".$_SERVER['SERVER_NAME']."] when [".gmdate('Y-m-d H:i:s')."] ip-guess [".$_SERVER['REMOTE_ADDR']."] script-name [".$_SERVER['SCRIPT_NAME']."] query-string [".$_SERVER['QUERY_STRING']."] info [".json_encode($err)."]";
//by default php does not have stack_trace for fatal errors. the xdebug extension will provide a stack trace.
//but also provide the context to the log file
$wrote_log_bytes = 0;
if ($log_fp)
$wrote_log_bytes = fwrite($log_fp, "ERROR HANDLER context: ".$message.". will ".($email_addrs ? "send email to [".implode(", ", $email_addrs)."]" : "not send email").".\n");
if (in_array($err['type'], array(E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR)))
if ($_SERVER["SCRIPT_FILENAME"] == $err["file"])
$startup_error_in_main_page = true;
$emailsuccessp = null;
if ($email_addrs) {
$subject = 'WEBSITE ERROR for '.$_SERVER['SERVER_NAME'];
$body = $wrote_log_bytes ?
"the error should have been written to the log file (FYI: at ".gmdate('Y-m-d H:i:s')." ".$err["message"]." in ".basename($err["file"])."(".$err["line"]."))" :
$headers = 'Date:'.date('r')."\n".'From:the-system <no-reply@localhost.com>'."\n";
for ($i = 0, $ii = count($email_addrs); $i < $ii; $i++) {
if ($email_addrs[$i]) {
//try all emails, but keep record if any failed.
if (mail($email_addrs[$i], $subject, $body, $headers)) {
if ($emailsuccessp==null)
$emailsuccessp = true;
} else {
$emailsuccessp = false;
if ($log_fp) {
if ($email_addrs && !$emailsuccessp)
fwrite($log_fp, "error email(s) not sent :(\n");
if ($startup_error_in_main_page)
print '<html><head><title>Bad Bug</title></head><body>';
print '<div>Ouch!<br /><br />There was a rather significant error on the site, could be that nothing at all was done.<br /><br />';
if ($email_addrs) {
if ($emailsuccessp)
print 'I sent an email to the site administrators who should address the issue soon.<br /><br />You can of course chase them.';
print ' You can contact an administrator at <a href="mailto:'.$email_addrs[0].'">'.$email_addrs[0].'</a>.';
} else {
print 'I was not asked to contact anyone directly, but if you want to notify the administrator please tell them the time of the error: <code style="color:#B20;">'.date('Y-m-d H:i:s')."</code><br />";
print '<br /><br />KTHXBYE</div>';
if ($startup_error_in_main_page)
print '</body></html>';
return null;

You can download our_error_.php here.

I'll soon post a few more lines where you can try to recover from some simple function errors