Pull to refresh

Правильная обработка ошибок в PHP

Reading time 7 min
Views 32K

Что я понимаю под правильной обработкой:


  • Универсальное решение, которое можно вставить в любой существующий код;
  • Легко расширяемое решение;
  • В PHP аж три «механизма ошибок»: собственно ошибки (error), исключения (exception) и утверждения (assertion). Свести три механизма к одному — exception. В комментариях к предыдущей статье на эту тему выражалось мнение, что exception это плохой и/или сложный метод обработки ошибок. Я так не считаю и готов это обсудить в комментариях;
  • Опциональное логирование;
  • Общий обработчик exception, который будет поддерживать разные форматы вывода и debug/production режимы;
  • В debug режиме должен выводится trace. Требования к trace: компактный, понятный и по возможности ссылки на открытие файлов в IDE.


Универсальное решение


Для этого сделаем класс exceptionHandlerClass. В exceptionHandlerClass будут храниться настройки и статические методы — обработчики error, exception и assertion. Еще нам нужны методы setupHandlers и restoreHandlers. Первый метод настроит перехват ошибок. Error и assertion обработчики будут бросать ErrorException. Exception обработчик будет обрабатывать необработанные Exception и в зависимости от настроек выводить соответствующий ответ. restoreHandlers вернет все обработчики в изначальное состояние — это поможет при встраивании класса в код с существующим механизмом обработки ошибок. Подключение выглядит так:
  1. require 'exceptionHandler/exceptionHandlerClass.php';
  2. exceptionHandlerClass::setupHandlers();

включение debug режима (по умолчанию выключен) :
  1. exceptionHandlerClass::$debug = true;

Форматы вывода


Проще объяснить на примере: для вывода trace на веб странице я оберну его в таги pre и применю htmlspecialchars(), с другой стороны этот же trace при выводе в консоль будет не удобно читать, было бы проще, если бы это был plainText. Если нужно вывести ошибку как ответ SоapServer, то это должен быть правильно сформированный XML документ (SoapFault). Если скрипт выводит бинарные данные, например изображение, то удобней выводить ошибки через WildFire. Во всех этих ситуация нужно просто применить разные форматы вывода.
Для разных форматов будем создавать разные классы. Я для начала реализую два формата вывода exceptionHandlerOutputWeb(для веба) и exceptionHandlerOutputCli(для командной строки). Так же нам понадобиться класс фабрика (exceptionHandlerOutputFactory), в котором будет инкапсулирована логика, когда какой формат вывода применить.
  1. public function getExceptionHandlerOutput(){
  2.     if(php_sapi_name() == 'cli'){
  3.         return new exceptionHandlerOutputCli();
  4.     }
  5.     return new exceptionHandlerOutputWeb();
  6. }

При вызове setupHandlers можно установить формат вывода, передав экземпляр класса exceptionHandlerOutput* или exceptionHandlerOutputFactory*.
  1. exceptionHandlerClass::setupHandlers(new exceptionHandlerOutputAjax());

Благодаря такой архитектуре можно легко расширять форматы. Для создания нового формата достаточно создать класс, который будет наследоваться от абстрактного класса exceptionHandlerOutput и реализовать один метод (output).
  1. class exceptionHandlerOutputAjax extends exceptionHandlerOutput{
  2.     public function output($exception, $debug){
  3.         header('HTTP/1.0 500 Internal Server Error', true, 500);
  4.         header('Status: 500 Internal Server Error', true, 500);
  5.         $response = array(
  6.             'error' => true,
  7.             'message' => '',
  8.         );
  9.         if($debug){
  10.             $response['message'] = $exception->getMessage();
  11.         } else {
  12.             $response['message'] = self::$productionMessage;
  13.         }
  14.         exit(json_encode($response));
  15.     }
  16. }

Если нужна более сложная логика для автоматического выбора формата вывода, нужно создать класс, наследуемый от exceptionHandlerOutputFactory и реализовать метод getExceptionHandlerOutput.
  1. class exceptionHandlerOutputAjaxFactory extends exceptionHandlerOutputDefaultFactory{
  2.     public function getExceptionHandlerOutput() {
  3.         if( self::detect() ){
  4.             return new exceptionHandlerOutputAjax();
  5.         }
  6.         parent::getExceptionHandlerOutput();
  7.     }
  8.     public static function detect(){
  9.         return (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) 
  10.             && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest');
  11.     }
  12. }
  13. exceptionHandlerClass::setupHandlers(new exceptionHandlerOutputAjaxFactory());

