Статьи

Mayday, Mayday, Mayday — PHP идет вниз


PHP предоставляет множество
инструментов для обработки ошибок и исключений , в частности
расширяемые
обработчики для управления ошибками и исключениями по мере их возникновения. Тем не менее, существует ряд фатальных
ошибок, которые PHP не предоставляет непосредственно инструментам для их обработки. В лучшем случае эти фатальные ошибки приведут к частичным выводам страниц или «белому экрану смерти».

Эти ошибки включают в себя:


E_ERROR

Фатальные ошибки времени выполнения.
Они указывают на ошибки, которые не могут быть восстановлены, такие как проблема выделения памяти. Выполнение скрипта остановлено

E_PARSE

Ошибки синтаксического анализа во время компиляции.
Ошибки разбора должны генерироваться только парсером

E_CORE_ERROR

Неустранимые ошибки, возникающие при первоначальном запуске PHP.
Это похоже на E_ERROR, за исключением того, что оно генерируется ядром PHP

E_COMPILE_ERROR

Фатальные ошибки времени компиляции.
Это похоже на E_ERROR, за исключением того, что оно генерируется Zend Scripting Engine.

Используя комбинацию функции выключения , error_get_last и буферизации вывода , можно создать структуру, которая позволит вам перехватывать фатальные ошибки. Теория работы довольно проста:

  1. Зарегистрируйте функцию отключения.
  2. Когда вызывается функция завершения работы, проверьте, завершил ли сценарий выполнение должным образом, запросив error_get_last на наличие фатальных ошибок.
  3. Обработайте фатальную ошибку, как считаете нужным.

Давайте посмотрим на некоторый код:

<?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();
?>

Вся сгенерированная страница перед отправкой клиенту помещается в выходной буфер. Использование здесь буферизации вывода позволяет нам избавиться от частично сгенерированного контента при возникновении фатальной ошибки и заменить его пользовательским сообщением об ошибке. Например:

<?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();

?>

Неудачный вызов require генерирует фатальный E_COMPILE_ERROR. Мы улавливаем ошибку, и вместо того, чтобы рассказывать клиенту о «моем плохом дне», мы убираем это и даем пользователю очень утешительное сообщение, что все в порядке, даже если наш сайт взорвался. Вместо того, чтобы делать что-то умное, например регистрировать ошибку, мы отображаем ее

<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>

Если мы исправим отсутствующий необходимый файл, добавив следующий скрипт:

<?php
i'm an idiot
?>

… Мы можем отловить фатальную ошибку E_PARSE в нашем обработчике завершения работы:

<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>

Давайте исправим этот необходимый файл с помощью реального кода:

<?php

myBadTypo();

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

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

?>

… Другой поймал 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>

Наконец, допустим, мы отсортировали необходимый файл, но взорвали банк с доступной памятью:

<?php

myBadDay();

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

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

?>

… мы можем перехватить фатальную ошибку E_ERROR и в нашем обработчике завершения работы:

<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.