Блог


Exception VS trigger_error при дебаггинге.

Для начала давайте разберемся, что это за кракозяблы в заголовке.

Exception (англ. исключение) - механизм перехвата ошибок, введенный в 5-й версии PHP. 
Своими словами можно объяснить так. Представьте школу, в ней класс, в классе ученики пишут контрольную. И вдруг один ученик начинает плохо себя вести: кривляется, пускает самолетики или вообще курит.



Учитель берет его за ухо и ведет в кабинет директора. И директор решает, что с ним сделать. Пожурить и простить на первый раз, вызвать родителей или вообще выгнать из школы без права на восстановление.

Тут школа - это программа, полностью приложение. Класс - это область влияния учителя:  блок try , в котором отлавливаются исключения. Кабинет директора - блок catch, где принимаются решения, как обработать ошибку.

Это конечно все условно и упрощенно, но для понимания проблемы пока достаточно. Рассмотрим пример.

1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 190
<?php

//Глобальная область видимости - школа

try // Блок try - класс в школе

    
$schoolboy 'Хулиган';

    if(
$schoolboy != 'Хороший мальчик'// Проверяемая строка 
       
throw new Exception('А '$schoolboy .' сорвал урок!');//Исключение
}                                                           
catch(Exception $e)  // Блок catch - кабинет директора
{
   
// Обработка исключений - наказание нерадивого.
    
echo '- Что случилось? <br>- '
    echo 
$e -> getMessage(), '<br>';
    echo 
'- А вот розгами его!';
}   

Тут все предельно понятно. Если ученик не отвечает требованиям "хороший мальчик", то тащим его к директору и применяем воспитательные меры по мягкому месту.


trigger_error - функция PHP, Используется для вызова пользовательских ошибок, можно использовать в связке с встроенным или пользовательским обработчиком. Действие её похоже на исключения, только она вызывает не блок catch, а штатный или свой обработчик. Вот этот же пример с  функцией trigger_error:

1
 2
 3
 4
 5
 6
 7
 8
 9
<?php


    $schoolboy 
