Апр
4
2011

Про ошибки и исключения в PHP

Про ошибки и исключения в PHP

Данная статья ни в коем случае не претендует на пересказ мануала, а содержит некие размышления на тему генерации эрроров и использования исключений в PHP5:

1. Errors & exceptions. В чем отличия и что когда использовать
2. Каким образом можно отлавливать Fatal Error-ы (E_ERROR)
3. Многоуровневая обработка исключений и разматывание стека вызовов методов

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-ы могут бросаться два раза.

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

  • Исключения важная штука и способов её применения масса, к примеру можно сделать глобальную обработку исключений какого-нибудь фреймворка:

    try
    {
    … // Выполняются контроллеры и представления
    } catch (NotFoundException $exc) {

    } catch (NeedAuthException $exc) {

    } catch (DBException $exc) {

    }

    В определённых блоках исключений выводятся припасенные вьюшки (например, страница с 404 ошибкой, если есть ЧПУ) и делаются записи в логи. Мне показалось это очень удобным.

    • Ага! Я так и делал недавно, когда писал один framework. В диспетчере, который обрабатывал маршрутизацию запросов, было нечто в духе:

      try {
              $this->_initRouter();
      	$this->_initView();
      	$controller = $this->_router->route();
      	$this->_dispatch($controller);
      } catch (Router_Exception $e) {
      	// 404 error - page not found
      	$this->_runError($e, '404');
      } catch (Exception $e) {
      	// 500 internal server error
      	trigger_error($e->getMessage(), E_USER_ERROR);
      	$this->_runError($e, '500');
      }

      То есть специльный тип Router_Exception использовался для Исключений в случае, когда надо отображать 404 страницу, и стандартный тип Exception — во всех остальных случаях.

Оставить комментарий

CAPTCHA image


Поля, отмеченные * обязательны для заполнения


XHTML: Вы можете использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">