PHP - Как вернуть управляемый контент при критической ошибке PHP (E_ERROR, E_PARSE)?

PHP*
Здравствуйте пытливые умы.
В данном вопросе я хотел бы продолжить популярную тему вопросов с обработками ошибок в PHP.

Условия.

1) Есть, код.
2) В одном из подключаемых файлах, допустим в одном из модулей закралась ошибка E_ERROR (допустим исключение или несовместимость кода под разные ОС, настройки серверов) или E_PARSE (случайно напортачил программист, и залил на сервер без теста, опаздывал к девушке например).
3) В случае рабочего сервера, где вывод ошибок отключен на соответствующей странице где ситуация случилось — выдает пустоту. Ниже приведу свои разработки на этот счет

Если вы заинтересовались условиями, то прошу под храбракат, тут еще интереснее.


Почему я привел 2 примера зная, что сущесвтуют set_exception_handler и set_error_handler?
Потому что в первый случай хотелось бы свести к унивесальному способу, а второй вариант не обрабатывает ошибки например E_PARSE, E_ERROR, он вообще тупо не запускает функцию перехвата. Хотелось убить 2 зайца одним разом.

Задача.

Отдать заголовки 404, в случае критических ошибок кода + некоторые данные, пускай даже статические вроде «ой, ошибка, сейчас мы это активно исправляем, заходите позже» (пояснения для пользователей).

Зачем?
Потому что поиковик должен на них напираться на 404, а не включать в индекс пустые страницы c 200 Ok + пользователю надо сообщить, что сейчас на сайте есть ошибка. В логи так или иначе это писаться будет, но не всегда в логи заглядываешь, если с виду все хорошо, ну и в меил тоже — мы же ушли к девушкам.

Решение с заголовками

<?php
header($_SERVER[«SERVER_PROTOCOL»].' '.'404'); // выставление заголовков перед генерацией контента

// Генерация контента, возможны ошибки работы кода, исключения, либо ошибки парса.
$data = generate();

header($_SERVER[«SERVER_PROTOCOL»].' '.'200'); // выставление правильных заголовков

// вывод данных к примеру, но лучше через буферизированный вывод
echo $data;
?>

Предположительное решение проблемы

  1. Мало, вот пытаюсь найти что-то вроде перехвата кодов ошибок на уровне Apache, но это жутко не универсально и привязывает к одному типу сервера. Более того я пока так и не нашел ни одного примера, как из PHP можно отправить Apache сообщение «Хьюстон у нас проблемы!», что бы тот загрузил дефолтную страницу ошибки.
  2. Через программный способ, то сейчас изучаю фунции буферизации вывода, может быть что дать сможет.
  3. Немного не представляю других способов, если конечно в PHP нет чего-нибуть этогкого, за это я возьмусь после первых 2-х пунктов.
  4. Есть вариант записать в вывод данные (как описано выше в Решение с заголовками), далее сбросить их и заново переписать с нуля с новыми заголовками — но это очень костыльно.

На все приведенное выше я не знаю решений, это только предположения.

Ссылки на материал на текущий момент

httpd.apache.org/docs/2.0/mod/core.html#errordocument
www.php.net/manual/ru/ref.outcontrol.php
php.net/manual/ru/function.set-error-handler.php
php.net/manual/ru/function.set-exception-handler.php
www.php.net/manual/ru/index.php

Господа, ваши предложения и главное решения с пруфлинками в студию

UPD:
Удивляюсь хомячкам, которые за редчайшие и тонкие вопросы минусуют карму, научитесь сами кодить для начала без костылей.

UPD2:
— В данном вопросе есть 2 ответа и все интересные, решение является наиболее элегантным, но второе решение тоже интересно.
— Тем кому опять-таки надо зарегистрировать метод класса прошу сюда, там я описал, как это делается.

UPD3:
В процессе разборов полетов, появилась дискуссия, которую советую прочитать для общего развития.
12 декабря 2011 в 16:29
29
wartur 8,7

отсортировано по дате по оценке
ответы (8)

