outer();
echo "Конец try-блока.<br>";
} catch (Exception $e) {
echo " Исключение: {$e->getMessage()}<br>";
}
echo "Конец программы.<br>";
function outer() {
echo "Вошли в функцию ".__METHOD__."<br>";
inner();
echo "Вышли из функции ".__METHOD__."<br>";
}
function inner() {
echo "Вошли в функцию ".__METHOD__."<br>";
throw new Exception("Hello!");
echo "Вышли из функции ".__METHOD__."<br>";
}
?>
Результат работы данного кода выглядит так:
Начало программы.
Начало try-блока.
Вошли в функцию outer
Вошли в функцию inner
Исключение: Hello!
Конец программы.
Мы убеждаемся, что ни один из операторов echo, вызываемых после инструкции throw, не "сработал". По сути, программа даже не дошла до них: управление было мгновенно передано в catch-блок, а после этого — в следующую за try...catch строку программы.
Данное поведение инструкции throw называют раскруткой стека вызовов функций, потому что объект-исключение последовательно передается из одной функции в другую, каждый раз приводя к ее завершению — как бы "отматывает" стек.
Можно заметить, что инструкция throw очень похожа на команду return, однако она вызывает "вылет" потока исполнения не только из текущей функции, но также и из тех, которые ее вызвали (до ближайшего соответствующего catch-блока).
Деструктор любого объекта вызывается всякий раз, когда последняя ссылка на этот объект оказывается потерянной, например, программа выходит за границу области видимости переменной. Применительно к механизму обработки исключений это дает мощный инструмент — корректное уничтожение всех объектов, созданных до вызова throw. Листинг 3.3 иллюстрирует ситуацию.
Листинг 3.3. Файл destr.php
<?php ## Деструкторы и исключения.
// Класс, комментирующий операции со своим объектом.
class Orator {
private $name;
function __construct($name) {
$this->name = $name;
echo "Созданобъект {$this->name}.<br>";
}
function __destruct() {
echo "Уничтоженобъект {$this->name}.<br>";
}
}
function outer() {
$obj = new Orator(__METHOD__);
inner();
}
function inner() {
$obj = new Orator(__METHOD__);
echo "Внимание, вбрасывание!<br>";
throw new Exception("Hello!");
}
// Основная программа.
echo "Начало программы.<br>";
try {
echo "Начало try-блока.<br>";
outer();
echo "Конец try-блока.<br>";
} catch (Exception $e) {
echo " Исключение: {$e->getMessage()}<br>";
}
echo "Конец программы.<br>";
?>
Создан специальный класс, который выводит на экран диагностические сообщения в своем конструкторе и деструкторе. Объекты этого класса создаются в первой строке каждой функции.
Результат работы программы выглядит так:
Начало программы.
Начало try-блока.
Создан объект outer.
Создан объект inner.
Внимание, вбрасывание!
Уничтожен объект inner.
Уничтожен объект outer.
Исключение: Hello!
Конец программы.
При вызове throw вначале произошел корректный выход из вложенных функций (с уничтожением всех локальных объектов и вызовом деструкторов), и уж только после этого запустился catch-обработчик. Данное поведение также называют раскруткой стека.
В п.2 рассматривали подход к обработке нефатальных ошибок, а именно установку функции-обработчика посредством вызова функции set_error_handler(). В РНР версии 4 он являлся единственно допустимым методом.
Функция-обработчик имеет один огромный недостаток: в ней неизвестно точно, что же следует предпринять в случае возникновения ошибки.
Сравним явно механизм обработки исключений и метод перехвата ошибок. Рассмотрим пример, похожий на скрипт из листинга 3.1, иллюстрирующий суть проблемы (листинг 3.4).
Листинг 3.4. Файл seh.php
<?php ## Недостатки set_error_handler().
echo "Начало программы.<br>";
set_error_handler("handler");
{
// Код, в котором перехватываются исключения.
echo "Все, что имеет начало...<br>";
// Генерируем ("выбрасываем") исключение.
trigger_error("Hello!");
echo "...имеет и конец.<br>";
}
echo "Конец программы.<br>";
// Функция-обработчик.
function handler($num, $str) {
// Код обработчика.
echo "Ошибка: $str<br>";
// exit();
}
?>
Первое, что бросается в глаза, — это излишняя многословность кода. Но давайте пойдем дальше и посмотрим, какой результат выдает данная программа:
Начало программы.
Все, что имеет начало...
Ошибка: Hello!
За счет использования exit () в функции handler()новая программа не только подвергает сомнению известный тезис (см. операторы echo), но также и утверждает, что любая, даже малейшая, ошибка является фатальной.
Что ж, раз проблема в команде exit(), попробуем ее убрать из скрипта и увидим следующий результат:
Начало программы.
Все, что имеет начало...
Ошибка: Hello!
...имеет и конец.
Конец программы.
И снова мы получили не то, что нужно: ошибка теперь уже не является "чересчур фатальной", как раньше, у нее противоположная проблема: она, наоборот, недостаточно фатальна.
Мы-то хотели разрушать идиому о конечности всего, что имеет начало, а получили — просто робкое замечание, произнесенное шепотом из-за кулис.
Обычно все серьезные ошибки в программе (и внутренние, и пользовательские) поддаются некоторой классификации. Механизм обработки исключений, помимо основного своего достоинства — возможности отделения кода обработки ошибки от кода ее генерации — имеет и еще один плюс. Он заключается в возможности перехвата исключений по их видовой принадлежности на основе иерархии классов. При этом каждое исключение может принадлежать одновременно нескольким видам, и перехватываться с учетом совпадения своего (или родительского) вида.
Листинг 3.5 иллюстрирует тот факт, что при перехвате исключений используется информация о наследовании классов-исключений.
Листинг 3.5. Файл inherit.php
<?php ## Наследование исключений.
// Исключение - ошибка файловых операций.
class FilesystemException extends Exception {
private $name;
public function __construct($name) {
parent::__construct($name);
$this->name = $name;
}
public function getName() { return $this->name; }
}
// Исключение - файлненайден.
class FileNotFoundException extends FilesystemException {}
// Исключение - Ошибка записи в файл.
class FileWriteException extends FilesystemException {}
try {
// Генерируемисключениетипа FileNotFoundException.
if (!file_exists("spoon"))
throw new FileNotFoundException("spoon");
} catch (FilesystemException $e) {
// Ловим ЛЮБОЕ файловое исключение!
echo "Ошибка при работе с файлом '{$e->getName()}'.<br>";
} catch (Exception $e) {
// Ловим все остальные исключения, которые еще не поймали.
echo "Другое исключение: {$e->getDirName()}.<br>";
}
?>
В программе мы генерируем ошибку типа FileNotFoundException, однако, ниже перехватываем исключение не прямо этого класса, а его "родителя" — FilesystemException. Так как любой объект типа FileNotFoundException является также и объектом класса FilesystemException, блок catch "срабатывает" для него. Кроме того, на всякий случай мы используем блок "поимки" объектов класса Exception — "родоначальника" всех исключений. Если вдруг в программе произойдет исключение другого типа (обязательно производного от Exception), оно также будет обработано.
К сожалению, в современной версии РНР реализация исключениями интерфейсов (а следовательно, и множественная классификация) не поддерживается. Точнее, можно создать класс-исключение, наследующий некоторый интерфейс, но попытка перехватить сгенерированное исключение по имени его интерфейса (а не по имени класса) не даст результата. Есть основания надеяться, что в будущих версиях РНР данное неудобство будет устранено.
РНР последних версий не допускает использования объектов произвольного типа в качестве исключений. Если вы создаете свой собственный класс-исключение, то должны унаследовать его от встроенного типа Exception.
До сих пор мы пользовались только стандартным классом Exception, не определяя от него производных. Дело в том, что данный класс уже содержит довольно много полезных методов (например, getMessage ()), которые можно применять в программе.
Итак, каждый класс-исключение в листинге 3.5 наследует встроенный в РНР тип Exception. В этом типе есть много полезных методов и свойств, которые мы сейчас перечислим (приведен интерфейс класса):
class Exception {
protected $message; // текстовоесообщение
protected $code; // числовой код
protected $file; // имя файла, где создано исключение
protected $line; // номер строки, где создан объект
private $trace; // стеквызовов
public function__construct([string $message] [,int $code]);
public final function getMessageО; // возвращает $this->message
public final function getCode{); // возвращает $this->code
public final function getFileO; // возвращает $this->file
public final function getLine(); // возвращает $this->line
public final function getTrace();
public final function getTraceAsStringO;
public function __toStringO;
}
Как видите, каждый объект-исключение хранит в себе довольно много разных данных, блокированных для прямого доступа (protected и private). Впрочем, их все можно получить при помощи соответствующих методов.
Мы не будем подробно рассматривать все методы класса Exception, потому что большинство из них выполняют вполне очевидные действия, следующие из их названий. Остановимся только на некоторых. Обратите внимание, что большинство методов определены как final, а значит, их нельзя переопределять в производных классах.
Конструктор класса принимает два необязательных аргумента, которые он записывает в соответствующие свойства объекта. Он также заполняет свойства $fiie, $line и $trace, соответственно, именем файла, номером строки и результатом вызова функции debug_backtrace() (информацию о функциях, вызвавших данную, см. в п. 2).
Стек вызовов, сохраненный в свойстве $trace, представляет собой список с именами функций (и информацией о них), которые вызвали текущую процедуру перед генерацией исключения. Данная информация полезна при отладке скрипта и может быть получена при помощи метода getTrace(). Дополнительный метод getTraceAsString() возвращает то же самое, но в строковом представлении.