'Хулиган';

    if(
$schoolboy != 'Хороший мальчик'
       
trigger_error('А '$schoolboy .' сорвал урок!'E_USER_ERROR);


Ошибка будет обработана, выведена на экран и слогирована.

Обработку ошибок можно так же как и в блоке catch настроить в своей функции, установив её как обработчик функцией set_error_handler Основные различия между этими подходами на первый взгляд только в разделении сфер влияния. 


Дебаггинг (от англ. Debugging - отладка) это этап разработки программы, в ходе которого обнаруживают, локализуют и исправляют баги (ошибки). Не нужно путать дебаггинг с защитой программы от внештатных, исключительных ситуаций. Дебаггинг, в отличии от логики обработки ошибок, штука одноразовая и производится либо в момент разработки программы, либо при обнаружении неверного её поведения.

Исключения были реализованы в PHP только в пятой версии, поэтому многие считают, что trigger_error() безнадежно устарела и что исключения пришли взамен её. И что обработка любых ошибок должна осуществляться эксепшенами. Так ли это на самом деле? Давайте посмотрим внимательнее. 

Мы рассмотрели пример, где проверяется условие. Но это "пользовательская", скорее логическая ошибка. Даже не столько ошибка, сколько действительно исключительная ситуация. Урок сорван, все плохо, скрипт нужно остановить и отреагировать.

А как же быть с системными? 

Допустим такой момент. Мы пытаемся открыть несуществующий файл и хотим обработать эту ошибку:

1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 158
<?php


try 
{

  if(!
$f fopen('dummy.txt''r'))
      throw new 
Exception('Не могу открыть файл');

catch (
Exception $e)
{  
    echo 
$e -> getMessage();




Поймать то мы её поймаем. И обработаем. Но одновременно сработает и штатный интерпретатор ошибок, что совершенно нам не нужно. Какой напрашивается вывод - загрубить уровень (а значит отказаться от логирования), поставить собачку или запретить вывод ошибок на экран (а значит не видеть вообще ничего, даже синтаксических). Все три способа неприемлемы. Поэтому для обработки warrning, а особенно notice исключения применять не совсем удобно.

Однако не все так плохо, давно придумано решение. Для дебаггинга можно установить пользовательским обработчиком callback функцию, которая выбросит исключение на любую ошибку, которая разрешена error_reporting(). Но нам понадобится кое-что переопределить в классе Exeption, а значит мы должны родить от него  наследника, добавив некоторые изменения:

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
 45
 46
 47
 48
 49
<?php

// Создаем промежуточный класс - наследник
class AllException extends Exception 
{
    public function 
__construct($message$code$file$line
    {
        
$this->file  $file;
        
$this->line  $line;        
        
parent::__construct($message$code);
    }
}
// функция запустит исключение при любой ошибке, позволенной настройками
    
function setAllException($code$message$file$line)
    {
        if(
error_reporting() & $code)
            throw new 
AllException($message$code$file$line);
    }
// Устанавливаем её как пользовательский обработчик
    
set_error_handler('setAllException');

try
{
 
// Теперь не нужно даже кидать исключения
    
$f fopen('dummy.txt''r');
}
catch(
Exception $e
{  // Тут наводим красоту, логируем, делаем что угодно 
    
$code $e -> getCode();
    
    switch(
$code)
    {
        case 
E_NOTICE :
            echo 
'<strong>Notice:</strong> ';
        break;
        
        case 
E_WARNING :
            echo 
'<strong>Warning:</strong> ';
        break;
        
// И так далее    
        
default :
            echo 
'Ошибка: ';
    }
    
    echo 
$e -> getMessage();
    echo 
' in '$e -> getFile();
    echo 
' on line '$e -> getLine();


Примерно по такому принципу работают интерпретаторы ошибок в фреймворках. Вот в Yii реализован красивый вывод со стеками и фрагментами кода:


Всё это хорошо, однако не все пользуются фреймворками. Кроме того, есть одно очень серьёзное различие между исключениями и trigger_error(). Дело в том, что исключения не порождают событий. А это как минимум значит, что нужно озаботиться собственной системой логирования в блоке catch. Но это полбеды, ничего сложного в этом нет, можно настроить обработку как угодно. Беда в том, что исключения, это не от слова "исключать", а от "исключительная ситуация".  А исключительная ситуация сродни Fatal error, при первой же малейшей ошибке или нотайсе останавливает скрипт. Это бывает очень неприятно при отладке, особенно если тестируется что-либо в цикле.  И самая большая неприятность - мы сможем слогировать только первую ошибку, все остальные могут остаться незамеченными.

Вот так мы увидим только первую ошибку, в отличие от штатного интерпретатора, который выдаст и слогирует оба варнинга:

1
 2
 3
 4
 5
 6
 7
 8
 9
 10
<?php

try
{
 
// А вот так мы увидим только первую ошибку.
    
$f fopen('dummy.txt''r');
    
$f fopen('test.txt''r');
}


Решается такая проблема очень просто. В нужный момент, или в продакшене, можно отключить обработку исключений. К примеру задать это константой, а её вынести куда-нибудь в конфигу:

1
 2
 3
 4
 5
 6
 7
 8
 9
<?php


    define
('IRB_ERROR_HANDLER'false);    
    
    if(
IRB_ERROR_HANDLER === true)
        
set_error_handler('setAllException');


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

У исключений есть еще один интересный момент, которым грех не воспользоваться. В арсенале PHP есть функция  set_exception_handler(). Она устанавливает обработчик для неотловленых исключений. А это значит, что можно бросать их прямо в глобальную область видимости и они будут отловлены и обработаны callbac-функцией. Причем это не помешает другим блокам try... catch, коли те появятся в коде.

Что это дает. А дает это возможность сделать отдельный, самодостаточный обработчик исключений и ошибок в отдельном например файле, и использовать его в любых проектах.

Вот допустим такой файлик:

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
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
<?php

// Константа для переключения способа обработки ошибок
    
define('IRB_ERROR_HANDLER'true);    
// Устанавливаем Callbac функции для установки режима исключений    
    
if(IRB_ERROR_HANDLER === true)
    { 
// Системные ошибки
        
set_error_handler('setAllException');
      
// Неотловленные исключения
        
set_exception_handler('setExceptionHandler');
    }
// Callbac - функция для выброса исключений на системные ошибки
    
function setAllException($code$message$file$line)
    {
        if(
error_reporting() & $code)
            throw new 
AllException($message$code, $file$line);
    }
// Callbac - функция вместо блока catch
// Здесь можно навести красоту, вывести стеки и так далее
    
function setExceptionHandler($e)
    {
        
$code $e -> getCode();
        
        switch(
$code)
        {
            case 
E_NOTICE :
                echo 
'<strong>Notice:</strong> ';
            break;
            
            case 
E_WARNING :
                echo 
'<strong>Warning:</strong> ';
            break;
            
// И так далее    
            
default :
                echo 
'Ошибка: ';
        }
        
        echo 
$e -> getMessage();
        echo 
' in '$e -> getFile();
        echo 
' on line '$e -> getLine();
    }     
// Создаем промежуточный класс - наследник
class AllException extends Exception 
{
    public function 
__construct($message$code$file$line
    {
        
$this->file  $file;
        
$this->line  $line;        
        
parent::__construct($message$code);
    }
}


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

Однако и тут не все гладко. Если мы теперь отключим режим исключений, то немедленно получим ошибку Fatal error: Uncaught exception. Неперехваченное исключение. Значит все, за что боролись, идет прахом.

И тут (барабанная дробь!!!) мы вспоминаем про незаслуженно забытый trigger_error(). Дело в том, что при такой схеме не нужно бросать исключений. Потому что любая ошибка (системная или пользовательская) теперь обрабатывается нашим обработчиком (callbac функцией). А это значит, что на каждую ошибку, сгенерированную PHP (как внутреннюю, так и пользовательскую по trigger_error) автоматически будет выброшено исключение, если включен этот режим. А при выключенном они легко обрабатываются штатным обработчиком. Можно вообще смело выкинуть этот файл из продакшена - ничего ровным счетом не случится.

А делается все очень просто. Теперь в самом приложении нужно первый листинг переписать так:

1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
<?php


    
include 'exception.php';

    
$schoolboy 'Хулиган';

    if(
$schoolboy != 'Хороший мальчик'
       
trigger_error('А '$schoolboy .' сорвал урок!'E_USER_WARNING);


И он сработает как исключение, совершенно одинаково.

Резюмируем. Плюсы такого подхода для дебаггинга:
1. Система становится более гибкой и универсальной. Можно взять любую часть и поместить в другое место, где нет обработки исключений. И наоборот - если сильно прижмет, можно включить режим exception и интегрировать в приложение любой скрипт с эксепшенами. 
2. Можно использовать как устроенный по своему усмотрению дебаггер на основе исключений, так и любой штатный, допустим тот же xdebug
3. Отключив режим исключений в продакшене мы имеем логирование всех ошибок, а не только первой случившейся.
4. Можно устанавливать режимы обработки "исключений" путем выбора режима error_reporting(), отключив ненужные для логирования. Те же пользовательские нотайсы к примеру.
5. Код при таком подходе чист и светел, не завален ексепшенами по делу и без.
6. Такая схема совершенно не мешает использовать исключения там, где они действительно необходимы. Так как ставится "поверх" всего и не мешает блокам try...catch любой вложенности.

Так что зря, ой как зря похоронили функцию trigger_error(). Как говорится: в кулацком хозяйстве и бычий орган - веревка.

 

 

Николай aka twin

 

Теги: PHP

Комментарии (2)

kdes70
01-08-2013
Спасибо отличная статья !!!!!
прохожий
04-04-2014
присоединяюсь к благодарностям. Статья и дала готовое практическое понимание ситуации с обработкой ошибок в php и ,опять же, готовую практическую идею для универсального использования. Инфа качественней и ценней любительского трепа на хабре . Спасибо за экономию времени осваивающему php на ходу начинающему фрилансеру)
slava
10-12-2015
Никак не пойму, что такого может дать Exception, что бы быстрее обнаружить ошибку. Вроде как идея хорошая. Только ошибка может быть и в самом приминении Exception. А так пхп итак ошибки выводит. Ну я не писал супер приложений, может поэтому и непонимаю.

 
Наверх