+9
Aco #
register_shutdown_function(function () {
   $error = error_get_last();
   if ($error && ($error['type'] == E_ERROR || $error['type'] == E_PARSE || $error['type'] == E_COMPILE_ERROR)) {
       if (strpos($error['message'], 'Allowed memory size') === 0) { // если кончилась память
           ini_set('memory_limit', (intval(ini_get('memory_limit'))+64)."M"); // выделяем немножко что бы доработать корректно
           Log::error("PHP Fatal: not enough memory in ".$error['file'].":".$error['line']);
	} else {
           Log::error("PHP Fatal: ".$error['message']." in ".$error['file'].":".$error['line']);
        }
        // ... завершаемая корректно ....
    }
})



Ловит так же падения при отсутствии свободной памяти, ошибки парсинга, и прочего
Согласен, отличное и не очевидное решение, я бы даже сказал что эта функция будет получше предложенной, за исключением того что приходится самому писать в логи, мне бы хотелось бы от этого абстрагироваться, что я и указал выше. Но такие знания золото. В любом случае, буду знать. wartur, 12 декабря 2011 в 18:01
Хех, я понял, почему я в своё время отбросил register_shutdown_function… до 4.1.0 (а я и с ними дело имел) при помощи этой функции нельзя было менять данные отсылаемые пользователю. Mear, 12 декабря 2011 в 18:30
Это естественно, отправив заголовки, вы уже не сможете отправить их повторно. Поэтому и разделяют приложения на логическую часть и отображение (именно в этой последовательности) Aco, 12 декабря 2011 в 18:33
По поводу логов. Логи stderr всё равно пишутся, даже с этой функцией. Если не хотите писать в логи ошибки, то объявите set_error_handler и делайте там return true; Aco, 12 декабря 2011 в 18:35
Господа, давайте подискутируем, кого отметить решением? Судя по количеству народа выбирать надо это но, по поводу заголовков я совершенно уверен, что надо тогда решением ставить другой вариант. wartur, 12 декабря 2011 в 18:38
Понимаете, заголовки то тоже надо отправить что 503 произошло, в этом дело. wartur, 12 декабря 2011 в 18:39
А что с заголовками? Aco, 12 декабря 2011 в 18:40
Если вдруг произошла ошибка, будет ли работать header($_SERVER[«SERVER_PROTOCOL»].' '.'503');?
Смотрите. Выполняется код (с ответом 200 Ok, как я привел выше без того куска кода «Решение с заголовками»), происходит ошибка, надо вернуть и заголовки, и результат уведомления для пользователя.
wartur, 12 декабря 2011 в 18:42
Если вы ничего не отправляли то работать будет.
register_shutdown_function(function () {
   $error = error_get_last();
   if ($error && ($error['type'] == E_ERROR || $error['type'] == E_PARSE || $error['type'] == E_COMPILE_ERROR)) {
       if (strpos($error['message'], 'Allowed memory size') === 0) { // если кончилась память
           ini_set('memory_limit', (intval(ini_get('memory_limit'))+64)."M"); // выделяем немножко что бы доработать корректно
           Log::error("PHP Fatal: not enough memory in ".$error['file'].":".$error['line']);
	} else {
           Log::error("PHP Fatal: ".$error['message']." in ".$error['file'].":".$error['line']);
        }
        if(!headers_sent()) {
             header($_SERVER[«SERVER_PROTOCOL»].' '.'503');
             // ... и тд
        }
    }
});



Дело в том что я не любитель копить вывод ob по нескольким причинам:
1. не рациональное использование памяти сервера. Например отдача большого контента, yml, например.
2. невозможность использовать flush(). Отдавая страницу по частям, вы ускоряете её(визуально), но для пользователей это то что надо. Так делают гугл, яндекс, яху и все остальные (в основном контент делится на 3 части: шапка, тело, подвал. В подвали загружаются скрипты). Отличный доклад был на хайлоаде 2010 от яху, почитайте на досуге.
3. не прозрачный контроль вывода (в своё время меня доставали «почему ничего не выводится при какой-то ошибке», а потому что ob_start() ставят)

