Michael Caplan is a web programmer, web and print designer, and in earlier days a non-profit consultant. Michael specializes in Internet solutions, and he is currently working with Labnet Dental Lab Systems, a division of Henry Schein Inc., heading up their web development initiatives. Michael has posted 1 posts at DZone. View Full User Profile

Mayday, Mayday, Mayday - PHP Going Down

08.25.2008
| 8011 views |
  • submit to reddit
PHP provides a variety of tools for handling errors and exceptions, in particular extensible handlers for managing errors and exceptions as they occur. However, there is a range of fatal errors that PHP does not directly provide tools for you to handle them. In the best of scenarios, these fatal errors will result in partially outputted pages, or the “white screen of death.”

These errors include:

E_ERROR
Fatal run-time errors. These indicate errors that can not be recovered from, such as a memory allocation problem. Execution of the script is halted
E_PARSE
Compile-time parse errors. Parse errors should only be generated by the parser
E_CORE_ERROR
Fatal errors that occur during PHP’s initial startup. This is like an E_ERROR, except it is generated by the core of PHP
E_COMPILE_ERROR
Fatal compile-time errors. This is like an E_ERROR, except it is generated by the Zend Scripting Engine.

By leveraging a combination of shutdown function, error_get_last, and output buffering it is possible to put a structure in place that will allow you to trap fatal errors. The theory of operation is quite simple:

  1. Register a shutdown function.
  2. When the shutdown function is called, check to see if the script finished executing as expected by interrogating error_get_last for any fatal errors.
  3. Handle the fatal error as you see fit.

Let’s look at some code:

<?php

ob_start();

$res = register_shutdown_function('shutdown');

function shutdown()
{
if ($error = error_get_last()) {
if (isset($error['type']) && ($error['type'] == E_ERROR || $error['type'] == E_PARSE || $error['type'] == E_COMPILE_ERROR)) {
ob_end_clean();

if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}

echo '<h1>Bad Stuff Happend</h1>';
echo '<p>But that is okay</p>';
echo '<code>' . print_r($error, true) . '</code>';
}
}
}

ob_end_flush();
?>

The overall page generated is wrapped in an output buffer before being sent to the client. The use of output buffering here allows us to get rid of the partially generated content when the fatal error occurs and replace it with a custom error message. For example:

<?php

ob_start();

$res = register_shutdown_function('shutdown');

function shutdown()
{
if ($error = error_get_last()) {
if (isset($error['type']) && ($error['type'] == E_ERROR || $error['type'] == E_PARSE || $error['type'] == E_COMPILE_ERROR)) {
ob_end_clean();

if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}

echo '<h1>Bad Stuff Happend</h1>';
echo '<p>But that is okay</p>';
echo '<code>' . print_r($error, true) . '</code>';
}
}
}

?>

<h1>My Bad Day</h1>

<?php require 'FileNotFound.php'; ?>

<p>What a bad day...</p>

<?php

ob_end_flush();

?>

The failed require call generates a fatal E_COMPILE_ERROR. We trap the error, and instead of telling the client about “my bad day” we retract that and give the user an ever so comforting message that all is okay even though our website blew up. Instead of doing something smart like logging the error, we display it:

<h1>Bad Stuff Happend</h1><p>But that is okay</p><code>Array
(
[type] => 64
[message] => require() [<a href="function.require">function.require</a>]: Failed opening required 'FileNotFound.php' (include_path='.:/usr/local/lib/php')
[file] => /home/michael/www/eggplant/public_html/samples/fatal/index.php
[line] => 28
)
</code>

If we fix the missing required file by adding the following script:

<?php
i'm an idiot
?>

…we are able to catch the E_PARSE fatal error in our shutdown handler:

<h1>Bad Stuff Happend</h1><p>But that is okay</p><code>Array
(
[type] => 4
[message] => syntax error, unexpected T_STRING
[file] => /home/michael/www/eggplant/public_html/samples/fatal/FileWithParseError.php
[line] => 2
)
</code>

Let’s fix up that required file with some real code:

<?php

myBadTypo();

function myBadDay()
{
ini_set('memory_limit', '5000');

echo str_repeat('bad bad bad', 50000);
}

?>

...Another caught E_ERROR:

<h1>Bad Stuff Happend</h1><p>But that is okay</p><code>Array
(
[type] => 1
[message] => Call to undefined function myBadTypo()
[file] => /home/michael/www/eggplant/public_html/samples/fatal/FileThatBlowsMemLimits.php
[line] => 3
)
</code>

Finally, let’s say we get the required file sorted out but blow the bank with available memory:

<?php

myBadDay();

function myBadDay()
{
ini_set('memory_limit', '5000');

echo str_repeat('bad bad bad', 50000);
}

?>

...we are able to catch the E_ERROR fatal error in our shutdown handler too:

<h1>Bad Stuff Happend</h1><p>But that is okay</p><code>Array
(
[type] => 1
[message] => Allowed memory size of 262144 bytes exhausted (tried to allocate 550001 bytes)
[file] => /home/michael/www/eggplant/public_html/samples/fatal/FileThatBlowsMemLimits.php
[line] => 9
)
</code>

That’s all pretty good, however there are some limits that need to be taken into consideration:

  • E_PARSE errors on the include that builds out the shutdown structure will not be caught, as the interpreter can’t get beyond the parse error to put it into place.
  • E_CORE_ERROR errors? These are environmental errors caught when PHP itself is bootstrapping — pre-script interpretation. No dice.
  • When using output buffering in combination with the ob_gzhandler, take care when cleaning the buffer. Chances are the Content-Encoding: gzip header was already set and you will need to follow suit.
  • If your output buffer has already been flushed before hitting a fatal error and the shutdown function, your content to that point is already gone. Check ob_get_status to see if that is the case.

For an similar implementation using a shutdown function, check out eZ Components’ Execution class.

References
Published at DZone with permission of its author, Michael Caplan. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)