Отладка PHP-скриптов

Как известно, при возникновении ошибки PHP в лучшем случае выдает номер строки, где она произошла, и ее краткое описание, а в худшем (если в настройках хостинга отключен показ ошибок вообще) — просто пустую страницу. Это не слишком удобно как для отладки, так и для конечного пользователя. Возникает вопрос: как сделать вывод сообщения об ошибке более информативным.

Оказывается, все достаточно просто. В PHP существует специальная функция set_error_handler(), которая позволяет задать свой собственный обработчик ошибок.Единственный ее параметр — это имя функции-обработчика, которая вызывается в случае возникновения ошибки. Функция-обработчик имеет 4 параметра: номер ошибки, текст ошибки, имя файла, в котором ошибка произошла и номер строки в этом файле.

Но этих данных может быть недостаточно. Часто требуется вывести и весь стек вызовов, чтобы понять, где именно произошла ошибка. Получить стек вызвов можно с помощью функции debug_backtrace, которая возвращает массив обратных вызовов. Каждый элемент этого массива является хешем со следующими полями: function — имя функции, args — аргументы функции, file — имя файла скрипта, в котором она была вызвана. line — номер строки, где был выполнен вызов функции.

Внимание: в ряде случаев file и line бывают не заданы, всегда проверяйте их (да и вообще любые индексы массива) с помощью isset, а константы — с помощью defined, иначе возникнет рекурсивный вызов обработчика ошибок. Кроме того, если сделать вывод аргументов функций, нужно принять меры, чтобы не выдавался пароль от базы данных в том случае, если ошибка возникнет на этапе подключения к СУБД (т.е. при вызове функции mysql_connect или ей подобной). Самый простой вариант — заменять его с помощью str_replace на звездочки или иные спецсимволы перед выводом.

Рассмотрим пример функции-обработчика:

 function error_handler($errno, $errstr, $errfile, $errline) {
$dbg_on = defined('CONFIG_debug') ? CONFIG_debug : false;
$debug=debug_backtrace();
$errmsg='<p>'.$errstr.' (строка '.$errline.', '.$errfile.', ошибка: '.$errno.')'.'</p><ul style="font-size: 0.9em; color: #600">';
$count=count($debug);
for ($i=1; $i<$count; $i++) { // обрабатываем с единицы, так как нулевой элемент — это вызов самого обработчика error_handler
$errmsg.='<li>'.$debug[$i]['function'].'()'.
' — '.((isset($debug[$i]['file'])) ? $debug[$i]['file'] : 'неизвестный файл').', '.
'строка '.((isset($debug[$i]['line'])) ? $debug[$i]['line'] : 'неизвестна').'</li>'; //.var_dump($debug[$i]['args'])
} $errmsg.='</ul>';
if (($errno & E_ERROR) || ($errno & E_USER_ERROR)) { // если ошибка фатальная и требует прекращения работы скрипта
if (!headers_sent()) { // если заголовок страницы еще не был отправлен
header($_SERVER['SERVER_PROTOCOL'].' 500 Internal Server Error');
header('Content-Type: text/html; charset=utf-8');
echo '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<head>
<title>Ошибка сайта</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head><body>';
}
$email = defined('CONFIG_email') ? CONFIG_email : '';
echo '<div style="font-size: 1em; padding: 4px; font-weight: bold; color: #C44; border: #C00 1px solid; margin: 4px">На сайте произошла ошибка. Попробуйте обновить страницу через несколько минут. Если ошибка не исчезнет, пожалуйста, сообщите о ней администратору сайта по адресу <a href="mailto:'.$email.'">'.$email.'</a> ';
if ($dbg_on) {
echo '<p>'.$errmsg.'</p>';
}
echo '</div></body></html>';
}
else {
// здесь можно вставить обработку предупреждений (warning) и примечаний (notice), например, с помощью функции _dbg, о которой см. ниже
}
}

Эта функция получает стек вызова и проверяет, включен ли отладочный режим (он определяется константой CONFIG_debug, которая задается, например, в файле конфигурации вместе с настройками подключения к БД). Дальше идет проверка, отправлены ли заголовки (т.е. делал ли скрипт до этого какой-то вывод). Если нет, то выдается статус 500 (внутренняя ошибка сервера), чтобы поисковые системы не воспринимали сообщение об ошибке как контент, который нужно индексировать, и формируется страница с сообщением об ошибке и предложением написать администратору, адрес которого задан в константе CONFIG_email. Если отладочный режим включен, то выдается и полный стек вызовов функций перед ошибкой. При необходимости в эту же функцию можно добавить сохранение отладочной информации в лог-файл.

Также часто при отладке скриптов требуется вывести значение какой-либо переменной, чтобы понять, на каком этапе берутся неправильные данные. Использование для этого print_r — не самое лучшее решение (особенно при отладке на живом сайте), так как значение может вывестись в неподходящем месте (например, если используется шаблонизатор, то еще до HTML-заголовка). Кроме того, Более корректное решение — запомнить значение в отладочную переменную, которую вывести потом там, где ее появление не будет мешать (например, в подвале сайта).

Я для этих целей использую такую функцию:

function _dbg() {

$dbg_on = defined('CONFIG_debug') ? CONFIG_debug : false;

if ($dbg_on) {

if (!isset($GLOBALS['IntBF_debug'])) $GLOBALS['IntBF_debug']='';

$GLOBALS['IntBF_debug'].='<p>';

foreach (func_get_args() as $name=>$value) {

if (is_array($value) || is_object($value)) $GLOBALS['IntBF_debug'].=$name.': '.nl2br(str_replace(' ','&nbsp;',htmlspecialchars(print_r($value,true))));

else $GLOBALS['IntBF_debug'].=$name.': '.htmlspecialchars($value).' ';

}

$GLOBALS['IntBF_debug'].="</p>\n";

}

}

Эта функция экранирует все переданные в нее параметры и запоминает в глобальную переменную $GLOBALS['IntBF_debug'], из которой ее можно вывести в любом подходящем месте с помощью обычного echo.

Если требуется прервать выполнение скрипта с ошибкой по инициативе разработчика (например, после ошибочного SQL-запроса), следует использовать функцию trigger_error. У этой функции два параметра: первый — строка с сообщением об ошибке, которая будет передана в параметр $errstr обработчика, а второй — код ошибки, который может быть одной из трех констант: E_USER_ERROR (приведет к завершению выполнения), E_USER_WARNING, E_USER_NOTICE.

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