Но это другая история…
Aco, 12 декабря 2011 в 18:51
Что бы не пробравшийся SERVER_PROTOCOL лучше писать
header(«Service Unavailable», true, 503);
Aco, 12 декабря 2011 в 18:53
*пробрасывать Aco, 12 декабря 2011 в 18:53
php.net/manual/ru/function.register-shutdown-function.php
Господа, читаем матчасть, написано с 4.1.0 в этой функции можно делать все что требуется. Значит наиболее универсальным решением будет это.
Всем спасибо. Обсуждением 100% можно считать оконечным.
wartur, 12 декабря 2011 в 18:54
Aco, пожалуйста почитайте это, очень интересно, к сожалению буферизацией лучше пользоваться, чем не пользоваться. habrahabr.ru/blogs/php/45016/, кроме того у меня у самого сейчас все копится в echo, я буду это через буферизацию выводить (ибо идея сама по себе заложена на уровне PHP функций, что бы как можно меньше пользоваться самим языком программирования, так как он медленный). Напрямую выводить, без буферизации выводить к сожалению нельзя по крайней мере в моем случае. wartur, 12 декабря 2011 в 18:58
> Что бы не пробравшийся SERVER_PROTOCOL лучше писать
Пасиба, не знал что так можно не в случае редиректа.
wartur, 12 декабря 2011 в 19:00
> копится в echo
ой ну и чушь написал, я имел ввиду
$result.=$new_data1;
$result.=$new_data2;
//…
$result.=$new_dataN;

echo $result

Эта конструкция работает на 30% медленней чем echo. потом буду исправлять.
wartur, 12 декабря 2011 в 19:12
Внимание, заголовки невозможно контролировать с помощью этого метода, так как данные уже отправлены на момент запуска данной функции, пользуйтесь другим способом.

Код на тестирование
register_shutdown_function(array(&$this, 'ShutDown'));

public function ShutDown()
{
if(headers_sent()) {
echo «ой, заголовки уже отправлены»;
}
}
wartur, 12 декабря 2011 в 20:31
Извиняюсь, беру свои слова обратно, я чета уже битые несколько часов оттачиваю вопрос, поэтому глаз замылился, в коде у меня ошибочка ))) wartur, 12 декабря 2011 в 20:35
+4
sectus #
Что сразу попадает в глаз: надо 503 ошибку вместо 404.
Понял, вполне согласен, буду знать. wartur, 12 декабря 2011 в 17:17
0
xaker1 #
Почему в Q&A?
+2
SelenIT2 #
Когда-то давно, насколько я в курсе, подобную задачу (осмысленный вывод в случае фатальной ошибки PHP) решал Дмитрий Котеров. Не знаю, насколько сейчас это решение актуально, но общий принцип в любом случае, полагаю, может быть полезен.
Пруф линк замечательный, так и знал что надо в эту сторону копать. Пасибо. wartur, 12 декабря 2011 в 17:46
+2
Mear #
Подобную проблему я решал при помощи стандартного перехвата буфера вывода (ob_start...). Фокус в том, что обработчик буфера вызывается даже в случае фатальных ошибок (указанных вами), таким образом проверив в обработчике буфера, не произошла ли ошибка, мы уже можем слать нужные заголовки и т.д.

ob_start('ob_end');

...

