Профессиональная разработка Web-приложений.  
Боишься нашего дизайна?
Новости
PDF журнал
Участники проектa
Сотрудничество
Ссылки
Карта сайта
Комментарии
Комментарии к статье
Добавить комментарий
Обсудить на форуме
Информация об авторе
Оценка статьи

Ловля ошибок в 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 Признаю, немного параноидален. Но лучше два раза проверить, чем один раз сделать ошибку.

ps/2 По просьбе Maxim Naumenko добавляю комменты к статье:

Q: Ну и чем это лучше, чем просто в php.ini указать error_log = "log_file.log" ?


A: Файл пишется в нашем формате. Нам же потом этот файл смотреть надо. Плюс - можно делать что угодно с этими ошибками (файл - это просто для примера). А в случае с error_log = "" - они ТОЛЬКО пишутся в файл и ничего более. Да и не везде вас пустят к php.ini.




For comment register here
   2002-10-03 11:48
Так как &quot;Можно было бы, конечно, использовать более логичное для таких целей хранилище - базу...&quot;, то ещё более логичнее обрабатывать &quot;код ошибки&quot; в финкции, и если он не имеет отношение к работе с СУБД, то спокойно писать в базу.else to e-mailelse to file

   2002-10-03 16:16
Классическая забава для тех, кому нечем заняться.E_ALL для своих и error_reporting(0) для остальных. Лог в файл. К php.ini не пускают? А как же php_value в .htaccess?Формат данных неудобен? А зачем лог вообще нужен: для сложного анализа или посмотреть - что сейчас имеет свойство падать. А вы их ещё и архивируете :)

   2002-10-06 22:56
попробуй array_key_exists то есть

if(array_key_exists("id",$_GET)){ $_GET['id']== $id}

   2002-10-08 11:18
У меня давно уже все ошибки в XML заворачиваются и по почте летят мне на ящик.
В XML - потому что так мне его смотреть удобнее. Кроме переданной в функцию-хэндлер информации, там записано очень много других полезных вещей.

   2002-10-09 12:48
>Классическая забава для тех, кому нечем заняться.
я бы так не стал говорить.

>К php.ini не пускают? А как же php_value в .htaccess?
а вы не в курсе, что и это можно отрубить ?
понятие "универсальность" вам знакомо ?
здесь следует говорить именно о нем.

>Формат данных неудобен?
..МНЕ ЛИЧНО.
вам удобен - не используйте.

>А вы их ещё и архивируете :)
хочется ходить на ФТП каждый час ?
ходите, я не запрещаю.
мне удобней, чтобы меня извещали автоматом.
ЭТО ПРИМЕР.
пример элементарный.
если для вас этот пример выглядит сложно (ой, архивируем!), то заходите на форум - поможем.
если слишком просто - неясно зачем тогда было его читать.

   2002-10-09 12:49
Да нет проблем.
это просто пример.
на реальном сайте это пример почти бесполезен имхо.

   2002-10-09 12:51
а такое не подойдет ?
if (isset($_GET['id'])) {
....
}

   2002-10-09 12:54
Обладающие встроенным парсером XML'я могут XML где угодно использовать, даже при составлении резюме на новой работе.
Мне было удобно такой пример привести.
Если хотите - давайте свой, выложим, обсудим.
Можно в форуме: http://phpclub.net/talk/

   2002-11-08 16:52
Если используется свой обработчик ошибок, то установка уровня ошибок с помощью error_reporting() не имеет смысла, т.к. в обработчик будут идти все ошибки и предупреждения

   2002-11-25 16:38
Все это здорово, но как сформировать "красивую" страницу с ошибкой? Ошибка возникает в процессе выполнения кода как правило после того как часть кода уже видна. Страница получается рваной, незавершенной или завершенной некорректно.

   2003-05-18 14:23
в файле read.php маленький баг.
строка 63: вместо $error_types[$row[1]], пишем
$error_type[$row[1]]


   2004-01-25 22:59
(В файле index.php)
45 строка:
> rename(LOG_FILE_NAME.'..'.$i,LOG_FILE_NAME. '.' .(1+$i--));
Откуда 2 точки в имени файла ?
Надо заменить на:
rename(LOG_FILE_NAME.'.'.$i,LOG_FILE_NAME. '.' .(1+$i--));

(B файле read.php)
Раз уж скрипт написан для register_globals off, то в 85 строка некорректна:
> echo " &nbsp; [<a href=\"".$SCRIPT_NAME."?log=".$n."\">".$n."</a>] &nbsp; ";
Заменить на:
echo " &nbsp; [<a href=\"".$_SERVER['SCRIPT_NAME']."?log=".$n."\">".$n."</a>] &nbsp; ";

Скрипт чтения логов позволяет смотреть сохраненные логи, но вернуться на начальный (активный файл текущих логов) нельзя. Поэтому желательно изменить 83 строку в read.php на:
$i>0 ? print "<tr>\n <th colspan='6'>\n &nbsp; [<a href=''>0</a>] &nbsp;\n " : true;

   2004-01-26 00:08
И еще. В файле read.php в строке 41:
> $log_file = $log!==false ? LOG_FILE_NAME.'.'.$log : LOG_FILE_NUM;
LOG_FILE_NUM нужно заменить на LOG_FILE_NAME
Правильно:
$log_file = $log!==false ? LOG_FILE_NAME.'.'.$log : LOG_FILE_NAME;

   2004-08-07 14:43
> Все это здорово, но как сформировать "красивую" страницу с ошибкой?
> Ошибка возникает в процессе выполнения кода как правило после того как
> часть кода уже видна. Страница получается рваной, незавершенной или
> завершенной некорректно.

Буферизируй вывод. напирмер, с помощью ob_start().

   2007-04-04 15:38
это всё замечательно, но в случае фатальных ошибок мы вынуждены лезть в error_log. так что лучше организовать нормальную работу с error_log. например - повесить демона, который в случае фатальных ошибок будет слать мыло админу... итд итп

&gt;Да и не везде вас пустят к php.ini.
а к .htaccess?

   2007-04-04 15:44
&gt;&gt;К php.ini не пускают? А как же php_value в .htaccess?
&gt;а вы не в курсе, что и это можно отрубить ?
а можно вообще сервер отрубить... так, о чём это мы? ах да, о сферических конях..

   2007-11-26 22:46
Пожулуста востановите архив с исходным кодом:
http://tony2001.phpclub.net/set_error_handler.tar.gz

На серъезных сайтах странно видеть, когда ошибки выводятся пользователю в браузер в самых неожиданных местах. Почему они выводятся? Ведь текст ошибок является информацией для дебага и предназначена для разработчика, а не для клиента. Пишем программу, дела

 
 
 
    © 1997-2008 PHPClubTeam
[]