Как стать автором
Обновить

Правильная работа с исключениями в PHP

PHP *
В предыдущей статье я предложил свести все «механизмы ошибок» к исключениям, поэтому логично будет объяснить, как правильно работать с исключениями в PHP.
Сначала поясню, почему я выбрал именно исключения, как механизм работы с ошибками:
  1. Исключения — это гибкий, расширяемый метод обработки ошибок;
  2. Это стандартизованный механизм – человеку, не работавшему с вашим кодом, не нужно будет читать мануал, чтобы понять, как обрабатывать ошибки. Ему достаточно знать, как работают исключения;
  3. С исключениями гораздо проще находить источник ошибок, так как всегда есть стек вызовов (trace).

Сразу скажу, что в этой статье я не открываю Америку. Описаны стандартные принципы работы с исключениями плюс некоторые особенности, налагаемые PHP. Полезно будет почитать новичкам, хотя может быть и опытные разработчики найдут что-нибудь новое для себя.

1. Никогда не бросайте абстрактное исключение (т.е. просто Exception). Объявите хотя бы один класс исключений специально для вашего приложения (модуля, библиотеки)
class baseException extends Exception{}

и замените все строки в своем коде
throw new Exception();

на
throw new baseException();

Таким образом, все исключения вашего кода можно будет отличить от исключений не вашего кода.


2. Исключения должны быть иерархичны. У вас должен быть базовый класс исключений, от которого наследуются все исключения, бросаемые в вашем коде. Например, у вас в коде есть модуль для работы с файлами fileModule, объявите исключение, которое будет бросаться только этим модулем
class fileModuleException extends baseException{}

Если вам нужна еще бОльшая различимость ошибок, например, среди всех ошибок, связанных с работой с файлами, вы хотите различать ситуацию, когда файл не найден, то нужно объявить еще одно исключение
class fileNotFoundException extends fileModuleException{}
 

Соблюдая иерархичность, вы сможете различать исключения от разных модулей в вашем приложении. Я не призываю наплодить кучу исключений, для каждого модуля. Исключения должны проектироваться не от кода, а от ситуаций, которые вы хотите по-особенному обработать.
И обратная ситуация, не скупитесь сделать разные исключения, если того требует обстоятельства
try{
    //...
}catch(fileModuleException $e){
    switch($e->getCode()){//так делать не надо
        case 1: echo 'file not found';
        case 2: echo 'file not readable';
        //...
    }
}
 

Чтобы такие ситуации в принципе не были возможны, можно «заглушить» code в базовом классе
function __construct($message = '', $code = 0) {
    parent::__construct($message, 0);
}


3. Не обрабатывайте исключения, если в данном контексте не понятно, как его обработать. Например, если вы следуете паттерну MVC, то в методе модели может быть не понятно, как обработать ошибку — как ее вывести, потому как за логику отвечает control, а за вывод view. Если не понятно, что делать с исключением, то «пробросьте» его дальше.
try{
    $db->begin();
    //...
    $db->commit();
}catch(Exception $e){
    $db->rollback();
    throw $e;
}

От метода, который пробрасывает исключения, можно ожидать любых исключений. Можно сузить количество исключений, бросаемых методом, преобразовав исключение:
try{
    //...
}catch(Exception $e){
    throw new baseException($message, 0, $e);//не разрывайте цепь
}

Тут очень важный момент — не разрывать цепь исключений. Третьим параметром передается изначальное исключение. Этот код нативно работает в 5.3 и с доработкой в 5.2. При таком подходе стек вызовов будет «цельным» от самого первого броска исключения.

4. У вас должен быть глобальный обработчик исключений. Это может быть или try...catch на самом верхнем уровне или ExceptionHandler. Все исключения, которые добрались до глобального обработчика, считаются критическими, так как не были правильно обработаны ранее. Их надо залогировать.

5. Исключение это объект, соответственно его можно расширять под свои потребности. Допустим у вас многоязычное приложение и текст ошибки в бросаемом исключении нужно выводить пользователю. Соответственно это сообщение нужно переводить. Это не сложно, если сообщение без переменных частей, например, «Ошибка при выполнении операции». Но что делать, если в сообщение входят переменные части, например, «У вас недостаточно денег на балансе (1000). Нужно 2000». Тогда можно отдельно передать шаблон текста ошибки и отдельно сами переменные. Пример кода Старый пример кода.


6. Преобразуйте все ошибки утверждений (assertion fail) и не фатальные ошибки в исключения (см. мою предыдущую статью)

7. Никогда не глушите исключения без какой либо обработки
try {
    //...
} catch (Exception $e) {
    //ничего делаем
}
 

потому, что в противном случае ошибку из-за таких действий будет очень сложно найти. Нужно хотя бы логировать:
try {
    //...
} catch (Exception $e) {
    exceptionHandlerClass::exceptionLog($e);
}


8. Документируйте исключения. Указывайте в докблоке, какие исключения выбрасывает метод (таг @throws, можно указывать больше одного). Это упростит всем жизнь.

Вот в принципе и все, что нужно знать про исключения. Еще один интересный факт напоследок — исключения можно ловить по интерфейсу:
interface iException{}
class customException extends baseException implements iException{}
try{
    //...
}catch(iException $e){
    //...
}


UPD исправлены замечания в комментариях:1, 2 и 3 (спасибо всем, кто поучаствовал в обсуждении).
Отдельное спасибо, хабраюзеру ckopobapkuh за активное участие
Теги:
Хабы:
Всего голосов 75: ↑64 и ↓11 +53
Просмотры 62K
Комментарии Комментарии 137