Логирование


Как я и сказал выше логирование можно включать по желанию. Для этого в exceptionHandlerClass создан метод exceptionLog
  1. public static function exceptionLog($exception, $logPriority = null){
  2.     if(!is_null(self::$exceptionHandlerLog)){
  3.         self::$exceptionHandlerLog->log($exception, $logPriority);
  4.     }
  5. }

если нужно включить логирование, то достаточно сделать следующее:
  1. exceptionHandlerClass::$exceptionHandlerLog = new exceptionHandlerSimpleLog();

Класс для логирования должен наследоваться от абстрактного exceptionHandlerLog и реализовывать метод log
  1. class exceptionHandlerSimpleLog extends exceptionHandlerLog{
  2.     public function log($exception, $logType){
  3.         switch ($logType){
  4.             case self::uncaughtException:
  5.                 error_log($exception->getMessage());
  6.                 break;
  7.         }
  8.     }
  9. }

logType это одна из констант объявленных exceptionHandlerLog
  1. const uncaughtException = 0; //необработанные исключения
  2. const caughtException = 1; //вызов метода логирования вне обработчиков ошибок
  3. const ignoredError = 2; //ошибки маскированные @, логируются если выключена опция scream
  4. const lowPriorityError = 3; //ошибки которые не превращаются exception
  5. const assertion = 4; //assertion 

Имея logType и exception разработчик может сам решить какие искллючения и как логировать. Например, uncaughtException можно высылать по почте, ignoredError с severity E_ERROR логировать в файл итп.

Trace


При выводе trace я хочу видеть тип исключения, сообщение и собственно сам trace. В trace для каждого вызова должно выводится, какая функция вызвалась, список «кратких» параметров, файл и строка где произошел вызов. Что такое «краткие» параметры объясню на примерах: если функцию вызвали со строкой длиной в 1000 символов, то наличие этой строки в trace ничем не поможет при решении проблемы, а только затруднит чтение trace, это же касается массивов с большой вложенностью. Вывод trace на экран просто должен подсказать, где искать. Чтобы разобраться, что именно происходит нужно дебажить с помощью xdebug или примитивных var_dump() и die(), кому как больше нравится.
Пример trace:
	[ErrorException]: E_WARNING - mysql_connect(): Can't connect to MySQL server on 'localhost' (10061)
	#0: mysql_connect()
	    D:\projects1\d\index.php:19
	#1: testClass::test1("длиная строка…eeeeeery long string"(56))
	    D:\projects1\d\index.php:22
	#2: testClass->test2(testClass(), -∞, i:iTest, c:testClass, fa:testClass::test2)
	    D:\projects1\d\index.php:27
	#3: testAll(r:stream, fs:testClass::test1)
	    D:\projects1\d\index.php:30
Легенда
  • r: — resource
  • fs: — function (callable string)
  • fa: — function (callable array)
  • i: — interface (string)
  • c: — class (string)
  • ∞/INF — infinity
  • testClass() — object of type testClass
  • ""(n) — string, в скобках указана длина, … — место где вырезана часть строки
  • array(n) — array, в скобках указана длина

И самое полезное… ссылки на открытие файлов в IDE прямо из trace.

При нажатии на ссылку в IDE откроется соответствующий файл на соответствующей строке.
Для консольного режима (консоль NetBeans):
NetBeans Console
  1. exceptionHandlerOutputCli::setFileLinkFormat(': in %f on line %l');
Для веб режима (TextMate):
  1. exceptionHandlerOutputWeb::setFileLinkFormat('txmt://open/?file://%f&line=%l');

Можно реализовать для NetbBeans (или другого IDE). Для этого нужно: зарегистрировать протокол; сделать обработчик этого протокола (самое простое — bat файл). В обработчике через командную строку вызвать NetBeans с соответствующим файлом и строкой. Но это тема для следующей статьи.
Код писался за два дня так, что возможны мелкие недочеты. Скачать (не было времени, чтобы выложить в репозиторий).

UPD: перенесено в блог PHP
UPD2: в продолжение темы работа с исключениями в PHP
Tags:
Hubs:
+63
Comments 102
Comments Comments 102

Articles