Error handling можно перевести как "работа с ошибками". В этой статье я немного расскажу о том, как это устроено в Друпале, и как этот процесс можно улучшить для облегчения отладки вашего кода.
PHP по умолчанию использует вывод ошибок прямо в окно браузера (я думаю, все видели это неприглядные, но крайне полезные сообщения вверху веб-страницы типа "notice: Use of undefined constant.. "), и предоставляет возможность разработчику переопределить функцию работы с ошибками. Можно, например, сохранять информацию об ошибках PHP интерпретатора в базу данных (как сделано в Друпале), слать емейлы админу, и т.д.
Переопределяется функция вот так:
У ошибок бывают разные уровни "тяжести" - notice, warning, fatal error, parse error, user error, ... В Друпаловскую функцию error_handler "долетают" только notice и warning + искусственно вызванные самим друпалом user error, по причинам сложности перехвата fatal и parse error (перехват fatal error возможен, но немного напоминает пляску с бубном, я не буду этой темы касаться сейчас, дабы не отклоняться от намеченной темы).
Вот вкратце и все. Теперь про то, как мы можем облегчить себе жизнь в Друпале.
1. Включаем вывод ошибок уровня E_NOTICE
По непонятным до сих пор мне причинам, нотисы в друпале жестко отключены. Разработчики аргументируют свое решение тем, что код ядра системы "не избавлен от нотисов". Это мы очень явно видим, когда запускаем install.php и получаем кучу ошибок, если в php.ini у нас стоит error_reporting(E_ALL);
Скрипт инсталлятора не использует error_handler Друпала (по понятным причинам, сам Друпал-то не установлен еще, а значит и фунции ядра использовать нет возможности), и поэтому приходится писать error_reporting(0); в начале install.php
После установки Друпала нотисов мы больше не увидим. Друпал берет на себя весь процесс и скрывает ошибки уровня E_NOTICE. Это значит, например, что если мы опечатаемся в написании переменной или константы, то не узнаем об этом. Лично меня такой подход совсем не устраивает - поэтому лезем в /includes/common.inc, находим функцию error_handler и строку 551
заменяем на
вуаля!
Друпал нас сразу радует нотисами:
notice: Undefined variable: no_theme_preprocess in y:\home\d5\www\includes\common.inc on line 1493.
Можно конечно заняться фиксингом кода ядра, дабы избавить его от этой парочки предупреждений (благо это совсем несложно), но потом открываем другую страницу и какой-нибудь модуль типа Views радует нас еще десятком сообщений об ошибках. Становится понятно, что все не перефиксишь Да и обновление модулей сразу сведет все наши старания на нет.
Что мы можем сделать? Включить нотисы только для нашего модуля.
Для этого доработаем функцию error_handler и определим дополнительную Друпаловскую переменную в settings.php:
В конец \sites\default\settings.php дописываем:
Так я включаю вывод нотисов только для двух файлов, '/modules/views/views.module' и '/includes/common.inc'
Теперь изменения в error_handler() (/includes/common.inc, строка 551) :
заменяем наше
на
$error_level = E_ALL ^ E_NOTICE;
// fix paths if in windows
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
$unified_filename = str_replace(DIRECTORY_SEPARATOR, '/', $filename);
} else {
$unified_filename = $filename;
}
$debug_files = variable_get('debug_files', array());
foreach($debug_files as $debug_file) {
if(strpos($unified_filename, $debug_file) !== FALSE) {
$error_level = E_ALL;
break;
}
}
if ($errno & ($error_level)) {
Заметьте, что можно разрешить нотисы например для всей папки /modules прописав в конфиг '/modules/'
2. Выводим backtrace
UPD: что описано в этом разделе, можно получить установив модуль devel: http://drupal.org/project/devel
Внимание: все изменения, которые вы внесли в родной Друпаловский error_hander, во время включенного devel не будут иметь силы, контроль ошибок будет перехвачен внутренней функцией devel - backtrace_error_handler()
В процессе разработки довольно часто вылазят ошибки, которые по названию файла и номеру строки не выловишь (например: какой-то модуль делает неверный sql запрос, а друпал нам выводит ошибку:
По этому сообщению мы лишь можем понять, что сама ошибка была в database.mysql.inc, но это и так понятно, ведь все модули общаются с базой через функции database.mysql.inc, в частности db_query(). А вот какой модуль этот самый запрос выполнял?
Чтобы узнать, какой модуль вызвал _db_query, нам надо отследить всю цепочку вызовов, предшествовавших нашей ошибке:
_db_query() // db_query вызвал _db_query, который вызвал ошибку
db_query() // а это node_load() вызвал db_query()
node_load() // а это наша функция вызвала node_load()
our_module_function() // это та самая функция с ошибкой!
// тут предыдущие вызовы
Сделать такое нам позволит PHP функция debug_backtrace(), которая возвращает стек всех вызовов в виде массива.
После модификации нашей функции error_handler() вместо вот такого малоинформативного скрина мы получаем вот такой отчет, что нам очень упростит отлов проблемы в коде. (Если вы внимательно посмотрите код ниже, то обнаружите, что я закомментировал распечатку значений аргументов функций в бектрейсе, т.к. это ломает верстку страницы - ведь друпал передает в некоторые функции html код. можно все аргументы прогонять через htmlspecialchars к примеру, и обрезать по длине, но я не стал усложнять этим свой код. Думаю, что заинтересовавшимся при желании не составит большого труда навести лоск на внешний вид сообщений.)
P.S. В Drupal 6 разработчики уже включили бектрейсинг, но почему-то сделали это достаточно замудренным и неоднозначным способом - только для функций для работы с базами данных..
Привожу полный код получившейся функции error_handler(), includes/common.inc:
// If the @ error suppression operator was used, error_reporting is temporarily set to 0
if (error_reporting() == 0) {
return;
}
//default error level
$error_level = E_ALL ^ E_NOTICE;
// fix paths if in windows
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
$unified_filename = str_replace(DIRECTORY_SEPARATOR, '/', $filename);
} else {
$unified_filename = $filename;
}
$debug_files = variable_get('debug_files', array());
foreach($debug_files as $debug_file) {
if(strpos($unified_filename, $debug_file) !== FALSE) {
$error_level = E_ALL;
break;
}
}
if ($errno & ($error_level)) {
$types = array(1 => 'error', 2 => 'warning', 4 => 'parse error', 8 => 'notice', 16 => 'core error', 32 => 'core warning', 64 => 'compile error', 128 => 'compile warning', 256 => 'user error', 512 => 'user warning', 1024 => 'user notice', 2048 => 'strict warning');
$entry = $types[$errno] .': '. $message .' in '. $filename .' on line '. $line .'.';
// BACKTRACING CODE
$backtrace = debug_backtrace();
array_shift($backtrace); //first element is current function, 'error_handler'
if(count($backtrace) > 1) {
$table_header = Array('File', 'Function');
$table_data = Array();
foreach($backtrace as $file) {
$arguments = '';
if($file['args']) {
// if you want to see arguments passed to functions uncomment these lines (it will most likely break your page layout)
/*if(count($file['args']) > 1) {
@$arguments = implode(' ,', $file['args']);
} else {
$arguments = $file['args'][0];
}*/
}
$file['file'] = isset($file['file']) ? $file['file'] : 'undefined';
$file['line'] = isset($file['line']) ? $file['line'] : 0;
$table_data[] = Array("$file[file] (line $file[line])", "$file[function]($arguments)");
}
}
$entry .= theme('table', $table_header, $table_data);
// Force display of error messages in update.php
if (variable_get('error_level', 1) == 1 || strstr($_SERVER['SCRIPT_NAME'], 'update.php')) {
drupal_set_message($entry, 'error');
}
watchdog('php', t('%message in %file on line %line.', array('%error' => $types[$errno], '%message' => $message, '%file' => $filename, '%line' => $line)), WATCHDOG_ERROR);
}
}
Комментарии
Осталось добавить, что если вам совсем не нужно сохранение ошибок PHP в базу данных, вы можете сделать сохранение ошибок в текстовый файл.
Для этого вместо
set_error_handler('error_handler');
надо вставить
ini_set("display_errors", False);
ini_set("log_errors", True);
ini_set("error_log", "../outside/of/web/root/error_log.txt");
+1
Удручает, что даже разработчики ядра не пытаются писать так, чтобы не было даже NOTICE.
Тут многие(включая меня) с грубыми ошибками в словах пишут, а вы хотите от многонациональной тусовки прозрачности кода
Отличная статья. В закладки.
ну не знаю.. у меня вот тепло на душе когда пишу правильно это касается не только php, но и стандартов html/css
Разве это нормально, применять конкатенацию к несуществующей переменной?
$a .= 'tratata'
по-моему нет..
по-моему однозначно правильнее сначала переменную определить просто ради порядка в коде. Да ведь и несложно это, совсем!
Backtrace также умеет выводить модуль Devel (http://drupal.org/project/devel ) без правки кода ядра Drupal.
кстати да, вы правы.
Don't hack Drupal, use devel
А для серьезных проектов все равно ничего лучше нормального ide с настроеным дебагом и брейкпоинтами не придумали.
Отличная статья для нового docs