История одного класса для обработки ошибок.

    Предисловие (Смело можно пропускать)



    Приятного времени суток.

    У меня возникло желанием поделиться с вами одной историей. Историей возникновения класса обработки ошибок. Почему просто не привести класс “as is” и не позволить вам судить о его достоинствах и недостатках? Причина проста. В этом случае не возможно будет понять, почему он стал именно таким, каким является. Невозможно будет обнаружить ошибки в логике его создания. Невозможно понять схожа ли у вас ситуация с моей и возможно ли для вас его использование или, возможно, лучше обратить свое внимание на другой класс обработки ошибок. Благо, таковых вы найдете множество.

    А с какого такого перепугу я вообще занялся сочинением собственного класса обработки ошибок? Ведь есть масса наработок в этой области. Наработок серьезных, очень неглупых людей. Причина проста. К созданию этих классов привели разные причины, создание этих классов преследовало разные цели. И, как всем нам хорошо известно, велосипедов мало не бывает и на вкус они все разные.

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

    Первые наброски



    Сказано – сделано. Классы были написаны. И, что вполне естественно, все они должны были как то работать с ошибками. Вот какие требования я предъявил к такой обработке:
    • Ошибки не должны выкидываться в поток. Классы используются в разных проектах и, часто в скриптах, к которым обращаются используя AJAX. Да и вообще, классы никак не должны соприкасаться с логикой представления.
    • Ошибке может соответствовать описательный текст.
    • После выполнения метода должны быть возможность определить — возникла ли во время его работы ошибка и каков ее описательный текст.
    • Проверка наличия ошибки не должна опираться на возвращаемое методом значение, ведь иногда невозможно вернуть false/true, надо возвращать данные.
    Итак, все относительно просто. Что мы добавляем в класс для реализации работы с ошибками:
    • Флаг err, который будет содержать false, если ошибки нет и сообщение, присвоенное ошибке в противном случае. Флаг не должен быть напрямую доступен вне класса.
    • Должен быть метод, очищающий флаг ошибки. Если ошибка возникла, но она не критична и ее возможность предусмотрена логикой программы, то ошибка должны сбрасываться при следующем вызове любого метода класса, что бы ее состояние всегда соответствовала наличию или отсутствию ошибок при последнем вызове метода класса.
    • Должен быть метод, осуществляющий взвод флага ошибки.
    • Должен быть метод, проверяющий была ли ошибка и возвращающий false в случае ее отсутствия и сообщение, присвоенное ошибке в противном случае.
    Copy Source | Copy HTML
    1. class test_class{
    2.     private $err;
    3.  
    4.     public function set_err($msg=''){
    5.         $this->err=$msg;
    6.     }
    7.  
    8.     public function clear_err(){
    9.         $this->err=false;
    10.     }
    11.  
    12.     public function is_err(){
    13.         return $this->err;
    14.     }
    15.  
    16.     public function some_method(){
    17.         $this->clear_err();
    18.         if(some_err){
    19.             $this->set_err($msg='Some error appear.');
    20.             return false;
    21.         }
    22.         return true;
    23.     }
    24.  
    25.     public function complex_method(){
    26.         $this->clear_err();
    27.         $this->some_method();
    28.         if($this->is_err()!==false){
    29.             return false;
    30.         }
    31.         return true;
    32.     }
    33. }
    34.  
    35. define('some_err',true);
    36. $test_instance = new test_class;
    37. $test_instance->complex_method();
    38. if($test_instance->is_err()!==false){
    39.     die($test_instance->is_err());
    40. }
    41. die('No error appear.');


    Выделение функционала в отдельный класс



    Ну вот. Все работает. Вот только есть один неприятный момент. Этот код повторяется в каждом классе. Пока класс был один, все было нормально. Но как только количество классов стало рости это стало, как минимум, неудобным. Так что код был выделен в отдельный класс.
    Copy Source | Copy HTML
    1. class error_class{
    2.     private $err=false;
    3.  
    4.     public function set_err($msg=''){
    5.         $this->err=$msg;
    6.     }
    7.  
    8.     public function clear_err(){
    9.         $this->err=false;
    10.     }
    11.  
    12.     public function is_err(){
    13.         return $this->err;
    14.     }
    15. }
    16. class test_class{
    17.     public $err;
    18.  
    19.     public function __construct(){
    20.         $this->err = new error_class();
    21.     }
    22.  
    23.     public function some_method(){
    24.         $this->err->clear_err();
    25.         if(some_err){
    26.             $this->err->set_err($msg='Some error appear.');
    27.             return false;
    28.         }
    29.         return true;
    30.     }
    31.  
    32.     public function complex_method(){
    33.         $this->err->clear_err();
    34.         $this->some_method();
    35.         if($this->err->is_err()!==false){
    36.             return false;
    37.         }
    38.         return true;
    39.     }
    40. }
    41.  
    42. define('some_err',true);
    43. $test_instance = new test_class;
    44. $test_instance->complex_method();
    45. if($test_instance->err->is_err()!==false){
    46.     die($test_instance->err->is_err());
    47. }
    48. die('No error appear.');
    49.  
    50.  


    Обработка критических ошибок, с помощью исключений.



    Ну вот. Уже лучше. Можно применять его с любым классом, добавив его единожды в начале файла класса, который его использует.
    Copy Source | Copy HTML
    1. require_once("error_class.php");


    Вот только информацию о наличии ошибок он передает, а обработкой их не занимается. Создавать дополнительный класс для перехвата и направления ошибок обработчику было бы довольно странно, так что давайте немножко расширим его функционал:
    • Упростим обработку критических ошибок. То есть, таких ошибок, которые сразу должны вызывать остановку работы скрипта и передавать ошибку обработчику.
    • Реализуем возможность назначать пользовательский обработчик ошибок. Такой обработчик невозможно унифицировать и, соответственно, он должен позволять подключать пользовательскую функцию.


    Для обработки критических ошибок используем исключения.
    set_exception_handler — определяет пользовательский обработчик для исключений.
    restore_exception_handler — восстанавливает предыдущий обработчик для исключений.
    Exception — базовый класс для всех исключений.

    Все элементарно.
    • В конструктор класса мы добавляем дополнительный аргумент $callback, который содержит callback функцию.
    • Добавляем переменную $exception_handler — это флаг, который указывает на то, что был назначен пользовательский обработчик исключений. Он нужен, что бы в дестракторе определить — нужно ли восстанавливать предшествующий обработчик исключений.
    • Добавляем в класс метод fire_err, с помощью которого можно будет вызывать исключения, в случае критической ошибки.
    • Создаем деструктор класса, который будет в случае необходимости восстанавливать обработчик исключений в его предыдущее состояние.


    И наш код преобретает следующий вид:
    Copy Source | Copy HTML
    1. class error_class{
    2.  
    3.     private $err=false;
    4.     private $exception_handler=false;
    5.  
    6.     public function __construct($callback=null){
    7.         if($callback!=null){
    8.             $this->exception_handler=true;
    9.             set_exception_handler($callback);
    10.         }
    11.     }
    12.  
    13.     public function set_err($msg=''){
    14.         $this->err=$msg;
    15.     }
    16.  
    17.     public function clear_err(){
    18.         $this->err=false;
    19.     }
    20.  
    21.     public function is_err(){
    22.         return $this->err;
    23.     }
    24.  
    25.     public function fire_err($msg='',$code=0){
    26.         $this->err=$msg;
    27.         throw new Exception($msg,$code);
    28.     }
    29.  
    30.     public function __destruct(){
    31.         if($this->exception_handler){
    32.             restore_exception_handler();
    33.         }
    34.     }
    35. }

    Использование класса теперь будет выглядеть следующим образом:
    Copy Source | Copy HTML
    1.  
    2. define('some_err',true);
    3. $err = new error_class("err_overwork");
    4. $test_instance = new test_class;
    5. $test_instance->complex_method();
    6. if($test_instance->err->is_err()!==false){
    7.     $err->fire_err($test_instance->err->is_err(),-1);
    8. }
    9.  
    10. function err_overwork($error){
    11.     die("Ошибка: ".$error->getMessage()."<br/>Файл: ".$error->getFile()."<br/>Строка: ".$error->getLine());
    12. }


    Обработка ошибок, генерируемых интерпретатором.



    Ну вот. Теперь осталось добавить только одно. Возможность (именно возможность) передавать пользовательской функции обработки ошибок и ошибки генерируемые интерпретатором (исключая, конечно, ошибки синтаксиса). Таким образом удастся добиться единообразия отображения ошибок. Безусловно, необходимость этого крайне спорная. Использовать или нет — решать вам. Мне кажется что это иногда может быть уместно.

    set_error_handler — позволит передать обработку ошибок пользовательскому обработчику.
    restore_error_handler — восстанавливает предидущий обработчик ошибок.
    ErrorException — Класс исключения для представления ошибки.

    В принципе ничего сложного нет. С помощью set_error_handler устанавливаем обработчик и все готово. В качестве дополнительных аргументов в конструкторе принимаем:

    Но есть нюанс. В случае сгенерированой нами ошибки обработчик получает обект Exception. А вот если ошибка была сгенерирована интерпретатором, он получает набор аргументов. Естественно, уместно сделать так, что бы вне зависимости от источника обработчик получал однородные данные. Специально для этого был разработан объект ErrorException. Итак, добавляем в класс обработки ошибок функцию redirect_err, которая будет обрабатывать ошибку, а именно — вызывать исключение, которое уже будет перехватываться обработчиком, назначенным пользователем. Кроме того добавляем флаг, который будет указывать на то, что перехватываются ошибки, генерируемые интерпретатором и нужно для того что бы в дестракторе класса восстанавливать, в случае необходимости, предыдущий обработчик с помощью функции restore_error_handler.
    И теперь наш класс принимает законченный и готовый к использованию вид:
    Copy Source | Copy HTML
    1. class error_class{
    2.  
    3.     private $err=false;
    4.     private $exception_handler=false;
    5.     private $err_handler=false;
    6.  
    7.     public function __construct($callback=null,$err_redirect=true,$level=E_ALL){
    8.         if($callback!=null){
    9.             $this->exception_handler=true;
    10.             set_exception_handler($callback);
    11.             if($err_redirect){
    12.                 $this->err_handler = true;
    13.                 set_error_handler(array("error_class","redirect_err"),$level);
    14.             }
    15.         }
    16.     }
    17.  
    18.     public function redirect_err($errno, $errstr, $errfile, $errline, $errcontext){
    19.         throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
    20.         return true;
    21.     }
    22.  
    23.     public function set_err($msg=''){
    24.         $this->err=$msg;
    25.     }
    26.  
    27.     public function clear_err(){
    28.         $this->err=false;
    29.     }
    30.  
    31.     public function is_err(){
    32.         return $this->err;
    33.     }
    34.  
    35.     public function fire_err($msg='',$code=0){
    36.         $this->err=$msg;
    37.         throw new Exception($msg,$code);
    38.     }
    39.  
    40.     public function __destruct(){
    41.         if($this->exception_handler){
    42.             restore_exception_handler();
    43.         }
    44.         if($this->err_redirect){
    45.             restore_error_handler();
    46.         }
    47.     }
    48. }

    А вот реально существующий пример кода, с его использованием:
    Copy Source | Copy HTML
    1. <?php
    2.     session_start();
    3.     require_once("error_class.php");
    4.     require_once("mail_class.php");
    5.  
    6.     $err = new error_class("err_overwork");
    7.  
    8.     if(
    9.        !isset($_POST['email'])||
    10.        !isset($_POST['message'])||
    11.        !isset($_POST['captcha'])
    12.        ){
    13.         $err->fire_err("Не все переменные заданы",-1);
    14.     }
    15.     if($_SESSION['captcha']!=$_POST['captcha']){
    16.         $err->fire_err("Неверно введен код обратного теста Тьюринга",-1);
    17.     }else{
    18.         unset($_SESSION['captcha']);
    19.     }
    20.  
    21.     $mailer = new mail_class();
    22.  
    23.     if(!$mailer->text(addslashes(htmlspecialchars($_POST['message'])))){
    24.         $err->fire_err($mailer->err->is_err(),-1);
    25.     }
    26.     if(!$mailer->to("Студия «Ночной народ»","raven@nightfolk.net")){
    27.         $err->fire_err($mailer->err->is_err(),-1);
    28.     }
    29.     if(!$mailer->from(addslashes($_POST['email']))){
    30.         $err->fire_err($mailer->err->is_err(),-1);
    31.     }
    32.     if(!$mailer->subj('Сообщение от посетителя')){
    33.         $err->fire_err($mailer->err->is_err(),-1);
    34.     }
    35.     if(!$mailer->reply_to(addslashes($_POST['email']))){
    36.         $err->fire_err($mailer->err->is_err(),-1);
    37.     }
    38.     if(!$mailer->send()){
    39.         $err->fire_err($mailer->err->is_err(),-1);
    40.     }
    41.  
    42.     header("location:../confirm.html");
    43.     die();
    44.  
    45.     function err_overwork($error){
    46.         $_SESSION['error_message']=$error->getMessage();
    47.         header("location:../error.php");
    48.         die();
    49.     }
    50. ?>

    Просветление (ВАЖНО)


    После бурного и продуктивного общения с массой сильно разгневанных людей,
    Чтения статьи "Обработка исключительных ситуаций в ООП"
    Троекратного перечитывания документации по механизму исключений в php
    И, наконец, обсуждения проблемы на форуме phpclub.ru.
    Я понял, что исключения — вполне достаточный механизм для реализации обработки ошибок и не имеет смысл приплетать что то еще.
    Однако, что бы не дублировать код назначения обработчика исключений по умолчанию и обработчика сообщений об ошибках, генерируемых интерпретатором, в каждой точке входа, имеет смысл существование класса в следующем виде:
    Copy Source | Copy HTML
    1. class error_class{
    2.  
    3.     private $err_handler=false;
    4.  
    5.     public function __construct($callback=null,$err_redirect=true,$level=E_ALL){
    6.         if($callback==null){
    7.             throw new Exception("Не определена callback функция.",E_ERROR);
    8.         }
    9.         set_exception_handler($callback);
    10.         if($err_redirect){
    11.             $this->err_handler = true;
    12.             set_error_handler(array("error_class","redirect_err"),$level);
    13.         }
    14.     }
    15.  
    16.     public function redirect_err($errno, $errstr, $errfile, $errline, $errcontext){
    17.         throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
    18.         return true;
    19.     }
    20.  
    21.  
    22.     public function __destruct(){
    23.         restore_exception_handler();
    24.         if($this->err_handler){
    25.             restore_error_handler();
    26.         }
    27.     }
    28. }
    Теперь можно назначать обработчик одной строкой:
    Copy Source | Copy HTML
    1. new error_class('err_overwork');
    2. function err_overwork($error){
    3. }


    Ну а скрипт будет выглядеть так:

    Copy Source | Copy HTML
    1. <?php
    2.     session_start();
    3.     header("content-type: text/html; charset=utf-8");
    4.  
    5.     require_once("mail_class.php");
    6.     require_once("error_class.php");
    7.  
    8.     $err = new error_class("err_overwork");
    9.  
    10.     if(
    11.        !isset($_POST['email'])||
    12.        !isset($_POST['message'])||
    13.        !isset($_POST['captcha'])
    14.        ){
    15.         throw new Exception("Не все переменные заданы",E_ERROR);
    16.     }
    17.  
    18.     if($_SESSION['captcha']!=$_POST['captcha']){
    19.         throw new Exception("Неверный код подтверждения",E_ERROR);
    20.     }else{
    21.         unset($_SESSION['captcha']);
    22.     }
    23.  
    24.     $mailer = new mail_class();
    25.     $mailer->text(addslashes(htmlspecialchars($_POST['message'])));
    26.     $mailer->to("Студия «Ночной народ»","raven@nightfolk.net");
    27.     $mailer->from(addslashes($_POST['email']));
    28.     $mailer->subj('Сообщение от посетителя');
    29.     $mailer->reply_to(addslashes($_POST['email']));
    30.     $mailer->send();
    31.  
    32.     header("location:../confirm.html");
    33.     die();
    34.  
    35.     function err_overwork($error){
    36.         $_SESSION['error_message']=$error->getMessage();
    37.         header("location:../error.php");
    38.         die();
    39.     }
    40. ?>


    Послесловие (В принципе — ничего особо важного)



    Надеюсь, что эта статья будет вам интересна или даже принесет какую то пользу.
    Буду этому очень рад.
    Я был бы очень благодарен вам, если бы вы написали свои соображения по поводу класса и процесса его разработки.
    Если вы видете то, что можно улучшить — напишите.
    Если вы видете какие то недоработки, уязвимости или ошибки — напишите.
    И, если напишите, то в этом мире, возможно, хотя бы не на долго станет одним счастливым мной больше.
    А еще — напишите, понравилась ли вам статья или она оказалась затянутой и нудной. Я хочу знать — писать ли мне еще… Или это будет всего навсего агония графомана.
    P.S. Могу описать еще класс кеширования или шаблонизатор. Да мало ли что. Главное, что бы это было интересно вам.

    Благодарности (Читать обязательно)



    Спасибо sunnybear за приглашение на Хабрахабр, за замечательный фреймворк YASS и за то, что он клееевый. Мне он еще на РИТе понравился.
    Спасибо habraname за то, что проявил интерес к YASS, написал забавный враппер и просто приятный в общении.
    Спасибо zerkms за то, что потратил время на пояснения и подтолкнул к дополнительным экспериментам.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 8

      +1
      Вы поломали habr/new
      • НЛО прилетело и опубликовало эту надпись здесь
        0
        Пытаюсь исправить.
          +2
          Мне кажется, что вы не совсем точно поняли механизм исключений.

          Наверняка, проще было бы написать вот так, так как уже есть готовые механизмы для бросания исключений и для их захвата (блоки try и catch)

          Copy Source | Copy HTML
          1. <?php
          2.     //Устанавливаем свой обработчик ошибок
          3.     set_error_handler("UserErrorHandler");
          4.  
          5.     //И опции вывода ошибок
          6.     ini_set('error_reportin', "E_ALL & ~E_NOTICE & ~E_USER_NOTICE & E_USER_WARNING");
          7.     ini_set('display_errors', 1);
          8.  
          9.     session_start();
          10.     require_once("mail_class.php");
          11.  
          12.     if(
          13.        !isset($_POST['email'])||
          14.        !isset($_POST['message'])||
          15.        !isset($_POST['captcha'])
          16.        ){
          17.         throw new Exception("Не все переменные заданы", E_USER_WARNING);
          18.     }
          19.     if($_SESSION['captcha']!=$_POST['captcha']){
          20.         throw new Exception("Неверно введен код обратного теста Тьюринга", E_USER_WARNING);
          21.     }else{
          22.         unset($_SESSION['captcha']);
          23.     }
          24.  
          25.     $mailer = new mail_class();
          26.  
          27.     if(!$mailer->text(addslashes(htmlspecialchars($_POST['message'])))){
          28.         throw new Exception($mailer->err->is_err(), E_USER_WARNING);
          29.     }
          30.  
          31.     throw new Exception("Простое сообщение", E_USER_NOTICE);
          32.     // ...
          33.  
          34.  
          35.     /**
                 * Функция - обработчик ПОЛЬЗОВАТЕЛЬСКИХ ошибок.
                 * @param $errno integer
                 * @param $errstr string
                 * @param $errfile string
                 * @param $errline integer
                 * @return bool
                 */
          36.     function UserErrorHandler($errno, $errstr, $errfile, $errline){
          37.          switch ($errno) {
          38.             case E_USER_ERROR:
          39.                 trigger_error("*USER ERROR* [$errno] $errstr<br />\n".
          40.                 "  User error on line $errline in file $errfile<br />\n");
          41.                 exit(1);
          42.                 break;
          43.             case E_USER_WARNING:
          44.                 // Тут мы что хотим, то и делаем, выводим ошибку или не выводим.. 
          45.                 trigger_error("*USER WARNING* [$errno] $errstr<br />\n".
          46.                 "  User warning on line $errline in file $errfile<br />\n");
          47.                 break;
          48.             case E_USER_NOTICE:
          49.                 trigger_error("*USER NOTICE* [$errno] $errstr<br />\n".
          50.                 "  User notice on line $errline in file $errfile<br />\n");
          51.                 break;
          52.         }
          53.         return true;
          54.     }
          55. ?>
            –1
            0. set_error_handler не определяет обработчик для исключений. throw new Exception будут просто выбрасыватся в поток. Обработчик для исключений задается set_exception_handler.

            1. Я фактически эти механизмы и использовал. Только сделал враппер, что бы их использование стало удобнее.
            Единственное, что, — помоему ошибка не всегда должна обрабатыватся с помощью исключений. Как мне кажется иногда удобнее использовать флаг ошибки и сообщение с текстом.
            Мой класс позволяет не создавая лишний try...catch проигнорировать ошибку, если в контексте приложения это разумно. Но делать это или нет — решать вам. Класс только реализует возможность.
            Если мы генерируем исключение, то будем подниматся вверх по стеку, пока не встретит try...catch или не поднимемся к корню, и тогда либо будет вызвана функция, вызванная обработчиком заданным set_exception_handler либо текст исключения выбросит в поток.
            А если нам это не нужно?
            Все что делает мой класс — дает альтернативу. И немного сокращает код. Например назначение обработчика для ошибок и для исключений нужно несколько строк кода. А у меня их можно задать через конструктор.
            Только и всего.
              +1
              Вы были правы. Исключения — достаточный инструмент для реализации системы обработки ошибок.
                0
                Спасибо вам, что смогли разобраться и упорядочить свои знания :-)

                Так держать!
                  0
                  А вам за попытку объяснить. Она сыграла большую роль в том что я засомневался и полез разбиратся.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое