4
2011
Про ошибки и исключения в PHP
Данная статья ни в коем случае не претендует на пересказ мануала, а содержит некие размышления на тему генерации эрроров и использования исключений в PHP5: 1. Errors & exceptions. В чем отличия и что когда использовать |
I. Самое первое и пожалуй самое важное. В чем отличие эрроров от исключений.
В принципе и то и другое появляется в результате некой нестандарной операции в ходе работы скрипта. Эрроры можно разделить на два типа:
1). пользовательские E_USER_NOTICE, E_USER_WARNING, E_USER_ERROR (то есть которые генерятся в ходе работы скрипта при вызове функции trigger_error())
2). и все остальные — E_NOTICE, E_WARNING, E_ERROR, E_PARSE, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING и пр. (которые появляются при нестандартной работе библиотечных функций, при неверном использовании лексем языка, при некорректной настройке интерпритатора PHP ну или же в результате банальных косяков программистов в духе «забыл закрывающую скобку»).
Исключения же подобно эррорам пользовательского типа генерятся в ходе работы скрипта при помощи инструкции:
throw new Exception('Exception text bla-bla-bla');
и перехватываются в блоках:
try { //... } catch (Exception $e) { //... }
Стоит различать в каком случае использовать генерацию пользовательских ошибок, а в каком — выброс Exceptions. Генерация пользовательских ошибок имеет смысл в случае, когда к примеру пишется некая функция (метод) и подразумевается, что программист будет использовать данную функцию неверно. Примеры: некорректные аргументы функции, ошибка при выполнении SQL запроса внутри функции, отсутствие прав доступа к необходимому файлу. Тогда программиста следует уведомить об этом и бросается E_USER_NOTICE, E_USER_WARNING или же E_USER_ERROR. Вот например:
1 2 3 4 5 | function showNews() { if (!file_exists('/srv/www/site/news.txt')) { trigger_error('News file not found', E_USER_ERROR); } } |
То есть иными словами генерация ошибок важна именно для отслеживания правильности функционирования скрипта. Исключения же следует использовать в первую очередь в тех случаях, когда вероятность нестандарной работы функции (метода) зависит от факторов, неподвластных программисту (например от нестандаратного пользовательского ввода), и пользователю следует выдать более менее корректную информацию при возникновении исключительной ситуации.
1 2 3 4 5 6 7 8 9 10 | try { $controller = findController(); if ($controller) { $controller->run(); } else { throw new Exception("page http://" . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] . " not found"); } } catch (Exception $e) { show404page($e->getMessage()); } |
В данном примере в зависимости от URL скрипт пытается найти контроллер, а если ничего не найдено, то генерится страница для отображения 404 ошибки с неким сообщением.
То есть в общем случае генерация error / notice / warning — это всегда ошибка (бага), которая должна быть исправлена и которая не должна повторяться в принципе. Это, так сказать, уведомление для программиста. Исключение же (exception) генерится далеко не всегда в результате ошибки, и может появляться при абсолютно правильной работе приложения, но при нестандратных пользоваетльских данных (как в примере выше — нестандартный URL).
Так же стоит отметить что функциональность исключений стоит использовать при написании библиотек, которые могут быть заюзаны в различных системах и на различных сайтах. Например, мы пишем некий ORM для работы с DB — набор классов с кучей различных методов. Для обобщения пусть у нас будет некий класс Table и у него будет метод query() для выполнения произвольного SQL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | class Table { protected $_name; public function __construct($name) { $this->_name = $name; } protected function _execute($sql) { // какой-то код... } protected function _createSql($data) { // какой-то код для формирования $sql из парметров $data return $sql; } public function query($data) { // на основании параметров $data формируем $sql $sql = $this->_createSql($data); // и выполняем его $result = $this->_execute($sql); if ($result) { return $result; } else { throw new Exception('Error in SQL query: ' . $sql); } } } |
Заместо того, чтобы бросать E_USER_ERROR при некорректном запросе, стоит внутри метода query() генерить Exception. Тогда программист, работающий с данной библиотекой сможет использовать простую конструкцию следующего вида, не вдаваясь в детали реализации библиотеки:
1 2 3 4 5 6 | try { $table = new Table('something'); $table->query($data); } catch (Exception $e) { echo '404'; } |
II. Вообще говоря пользователь сайта ни при каких обстоятельствах не должен знать, что произошла какая-то ошибка. Так как чисто белая страница в ответ на некоторые запросы может ввести пользователей в ступор и сильно подпортить рейтинг веб-проекта в глазах людей. Поэтому даже если генерится эррор, нужно так или иначе отображать более-менее приличную страницу с более-менее приличным сообщением в духе «Извини, в данный момент страница недоступна, попробуй позже». Ошибки вида E_USER_ERROR перехватываются легко при помощи функции set_error_handler(), а вот чтобы перехватывать Fatal Error-ы (E_ERROR) нужно немного изловчиться.
Например вот так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | ini_set('display_errors', 0); register_shutdown_function('shutdownHandler'); function show404page() { //.. } function logError($message, $file, $line) { //.. } function shutdownHandler() { $someError = error_get_last(); if ($someError['type'] === E_ERROR) { logError($someError['message'], $someError['file'], $someError['line']); show404page(); } } |
или же так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | ini_set('display_errors', 0); function show404page() { //.. } function logError($message, $file, $line) { //.. } function buffer_handler($buffer) { $someError = error_get_last(); if ($someError['type'] === E_ERROR) { logError($someError['message'], $someError['file'], $someError['line']); return show404page(); } else { return $buffer; } } ob_start('buffer_handler'); // код, который потенциально может привести к Fatal Error-у // ... ob_end_flush(); |
III. Многоуровневая обработка исключений и разматывание стека вызовов методов. При помощи механизма try / catch / exceptions можно построить приложение так, что в случае генерации эксепшена в неком методе, закопанном глубоко в недрах кода, можно воспроизвести стек вызовов методов (которые привели к данной ситуации) с подробным описанием возникшего исключения.
Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | class A { protected $_b; public function __construct() { $this->_b = new B(); } public function run() { $this->_b->doSomething(); } } class B { protected $_c; public function __construct() { $this->_c = new C(); } public function doSomething() { $this->_c->doException(); } } class C { public function doException() { throw new Exception('Error in method ' . __METHOD__ . ' !'); } } try { $a = new A(); $a->run(); } catch (Exception $e) { echo $e; } |
тогда при выполнении данного кода мы получим сообщение на подобии:
exception 'Exception' with message 'Error in method C::doException !' in /srv/www/localhost/web/index.php:40
Stack trace:
#0 /srv/www/localhost/web/index.php(32): C->doException()
#1 /srv/www/localhost/web/index.php(17): B->doSomething()
#2 /srv/www/localhost/web/index.php(46): A->run()
#3 {main}
Как видно, при вызове метода A::run() генерится Exception, «закопанный» глубоко внутри методов связных классов B и C. При возникновении исключения мы движемся по стеку вызова методов, в поисках блока перехватчика try { } catch { }, а при нахождении такового в блоке catch { } можем корректно обработать исключение и даже, при желании, бросить ещё один Exception (такая ситуация может возникнуть в случае если мы хотим вызвать кучу методов со сложной логикой — например для сохранения сообщения в БД).
Вот например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class NotFoundException extends Exception { // ... } class CommonException extends Exception { // ... } try { try { $controller = findController(); $controller->run(); } catch (NotFoundException $e) { show404page($e->getMessage()); } } catch (CommonException $e) { logException($e); header('Location: http://' . $_SERVER['HTTP_HOST'] . '/'); die(); } |
тут задаются два типа пользовательских исключений: NotFoundException и CommonException, которые наследуют стандартный класс Exception (кстати говоря, можно прееопределять стандартные методы класса Exception). И как видно из кода исключение типа CommonException может быть брошено как при вызове функций findController() и run(), так и при вызове функции show404page() (уже после выброса NotFoundException). То есть за одно обращение к скрипту Exceptions-ы могут бросаться два раза.
Пн | Вт | Ср | Чт | Пт | Сб | Вс |
---|---|---|---|---|---|---|
« Сен | ||||||
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 |
Метки
Рубрики
- Apache (1)
- Highload (4)
- JavaScript (1)
- Linux (3)
- MongoDB (1)
- MySQL (1)
- Perl (1)
- PHP (5)
- Python (5)
- Web-разработка (5)
- Алгоритмы (1)
- За жизнь (2)
- Конференции (6)
Исключения важная штука и способов её применения масса, к примеру можно сделать глобальную обработку исключений какого-нибудь фреймворка:
try
{
… // Выполняются контроллеры и представления
} catch (NotFoundException $exc) {
…
} catch (NeedAuthException $exc) {
…
} catch (DBException $exc) {
…
}
В определённых блоках исключений выводятся припасенные вьюшки (например, страница с 404 ошибкой, если есть ЧПУ) и делаются записи в логи. Мне показалось это очень удобным.
Ага! Я так и делал недавно, когда писал один framework. В диспетчере, который обрабатывал маршрутизацию запросов, было нечто в духе:
То есть специльный тип Router_Exception использовался для Исключений в случае, когда надо отображать 404 страницу, и стандартный тип Exception — во всех остальных случаях.