Улучшаем error handling в Drupal 5

Главные вкладки

Аватар пользователя restyler restyler 25 ноября 2007 в 0:24

Error handling можно перевести как "работа с ошибками". В этой статье я немного расскажу о том, как это устроено в Друпале, и как этот процесс можно улучшить для облегчения отладки вашего кода.

PHP по умолчанию использует вывод ошибок прямо в окно браузера (я думаю, все видели это неприглядные, но крайне полезные сообщения вверху веб-страницы типа "notice: Use of undefined constant.. "), и предоставляет возможность разработчику переопределить функцию работы с ошибками. Можно, например, сохранять информацию об ошибках PHP интерпретатора в базу данных (как сделано в Друпале), слать емейлы админу, и т.д.

Переопределяется функция вот так:

set_error_handler('error_handler'); // error_handler() - функция, на которую PHP перекладывает error handling

У ошибок бывают разные уровни "тяжести" - 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

if ($errno & (E_ALL ^ E_NOTICE)) {

заменяем на

if ($errno & (E_ALL)) {

вуаля!
Друпал нас сразу радует нотисами:

notice: Undefined variable: no_module_preprocess in y:\home\d5\www\includes\common.inc on line 1493.
notice: Undefined variable: no_theme_preprocess in y:\home\d5\www\includes\common.inc on line 1493.

Можно конечно заняться фиксингом кода ядра, дабы избавить его от этой парочки предупреждений (благо это совсем несложно), но потом открываем другую страницу и какой-нибудь модуль типа Views радует нас еще десятком сообщений об ошибках. Становится понятно, что все не перефиксишь Smile Да и обновление модулей сразу сведет все наши старания на нет.

Что мы можем сделать? Включить нотисы только для нашего модуля.
Для этого доработаем функцию error_handler и определим дополнительную Друпаловскую переменную в settings.php:

В конец \sites\default\settings.php дописываем:

$conf = array();
$conf['debug_files'] = array(
  '/modules/views/views.module',
  '/includes/common.inc'
);

Так я включаю вывод нотисов только для двух файлов, '/modules/views/views.module' и '/includes/common.inc'

Теперь изменения в error_handler() (/includes/common.inc, строка 551) :
заменяем наше

if ($errno & (E_ALL)) {

на

//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)) {

Заметьте, что можно разрешить нотисы например для всей папки /modules прописав в конфиг '/modules/'

2. Выводим backtrace

UPD: что описано в этом разделе, можно получить установив модуль devel: http://drupal.org/project/devel
Внимание: все изменения, которые вы внесли в родной Друпаловский error_hander, во время включенного devel не будут иметь силы, контроль ошибок будет перехвачен внутренней функцией devel - backtrace_error_handler()

В процессе разработки довольно часто вылазят ошибки, которые по названию файла и номеру строки не выловишь (например: какой-то модуль делает неверный sql запрос, а друпал нам выводит ошибку:

"user warning: Unknown column 'n.ndid' in 'where clause' query: SELECT n.nid, n.vid, n.type, n.status, n.created, n.changed, n.comment, n.promote, n.sticky, r.timestamp AS revision_timestamp, r.title, r.body, r.teaser, r.log, r.format, u.uid, u.name, u.picture, u.data FROM node n INNER JOIN users u ON u.uid = n.uid INNER JOIN node_revisions r ON r.vid = n.vid WHERE n.ndid = 9 in y:\home\d5\www\includes\database.mysql.inc on line 172."

По этому сообщению мы лишь можем понять, что сама ошибка была в 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:

function error_handler($errno, $message, $filename, $line) {
  // 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);
  }
}

Комментарии

Аватар пользователя inc inc 25 ноября 2007 в 13:50

Осталось добавить, что если вам совсем не нужно сохранение ошибок PHP в базу данных, вы можете сделать сохранение ошибок в текстовый файл.
Для этого вместо
set_error_handler('error_handler');
надо вставить

error_reporting(E_ALL );
ini_set("display_errors", False);
ini_set("log_errors", True);
ini_set("error_log", "../outside/of/web/root/error_log.txt");
Аватар пользователя PVasili PVasili 26 ноября 2007 в 16:15

Тут многие(включая меня) с грубыми ошибками в словах пишут, а вы хотите от многонациональной тусовки прозрачности кода Wink

Аватар пользователя restyler restyler 26 ноября 2007 в 16:42

Тут многие(включая меня) с грубыми ошибками в словах пишут, а вы хотите от многонациональной тусовки прозрачности кода Wink

ну не знаю.. у меня вот тепло на душе когда пишу правильно Smile это касается не только php, но и стандартов html/css
Разве это нормально, применять конкатенацию к несуществующей переменной?
$a .= 'tratata'
по-моему нет..
по-моему однозначно правильнее сначала переменную определить Smile просто ради порядка в коде. Да ведь и несложно это, совсем!

Аватар пользователя restyler restyler 19 декабря 2007 в 23:02

Backtrace также умеет выводить модуль Devel (http://drupal.org/project/devel ) без правки кода ядра Drupal.

кстати да, вы правы.
Don't hack Drupal, use devel Smile
А для серьезных проектов все равно ничего лучше нормального ide с настроеным дебагом и брейкпоинтами не придумали.