Обработка ошибок в PHP и исключения

Итак. Работа над RedBox ведется полным ходом. Сегодня расскажу про обработку ошибок.

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

Почему? С одной стороны, пользователя, зашедшего на сайт не интересует по какой причине он не работает. Ошибка MySQL, или PHP, или другая? Ему по барабану. Он пришел за информацией, а ее нет. Важнее подсказать ему, что делать («Загляните через пару минут», «Воспользуйтесь зеркалом нашего сайта по адресу…»). С другой – ни к чему светить информацию, которую он видеть не должен (тексты запросов, document root и пр.).

Разделение вывода

Начнем со второй проблемы. Ее я решил очень просто: в настройках сайта, имеется специальная константа debug_mode. Если она == 1 – выводим всю инфу в браузер, в остальных случаях – показываем пользователю текст («Упс, у нас ошибочка» или подобное). Еще у меня реализована отсылка сообщений на почту в случае возникновения некоторых ошибок.

Помимо этого, если сайт в режиме отладки, в публичной части выводится заметное сообщение об этом. Поэтому залить на хостинг и забыть выключить «отладку» не получится.

Ошибки или исключения?

До появления 5-ой версии PHP все ошибки приходилось обрабатывать разработчику. Каждый выходил из этой ситуации по-разному. Кто-то просто писал:


die('Ашыпка!!!');

Более продвинутые товарищи писали что-то наподобие этого:


echo 'Ашыпка!!!';
return false;

Самые умные –

trigger_error('Ашыпка!!!', E_USER_ERROR);

Но, согласитесь, это все костыли. В первом случае ошибку никак не обработать, во втором – нужно на каждом уровне вложенности стека вызова проверять что вернула функция. Третий вариант – наиболее близок к идеалу.

С появлением механизма исключений все сильно упростилось:


try {
// Код
if (/* какая-то проверка */) {
throw new Exception('Ашыпка!!!', 0)
}
}
catch (Exception $e) {
// обрабатываем
// например, просто выводим
echo $e // тут работает "магический" метод __toString класса Exception
}

Причем можно определять классы-потомки от Exception и строить обработку ошибок на основе этих классов:


class SeriosErrorException extends Exception;
class SomeTinyErrorException extends Exception;
try {
// Код
if (/* какая-то проверка */) {
throw new SeriosErrorException('Сурьезная ашыпка!!!', 0)
}

if (/* еще какая-то проверка */) {
throw new SomeTinyErrorException('Так себе ошибочка!!!', 0)
}
}
catch (SeriosErrorException $e) {
// обрабатываем
}
catch (SomeTinyErrorException $e) {
// обрабатываем
}

Т.е. первый блок catch ловит только ошибки класса SeriosErrorException, все остальные «спускаются ниже».

Вывод пользовательских ошибок

Пару слов о том, как выводятся ошибки в RedBox CMS.

Для таких ошибок создан класс UserException примерно следующего содержания:


class UserException extends Exception {
private $message_array = array(
// коды ошибок
);
private $info;

public function __construct($code, $info = NULL) {
$this->info = $info;
parent::__construct($this->message_array[$code], $code);
}

public function getText() {
// Вывод ошибки в виде текста
}

public function getHtml() {
// Вывод ошибки в виде HTML
}
}

Например ошибка запроса к БД кидается следующим образом:


throw new UserException(1202, array('mysql' => $this->_query, 'text' => mysql_error()));

В режиме отладки это отображается так:

(для подсветки я использую GeSHi)

Вывод стандартных ошибок

По-умолчанию, PHP не очень разговорчив и выдает такие сообщения:


Warning: Wrong parameter count for strpos() in /usr/home/knave11t/ab.ru/WWW/inc/classes/News.class.php on line 52

Я предлагаю «переделывать» все ошибки в исключения с помощью функции set_error_handler. А дальше ловить эти исключения и обрабатывать их.

Для таких целей я использую класс, разработанный Дмитрием Котеровым.

5 комментариев on "Обработка ошибок в PHP и исключения"

  1. ивзращенец :))

  2. Ну-ка, ну-ка, покажи как ты их обрабатываешь. :)

  3. Роман:

    А откуда стек вызова брать?

  4. Роман:

    Нашел откуда брать, но как построить строку кода, как в твоем примере? Можно из массива — до некоторых пор я так и делал, но тогда неверно отображаются параметры:
    test(string so long…);
    Неплохо было бы чтобы строки заключались в кавычки…
    test(‘string so long…’);
    Но это возможно только в том случае, если брать стек из getTraceAsStrng – но как его разбирать на части не пойму…

  5. Вот как у меня выглядит функция вывода исключения:


    public function getHtml() {
    $html = '

    RedBox — ошибка

    ';
    $html .= '

    ' . $this->message . '

    ';

    $html .= '

    Строка ' . $this->line . ' файла ' . $this->getFile() . '

    ';

    if (!empty($this->info) && is_array($this->info)) {
    $html .= '

    Дополнительная информация

    ';
    $html .= '

      ';

      foreach ($this->info as $lang => $text) {
      $geshi = new GeSHi($text, $lang);
      $geshi->set_header_type(GESHI_HEADER_DIV);
      $geshi->enable_classes(true);
      $geshi->set_overall_class('code-block');

      $html .= '

    • ' . $lang . $geshi->parse_code() . '
    • ';
      }

      $html .= '

    ';
    }

    $stack = $this->getTrace();
    if (is_array($stack) && count($stack) > 0) {
    $html .= '

    Стек вызова

    ';
    $html .= '

      ';

      foreach ($stack as $stackLine) {
      $html .= '

    • ';

      $html .= 'Строка ' . $stackLine['line'] . ' файла ' . $stackLine['file'];

      $code = '';

      if(!empty($stackLine['class'])) {
      $code .= $stackLine['class'];
      }
      if(!empty($stackLine['type'])) {
      $code .= $stackLine['type'];
      }
      if(!empty($stackLine['function'])) {
      $code .= $stackLine['function'];
      }

      $args = array();
      foreach ($stackLine['args'] as $arg) {
      if (is_string($arg))
      $args[] = '"' . $arg . '"';
      elseif (is_array($arg))
      $args[] = print_r($arg, true);
      elseif (is_object($arg))
      $args[] = print_r($arg, true);
      elseif (is_bool($arg))
      $args[] = ($arg === true ? 'true' : 'false');
      elseif (is_null($arg))
      $args[] = 'null';
      else
      $args[] = $arg;
      }
      $code .= '(' . implode(', ', $args) . ');';

      $geshi = new GeSHi($code, 'php');
      $geshi->set_header_type(GESHI_HEADER_DIV);
      $geshi->enable_line_numbers(GESHI_NORMAL_LINE_NUMBERS);
      $geshi->set_tab_width(4);
      $geshi->enable_classes(true);
      $geshi->set_overall_class('code-block');
      $geshi->start_line_numbers_at($stackLine['line']);

      $html .= $geshi->parse_code();

      $html .= '

    • ';
      }

      $html .= '

    ';
    }

    return $html;
    }

Got something to say? Go for it!