function ob_end($outputData)
{
  $error = error_get_last();
      
  if (is_array($error) && in_array($error['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR)))
  {
    /* Фатальная ошибка, отдаем 500 */
  }

  return $outputData;
}
Пасиба друзья, знал что надо капать в эту сторону. Пасибо за ясный пример, сэкономил много времени. wartur, 12 декабря 2011 в 17:47
E_PARSE вроде нельзя поймать. sectus, 12 декабря 2011 в 18:08
Можно, если данное событие произошло внутри include или eval. Само-собой, что E_PARSE поймать в том же файле где и обработчик — нельзя. Mear, 12 декабря 2011 в 18:23
Ну это в любом случае, мы знаем что тестированный код, где определена сама функция у нас 100% работает. Вопрос не в этом. wartur, 12 декабря 2011 в 18:40
+2
easterism #
Ет все хорошо, но как-то забыли всем напомнить, что error_get_last() поддерживается, начиная с 5.2.0
Костыли для более ранних версий, это просто костыли.
+1
galaxy #
200 отдается не всегда: stackoverflow.com/a/8295606
Хотя Вашу задачу это не решит, т.к. заставить это работать в связке с ErrorDocument, видимо, нельзя
да уже и не нужно, решение которое я предполагал оказалось в 3-м пункте моего исследования, по сравнению с этим методом все остальное уже костыли. wartur, 12 декабря 2011 в 20:42
0
png #
Коллеги.
Во-первых, согласен с тем, что говорилось выше. Это тоже правильно. Сам делал аналогичные вещи, перекрывал ошибки, писал в лог, слал на почту и т.п.

Во-вторых, если бы меня попросили сделать такую фичу в хорошем добротном проекте, то я бы не стал изобретать велосипед, а воспользовался решением из коробки.
Популярные фреймворки Symfony2, Yii — уже поддерживают это из коробки, причем достаточно давно.
Переполнение по памяти не проверял, остальное ловит без проблем и отдает 500 ошибку, то есть как надо(поправка выше, не обязательно отдавать 503-ю. можно любую 5хх, этот жест скажет поисковику — «Ой, спроси меня позже...», 404 — в таком случае лучше не отдавать, оно может повлиять на позиции сайта в поиске — если для вас это важно.).

Единственное ограничение к стилю кода, пишите без варингов и нотисов, они тоже ловятся. Иначе рискуете сделать проект, которые будет работать только в продакшен-режиме, то есть при выключенных ошибках. Отлаживать такие веши — не самое приятное занятие. К тому же, количество ошибок в таком коде выше на порядок.

В-третьих, на проблему нужно смотреть шире.
То, что код на php вываливается, когда случается ошибка синтаксического рода или нехватка памяти — это мега плохо. Решение — перекрывать функции обработки ошибок — ихмо, костыль, но иногда без него никуда.

Как сделать такой костыль — описано чуть ли не в самом вопросе. Для себя я суть дискуссии понял так — а как нужно строить логику приложения (архитектуру — если хотите), чтобы было правильно с точки зрения обработки ошибок.

Пример с ошибками и статусами HTTP — не единственный. Возможны и другие варианты.

Идея вот в чем. Что и как нужно делать — зависит от вашего проекта. Это может быть не сложный сайт, который отдает информацию, а может быть достаточно сложный комплекс, в котором заложена бизнес-логика, она может отрабатывать как при получении страниц, так и при выполнении фоновых заданий(например, скрипты в CronTab).

Если в двух словах, то я думаю так:
— разбить код логики на блоки (желательно независимые, они могут быть ирерархичными).
— запускать эти блоки как бы «в песочнице» (вопрос как — зависит от самого приложение, у кого-то может и не получиться, или придется ради этого менять структуру БД, какие-то алгоритмы внутри приложения и т.п.)
— по успешному выполнению блока, применяем результат. Что-то сломалось, уведомляем админа о критичной ситуации.

Критичные ситуации лучше всего ловить Unit-тестами, а если это не возможно, то всеми видами тестирования.

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

— Про ошибки 5хх уловил инфу, не знал что 404 хуже чем 503, буду осторожней с этим.

> Критичные ситуации лучше всего ловить Unit-тестами, а если это не возможно, то всеми видами тестирования.
— признаюсь, что очень трудная технология производства через тесты.

— да, модульная система — это сила, один модуль отказал, несколько динамических страниц отвалилась, другие работают. Совершенно согласен.

> пишите без варингов и нотисов
надо вообще синтаксически правильно писать всегда и еще могу добавить, что надо не полениться настроить локальный сервер с xdebug, он имеет классную функцию, приятно для глаз ошибки выводит, и просто без xdebug работать невозможно.
wartur, 12 декабря 2011 в 21:46
>> должен писать только бизнес логику
это как, можно подробней?

xdebug — да — действительно вещь

>> признаюсь, что очень трудная технология производства через тесты.
да, тяжело себя перестроить. Однако качество кода повышает на порядок. А когда привыкните, то будете писать код быстрее, чем раньше, экономя время на простой отладке.

+ ловить разные сложные ситуации удобно.
например, ситуация:
function test(User $user) {...}
если при вызове функции тест передать туда не объект User, а что-то ещё, то возникнет синтаксическая ошибка. Такие вещи ловятся логикой, в которой надо зашить то, чтобы неправильно функцию test никто не вызывал.
Логику желательно описывать тестами. (читай TDD)
png, 12 декабря 2011 в 22:02
>>> должен писать только бизнес логику
>> это как, можно подробней?
— Ну то есть, допустим в данном случае, пользователь не должен думать над тем, что будет, если появится ошибка в коде или необработанное исключение. Решения в случае экстренных действий должна решать платформа.
wartur, 12 декабря 2011 в 23:17

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.