Ловля ошибок в PHP
Автор: Антон Довгаль
На серъезных сайтах странно видеть, когда ошибки выводятся
пользователю в браузер в самых неожиданных местах. Почему они
появляются - это отдельный разговор. Но почему они выводятся ?
Ведь текст ошибок является информацией для дебага и
предназначена для разработчика, а не для клиента.
Кроме того, именно эта служебная информация обычно помогает
злым хакерам ломать сайт. В качестве классического примера
можно привести вариант с выводом запроса при ошибке: "you have an error in query near WHERE id= "
...
Большое спасибо. Подставляем после "WHERE id=..." строку "0 OR
1>0" и запрос выполняется по всей таблице. Если запрос на
удаление, то...сами понимаете, весело =). Поэтому я всегда
переменные в запросах заключаю в кавычки. На всякий случай...
Но я увлекся. Сегодня не об этом. Сегодня поговорим о том,
как избежать вывода ошибок клиенту, сохранив при этом все
сообщения вебмастеру на память.
Начнем, пожалуй, с краткого обзора видов ошибок в
РНР.
Таблица 1. Описания ошибок в PHP4 (оригинальный список)
Числовое
значение |
Константа |
Описание |
Ловится/нет |
1 |
E_ERROR |
Фатальные ошибки. Например, ошибка при обращении к
памяти. Выполнение скрипта при этом прерывается. |
нет |
2 |
E_WARNING |
Предупреждения (не фатальные ошибки). Выполнение
скрипта не прерывается. |
да |
4 |
E_PARSE |
Ошибки во время анализа синтаксиса. Генерируются
парсером. |
нет |
8 |
E_NOTICE |
Замечания (менее серьезные ошибки, чем
предупреждения). Указывают на ситуацию, которая может
стать причиной более серьезной ошибки, но могут
случаться и в процессе нормальной работы скрипта. |
да |
16 |
E_CORE_ERROR |
Ошибки во время загрузки РНР. Аналог E_ERROR,
генерируется ядром РНР. |
нет |
32 |
E_CORE_WARNING |
Предупреждения во время загрузки РНР Аналог
E_WARNING, генерируется ядром РНР. |
нет |
64 |
E_COMPILE_ERROR |
Фатальные ошибки во время компиляции кода. Аналог
E_ERROR, генерируется зендовским движком. |
нет |
128 |
E_COMPILE_WARNING |
Предупреждения во время компиляции кода. Аналог
E_WARNING, генерируется зендовским движком. |
нет |
256 |
E_USER_ERROR |
Пользовательская ошибка. |
да |
512 |
E_USER_WARNING |
Пользовательское предупреждение. |
да |
1024 |
E_USER_NOTICE |
Пользовательское замечание |
да |
Нас интересуют те ошибки, которые мы можем перехватить. К
ним относятся: E_WARNING, E_NOTICE и E_USER_*. Остальные виды
ошибок перехвату не поддаются либо из-за того, что происходят
они еще до окончания загрузки самого ядра РНР, либо из-за
того, что происходят на этапе синтаксического анализа и
компилирования РНР-кода, поэтому их вывод придется просто
отключить:
ini_set('display_errors',0);
Но я предполагаю, что наши скрипты достаточно отлажены,
чтобы в них не было элементарных синтаксических ошибок,
поэтому потерять мы ничего не должны.
По умолчанию уровень ошибок в РНР имеет значение E_ALL
& ~E_NOTICE (или 2039 в числовой форме), что означает, что
мы пропускаем мимо ушей замечания, но сообщаем о всех
остальных ошибках.
Кстати, сами разработчики рекомендуют включать на стадии
разработки и E_NOTICE - помогает обнаружить потенциально
опасные места.
Поэтому изменим уровень вывода ошибок на E_ALL:
error_reporting(E_ALL);
Теперь переопределим хэндлер ошибок и подставим вместо него
нашу функцию user_log(),
которая и будет заниматься теперь обработкой ошибок:
set_error_handler('user_log');
Рассмотрим эту функцию подробней. Ей передаются 5
параметров:
- код ошибки
- текст ошибки
- имя файла, в котором произошла ошибка
- строка в файле
- массив переменных
Возвращать эта функция ничего не обязана. Так как мы
собираемся просматривать потом лог ошибок, то надо сделать
запись лога, например, в файл так, чтобы нам потом было удобно
с ним работать.
Итак, код с комментариями:
<?php
/* Наша функция-хэндлер */
function user_log ($errno, $errmsg, $file, $line) {
// время события
$timestamp = time();
//формируем новую строку в логе
$err_str = $timestamp.'||';
$err_str .= $errno.'||';
$err_str .= $file.'||';
$err_str .= $line.'||';
$err_str .= $errmsg."\n";
//проверка на максимальный размер
if (is_file(LOG_FILE_NAME) AND filesize(LOG_FILE_NAME)>=(LOG_FILE_MAXSIZE*1024)) {
//проверяем настройки, если установлен лог_ротэйт,
//то "сдвигаем" старые файлы на один вниз и создаем пустой лог
//если нет - чистим и пишем вместо старого лога
if (LOG_ROTATE===true) {
$i=1;
//считаем старые логи в каталоге
while (is_file(LOG_FILE_NAME.'.'.$i)) { $i++; }
$i--;
//у каждого из них по очереди увеличиваем номер на 1
while ($i>0) {
rename(LOG_FILE_NAME.'..'.$i,LOG_FILE_NAME. '.' .(1+$i--));
}
rename (LOG_FILE_NAME,LOG_FILE_NAME.'.1');
touch(LOG_FILE_NAME);
}
elseif(is_file(LOG_FILE_NAME)) {
//если пишем логи сверху, то удалим
//и создадим заново пустой файл
unlink(LOG_FILE_NAME);
touch(LOG_FILE_NAME);
}
}
/*
проверяем есть ли такой файл
если нет - можем ли мы его создать
если есть - можем ли мы писать в него
*/
if(!is_file(LOG_FILE_NAME)) {
if (!touch(LOG_FILE_NAME)) {
return 'can\'t create log file';
}
}
elseif(!is_writable(LOG_FILE_NAME)) {
return 'can\'t write to log file';
}
//обратите внимание на функцию, которой мы пишем лог.
error_log($err_str, 3, LOG_FILE_NAME);
}
?>
Весь код вы можете посмотреть тут или взять все в архиве.
Можно было бы, конечно, использовать более логичное для
таких целей хранилище - базу, но ведь ошибки, в большинстве
своем, возникают именно при работе с базой, поэтому я бы на
нее не полагался.
Собственно, это все. Остальное, я думаю, не составит для
вас труда, особенно, если пользоваться функциями file();
& explode();
. А если все-таки составит,
то вы можете воспользоваться [вот
этим кодом].
Предвидя вопрос "почему я не использовал CSV, который,
казалось бы, логично использовать в этой ситуации?", отвечаю:
сообщения об ошибках могут содержать неизвестное количество
служебных символов (ака запятых и точек с запятой), что явно
затруднило бы разбор CSV. Да и не собираюсь я просматривать
лог в Экселе.
Еще разные мысли на эту тему:
- при устаревании лога gz'иповать файл и складывать его в
архив;
- то же, но с посылкой на почту;
- при возникновении критических ошибок - слать мэйл (см.
пример из мануала по функции set_error_handler);
- для мазохистов можно использовать при этом XML.
Вздохнули спокойно? Я надеюсь, что нет. Ибо переопределение
еррор-хэндлера - это никак не панацея, просто одна из удобных
фич РНР.
Кто предупрежден, тот защищен - так ведь?
ps Признаю, немного параноидален. Но лучше два раза
проверить, чем один раз сделать ошибку.
ps2 По просьбе Maxim Naumenko добавляю комменты к статье:
Q: Ну и чем это лучше, чем просто в php.ini
указать error_log = "log_file.log"
?
A: Файл пишется в нашем формате. Нам
же потом этот файл смотреть надо. Плюс - можно делать что
угодно с этими ошибками (файл - это просто для примера). А в
случае с error_log = "" - они ТОЛЬКО пишутся в файл и ничего
более. Да и не везде вас пустят к
php.ini.