Обработка ошибок в коде программ РНР
ФЕДЕРАЛЬНОЕ АГЕНТСТВО ПО ОБРАЗОВАНИЮ
РОССИЙСКИЙ ХИМИКО-ТЕХНОЛОГИЧЕСКИЙ УНИВЕРСИТЕТ
им. Д.И. Менделеева
НОВОМОСКОВСКИЙ ИНСТИТУТ
ОБРАБОТКА ОШИБОК В КОДЕ ПРОГРАММ PHP
УЧЕБНОЕ ПОСОБИЕ
Новомосковск 2008
ФЕДЕРАЛЬНОЕ АГЕНТСТВО ПО ОБРАЗОВАНИЮ
РОССИЙСКИЙ ХИМИКО-ТЕХНОЛОГИЧЕСКИЙ УНИВЕРСИТЕТ
им. Д.И. Менделеева
НОВОМОСКОВСКИЙ ИНСТИТУТ
ОБРАБОТКА ОШИБОК В КОДЕ ПРОГРАММ PHP
УЧЕБНОЕ ПОСОБИЕ
Составитель: В. С. Прохоров
Содержание
ВВЕДЕНИЕ
1. КОНТРОЛЬ ОШИБОК
1.1 РОЛИ ОШИБОК
1.2 ВИДЫ ОШИБОК
1.2.1 НЕСЕРЬЕЗНЫЕ ОШИБКИ
1.2.2 СЕРЬЕЗНЫЕ ОШИБКИ
1.2.2.1 Прекращение выполнения программы
1.2.2.2Возврат недопустимого значения
1.2.2.3 Ненормальное состояние программы
1.2.2.4 Вызов функции-обработчика
1.3 ДИРЕКТИВЫ РНР КОНТРОЛЯ ОШИБОК
1.3.1 ДИРЕКТИВА error_reporting
1.3.2 ДИРЕКТИВА display_errors
1.3.3 ДИРЕКТИВА error_log
1.4 УСТАНОВКА РЕЖИМА ВЫВОДА ОШИБОК
1.5 ОПЕРАТОР ОТКЛЮЧЕНИЯ ОШИБОК
1.5.1 ПРИМЕР ИСПОЛЬЗОВАНИЯ ОПЕРАТОРА @
1.5.2 ПРЕДОСТЕРИЖЕНИЯ ПО ПРИМЕНЕНИЮ ОПЕРАТОРА ОТКЛЮЧЕНИЯ ОШИБОК @
2 ПЕРЕХВАТ ОШИБОК. МЕТОД РЕГИСТРАЦИИ ОБРАБОТЧИКА ОШИБОК
2.1 ФУНКЦИЯ set_error_handler
2.2 ФУНКЦИЯ restore_error_handler()
2.3 ПРОБЛЕМЫ С ОПЕРАТОРОМ @
2.4 ГЕНЕРАЦИЯ ОШИБОК
2.5 СТЕК ВЫЗОВОВ ФУНКЦИЙ
2.6 ПРИНУДИТЕЛЬНОЕ ЗАВЕРШЕНИЕ ПРОГРАММЫ
2.7 ФИНАЛИЗАТОРЫ
3. ПЕРЕХВАТ ОШИБОК. МЕТОД ИСКЛЮЧЕНИЙ
3.1 БАЗОВЫЙ СИНТАКСИС
3.2 ИНСТРУКЦИЯ throw
3.3 РАСКРУТКА СТЕКА
3.4 ИСКЛЮЧЕНИЯ И ДЕСТРУКТОРЫ
3.5 ИСКЛЮЧЕНИЯ И set_error_handler()
3.6 КЛАССИФИКАЦИЯ И НАСЛЕДОВАНИЕ
3.7 БАЗОВЫЙ КЛАСС Exception
3.8 ИСПОЛЬЗОВАНИЕ ИНТЕРФЕЙСОВ
3.9 БЛОКИ-ФИНАЛИЗАТОРЫ
3.9.1 Неподдерживаемая конструкция try...finally
3.9.2 "Выделение ресурса есть инициализация"
3.9.3 Перехват всех исключений
3.10 ТРАНСФОРМАЦИЯ ОШИБОК
3.10.1 Серьезность "несерьезных" ошибок
3.10.2 Преобразование ошибок в исключения
3.10.3 Код библиотеки PHP_Exceptionizer
3.10.4 Иерархия исключений
3.10.5 Фильтрация по типам ошибок
3.10.6 Перспективы
ЗАКЛЮЧЕНИЕ
ЛИТЕРАТУРА
ВВЕДЕНИЕ
Имеется мнение: "В любой программе есть хотя бы одна ошибка". На практике "хотя бы одна" означает "много" или даже "очень много".
Фаза "избавления" программы от ошибок (фаза отладки) является наиболее длительной и трудоемкой. Основное времяпровождение программиста (и не только) — это борьба с ошибками.
Одна из самых сильных черт РНР — возможность отображения сообщений об ошибках прямо в браузере. В зависимости от состояния интерпретатора сообщения будут выводиться в браузер или подавляться.
Для успешной борьбы с ошибками нужно научиться управлять настройками РНР, узнать о его тонких местах и о возможностях основных директив. Отдельное внимание следует уделять методикам отладки скриптов, а точнее — обработке сообщений об ошибках и предупреждений, которые могут возникнуть во время работы программы, а также выводу стека вызовов процедур (подобного тому, что существует в языках Java и Perl). Следует с осторожностью использовать оператор отключения предупреждений об ошибках.
Задача обработки ошибок в коде программы - одна из самых важных и популярных при программировании. Для ее успешного решения требуется уточнить понятие термина "ошибка" и определить его роль в программировании, а также изучить различные классификации ошибочных ситуаций. Эта задача может быть эффективно решена при использовании понятия "исключение" и способов применения конструкции try...catch. Использование механизма наследования и классификации исключений может сильно сократить код программы и сделать его универсальным. Существуют коды библиотек, позволяющих обрабатывать многочисленные ошибки и предупреждения, генерируемые функциями РНР, как обыкновенные исключения.
Грамотный перехват ошибок с самого зарождения программирования считался трудной задачей. Механизм обработки исключений, хотя и упрощает ее, но все равно остается весьма сложным.
1. КОНТРОЛЬ ОШИБОК
Термин "ошибка" имеет три различных значений:
1. Ошибочная ситуация — факт наличия ошибки в программе. Это может быть, например, синтаксическая ошибка (пропущенная скобка), или же ошибка семантическая — смысловая (использование переменной, которая ранее не была определена).
2. Внутреннее сообщение об ошибке ("внутренняя ошибка"), которую выдает РНР в ответ на различные неверные действия программы (например, открытие несуществующего файла).
В РНР можно устанавливать различные режимы отображения ошибок, поэтому факт наличия ошибки в программе в смысле предыдущего пункта далеко не всегда приводит к выводу сообщения о ней.
3. Пользовательское сообщение об ошибке ("пользовательская ошибка"), к которой причисляются все сообщения или состояния, генерируемые и обрабатываемые самой программой. Например, в скрипте авторизации ситуация "введен неверный пароль" — ошибка именно такого рода.
1.1 РОЛИ ОШИБОК
Внутреннее сообщение об ошибке означает ошибку, которую нет смысла показывать в браузере пользователя. Это необходимо делать на этапе отладки скрипта, когда в роли пользователя выступает сам разработчик. Такое сообщение лучше всего записывать в файлы журнала для дальнейшего анализа, а в браузер выводить стандартный текст, например: "Произошла внутренняя ошибка, информация о ней будет доступна разработчику скрипта позже". Многие программисты предпочитают также в конце страницы выдавать дополнительные сведения об ошибке, т. е. записывать сообщение и в файл журнала, и выводить на экран. Такая практика в большинстве случаев помогает разработчику "на месте" выяснить, что же произошло.
Для записи сообщений об ошибках в журнал в РНР существуют специальные средства: директивы log errors, error log, а также функция error log () (подробнее см.п.п. 1.3.2, 1.3.3).
Пользовательское сообщение об ошибке предназначено для отображения пользователю — отсюда и его название. При возникновении ошибочной ситуации такого рода пользователь должен увидеть осмысленный текст в браузере, а также, возможно, советы, что же ему теперь делать. Не рекомендуется противопоставлять пользовательские ошибки внутренним — часто они могут в какой-то степени перекрываться. Например, при невозможности соединения с SQL-сервером в программе допустима генерация сразу двух видов сообщений:
● внутреннее сообщение: ответ SQL-сервера, дата и время ошибки, номер строки в программе и т. д.;
● пользовательское сообщение: например, текст "Ошибка соединения с SQL-сервером, попробуйте зайти позже".
1.2 ВИДЫ ОШИБОК
В простейшем случае информация об ошибке включает в себя текст диагностического сообщения, но могут также уточняться и дополнительные данные, например, номер строки и имя файла, где возникла ошибочная ситуация. Если в программе возникла ошибочная ситуация, необходимо принять решение, что же в этом случае делать. Код, который этим занимается (если он присутствует), называют кодом восстановления после ошибки, а запуск этого кода — восстановлением после ошибки. Рассмотрим, например, такой код:
$f = @fopen("spoon.txt", "r");
if (!$f) return;
В этом примере код восстановления — это инструкция if, которая явно обрабатывает ситуацию невозможности открытия файла. Обратите внимание, что используется оператор @ перед fopen(), чтобы не получать диагностическое сообщение от самого РНР — оно не нужно, у нас же собственный обработчик ошибочной ситуации (код восстановления).
В данной терминологии диагностические сообщения, которые выдает РНР, также можно назвать кодом восстановления.
Ошибки по своей "серьезности" можно подразделить на два больших класса:
● серьезные ошибки с невозможностью автоматического восстановления. Например, если вы пытаетесь открыть несуществующий файл, то далее обязательно должны указать, что делать, если это не удастся: ведь записывать или считывать данные из неоткрытого файла нельзя;
● несерьезные (нефатальные) ошибки, восстановление после которых не требуется, например, предупреждения (warnings), уведомления (notices), а также отладочные сообщения (debug notices). Обычно в случае возникновения такого рода ошибочных ситуаций нет необходимости предпринимать что-то особенное и нестандартное, вполне достаточно просто сохранить где-нибудь информацию об ошибке (например, в файле журнала).
Для серьезных ошибок необходимо вручную писать код восстановления и прерывать обычный ход программы, в то время как для ошибок несерьезных ничего особенного делать не нужно.
1.2.1 НЕСЕРЬЕЗНЫЕ ОШИБКИ
Для обработки нефатальных ошибок, после которых не требуется "персональное" восстановление, в РНР имеется инструмент, называемый установкой обработчика ошибок (или перехватом ошибок; подробнее см. п. 2).
Метод заключается в том, что в программе пишется специальная функция — обработчик ошибки, которая вызывается РНР всякий раз, когда наступает та или иная ошибочная ситуация. Задача обработчика — сохранить где-нибудь информацию об ошибке или же просто вывести ее в браузер, красиво оформив.
1.2.2 СЕРЬЕЗНЫЕ ОШИБКИ
Серьезные ошибки в общем случае невозможно обработать с использованием set_error_handler(), потому что в каждом конкретном случае нужно писать "персональный" код восстановления.
В функции-обработчике для восстановления после ошибки можно выполнить всего лишь одно осмысленное действие — это завершить программу.
Главный вопрос при работе с серьезными ошибками — написание кода восстановления. Он должен иметь достаточный контроль над ходом выполнения программы (например, мог выполнять инструкции return или break, а не только лишь завершал программу по exit ()).
1.2.2.1 Прекращение выполнения программы
В случае возникновения серьезной ошибки программа завершает работу по exit() или die().
Такой метод неприемлем в коде библиотек общего пользования: ведь вызывающая программа может не ожидать, что ее выполнение может быть прервано из-за какой-нибудь мелочи вроде невозможности открытия файла журнала.
1.2.2.2 Возврат недопустимого значения
Практически все стандартные функции РНР в случае возникновения ошибочной ситуации возвращают false или NULL, а также вызывают trigger_error() для фиксирования диагностического сообщения (его можно потом перехватить при помощи функции-обработчика). Например, функция fopen() при невозможности открытия файла возвращает false, и мы в дальнейшем должны проверить результат на "истинность".
Этот метод имеет три недостатка.
● Главный недостаток — программист может забыть проверить возвращенное функцией значение, и продолжить выполнение программы так, будто бы ничего не произошло. В большинстве случаев это породит лавинообразное нарастание количества диагностических сообщений, не имеющих никакого отношения к сути проблемы.
● Любое значение, возвращаемое функцией, может быть по смыслу допустимым. Например, стандартная функция unserialize() распаковывает некоторую переменную из ее строкового представления (сгенерированного вызовом serialize()) и возвращает ее исходное значение. Что должна вернуть функция в случае ошибки? Если, например, NULL, то где гарантия, что действительно произошла ошибка, а исходная переменная не содержала просто значение NULL до упаковки?
● Код восстановления приходится постоянно дублировать. Например, если в программе нужно открыть 10 разных файлов, мы будем вынуждены 10 раз проверить возвращаемое функцией fopen() значение. Легко представить, как сильно это "раздует" программу. Еще хуже вы себя почувствуете, если вспомните, что в будущем может понадобиться модифицировать код восстановления: придется делать это в 10 местах.
1.2.2.3 Ненормальное состояние программы
Часто одного лишь недопустимого возвращаемого значения оказывается недостаточно для хранения всей информации. Поэтому в дополнение к предыдущему способу в РНР иногда применяется запись информации об ошибке в некоторую внутреннюю переменную, которую можно будет в дальнейшем считать и проанализировать другими функциями.
Так работают, например, инструменты для доступа к MySQL в PHP: mysql_connect(), mysql_query() и т. д. Вы можете проверить, не вернули ли функции "ложное" значение, а потом использовать mysql_error() или mysql_errno() для получения дополнительной информации. Состояние программы, в котором когда-то ранее произошла ошибка, требующая дополнительного анализа, называют ненормальным. Главный недостаток ненормального состояния в том, что если вдруг произойдет еще одна ошибка, то информация о ней "затрет" предыдущее сообщение. Таким образом, мы вынуждены периодически проверять, не находится ли программа в ненормальном состоянии, и предпринимать дополнительные действия. Это сильно увеличивает код, кроме того, программист может забыть вставить в программу требуемые проверки.
1.2.2.4 Вызов функции-обработчика
Этот способ применяют к обработке несерьезных ошибок. Для перехвата серьезных ошибочных ситуаций он применим мало. Действительно, функция-обработчик имеет в своем распоряжении те же самые три альтернативы. Она не получает в свое распоряжение ни текущие локальные переменные программы, ни информацию о том, что следует делать в каждом персональном случае.
1.3 ДИРЕКТИВЫ РНР КОНТРОЛЯ ОШИБОК
Уровнем детализации сообщений об ошибках, а также другими параметрами управляют директивы РНР, перечисленные ниже.
1.3.1 ДИРЕКТИВА error_reporting
error_reporting
● Возможные значения: числовая константа (по умолчанию — E_ALL~E_NOTICE.
● Где устанавливается: php.ini, .htaccess, ini_set ().
Устанавливает "уровень строгости" для системы контроля ошибок РНР. Значение этого параметра должно быть целым числом, которое интерпретируется как десятичное представление двоичной битовой маски. Установленные в 1 биты задают, насколько детальным должен быть контроль. Можно также не возиться с битами, а использовать константы. В табл. 1.1 приведены некоторые константы, которые на практике применяются чаще всего.
Таблица 1.1. Биты, управляющие контролем ошибок
Бит |
Константа PHP |
Назначение |
1 |
E_ERROR |
Фатальные ошибки |
2 |
E_WARNING |
Общие предупреждения |
4 |
E_PARSE |
Ошибки трансляции |
8 |
E_NOTICE |
Предупреждения |
16 |
E_CORRE_ ERROR |
Глобальные предупреждения (почти не используется) |
32 |
E_CORRE_STRING |
Глобальные ошибки (не используется) |
2048 |
E_STRICT |
Различные "рекомендации" РНР по улучшению кода (например, замечания насчет вызова устаревших функций) |
2047 |
E_ALL |
Все перечисленные флаги, за исключением E_STRICT |
Чаще всего встречается значение 7 (1+2 + 4), или, что то же самое, E_ALL ~ E_NOTICE (здесь оператор ~ означает побитовое "исключающее ИЛИ"). Оно задает полный контроль, кроме некритичных предупреждений интерпретатора (таких, например, как обращение к неинициализированной переменной). Часто это значение задается по умолчанию при установке РНР.
Если вы разрабатываете скрипты на РНР, первое, что вам стоит сделать, — это устанавливать значение error_reporting равным E_ALL или даже E_ALL | E_STRICT, т.е. включить абсолютно все сообщения об ошибках.
Хотя в уже имеющихся сценариях (включая популярные системы phpBB, phpNuke и т. д.) это, скорее всего, породит целые легионы самых разнообразных предупреждений, не стоит их пугаться: они свидетельствуют лишь о недостаточно хорошем качестве кода.
Режим E_ALL очень помогает при отладке скриптов. Существуют распространенные ошибки, над которыми можно просидеть не один час, в то время как с включенным режимом E_ALL они обнаруживаются в течение нескольких минут.
Лучше всего держать в файле php.ini максимально возможный режим контроля ошибок —E_ALL или даже E_ALL E_STRICT, т.е. включить абсолютно все сообщения об ошибках, а в случае крайней необходимости отключать его в скриптах в персональном порядке. Для этого существует три способа.
Способы отключения режима контроля ошибок:
● использовать функцию error_reporting(E_ALL~E_NOTICE);
● запустить функцию ini_set("error_reporting", E_ALL~E_NOTICE);
● использовать оператор @ для локального отключения ошибок.
1.3.2 ДИРЕКТИВА display_errors
display_errors
log_errors
● Возможные значения: on или off.
● Где устанавливается: php.ini, .htaccess, iniseto.
Если директива display_errors установлена в значение on, все сообщения об ошибках и предупреждения выводятся в браузер пользователя, запустившего скрипт.
Если же установлен параметр log_errors, то сообщения дополнительно попадают и в файл журнала (см. ниже директиву error_log).
При отладке скриптов рекомендуется устанавливать display_errors в значение on, потому что это сильно упрощает работу. И наоборот, если скрипт работает на хостинге, сообщения об ошибках нежелательны — лучше их выключить, а вместо этого включить log_errors.
1.3.3 ДИРЕКТИВА error_log
error_log
● Возможные значения: абсолютный путь к файлу (по умолчанию — не задан).
● Где устанавливается: php.ini, .htaccess, ini_set().
В PHP существуют два метода вывода сообщений об ошибках: печать ошибок в браузер и запись их в файл журнала (log-файл). Директива error_log задает путь к журналу.
1.4 УСТАНОВКА РЕЖИМА ВЫВОДА ОШИБОК
Для установки режима вывода ошибок во время работы программы служит функция error_reporting().
int error_reporting([int $level])
Эта функция устанавливает "уровень строгости" для системы контроля ошибок РНР, т. е. величину параметра error_reporting в конфигурации РНР.
Рекомендуется первой строкой сценария ставить вызов:
error_reporting(E_ALL);
При этом могут раздражать "мелкие" сообщения типа "использование неинициализированной переменной". Практика показывает, что эти предупреждения свидетельствуют (чаще всего) о возможной логической ошибке в программе, и что при их отключении может возникнуть ситуация, когда программу будет очень трудно отладить.
1.5 ОПЕРАТОР ОТКЛЮЧЕНИЯ ОШИБОК
Есть и еще один аргумент в пользу того, чтобы всегда включать полный контроль ошибок. Это — существование в РНР оператора @. Если этот оператор поставить перед любым выражением, то все ошибки, которые там возникнут, будут проигнорированы.
Если в выражении используется результат работы функции, из которой вызывается другая функция и т. д., то предупреждения будут заблокированы для каждой из них. Поэтому осторожно применяйте @.
Например:
if (!@filemtime("notextst.txt") )
echo "Файл не существует!";
Попробуйте убрать оператор @ — тут же получите сообщение: "Файл не найден", а только после этого — вывод оператора echo. Однако с оператором @ предупреждение будет подавлено, что и требовалось.
В приведенном примере, возможно, несколько логичнее было бы воспользоваться функцией file_exists(), которая как раз и предназначена для определения факта существования файла, но в некоторых ситуациях это не подойдет. Например:
//Обновить файл, если он не существует или очень старый
if ( ! file_exists($fname) | | filemtime ($fname) <time () -60*60)
myFunctionForUpdateFile($fname);
Сравните со следующим фрагментом:
// Обновить файл, если он не существует или очень старый
if (@filemtime($fname)<time()-60*60)
myFunctionForUpdateFile($fname);
Всегда помните об операторе @. Он удобен. Рекомендуется не рисковать, задавая слабый контроль ошибок при помощи функции error_reporting(), если такой контроль и так можно локально установить при помощи оператора @?
Оператор отключения ошибок @ ценен еще и тем, что он блокирует не только вывод ошибок в браузер, но также и в log-файл. Пример из листинга 1.1 иллюстрирует ситуацию.
Листинг 1.1. Файл er.php
<?php ## Отключение ошибок: логи не модифицируются.
error_reporting(E_ALL);
ini_set("error_log", "log.txt");
ini_set("log_errors", true);
@filemtime("spoon");
?>
Запустив приведенный скрипт, вы заметите, что файл журнала log.txt даже не создался. Попробуйте теперь убрать оператор @ — вы- получите предупреждение "stat failed for spoon", и оно же запишется в log.txt.
1.5.1 ПРИМЕР ИСПОЛЬЗОВАНИЯ ОПЕРАТОРА @
Пусть имеется форма с sub>mit-кнопкой, и нужно в сценарии определить, нажата ли она. Можно сделать это так:
<?php
if (!empty($sub>mit)) echo "Кнопка нажата!";
…
?>
Согласитесь, код листинга 1.2 элегантнее.
Листинг 1.2. Файл sub>mit.php
<?php ## Удобство оператора @.
if (@$_REQUEST['sub>mit']) echo "Кнопка нажата!"
?>
<form action="<?=$_SERVER['SCRIPT_NAME']?>">
<input type="sub>mit" name="sub>mit" value="Go!">
</form>
1.5.2 ПРЕДОСТЕРИЖЕНИЯ ПО ПРИМЕНЕНИЮ ОПЕРАТОРА ОТКЛЮЧЕНИЯ ОШИБОК @
Оператор @ следует применять с осторожностью. Например, следующий код никуда не годится — постарайтесь не повторять его в своих программах.
// Не подавляйте сообщения об ошибках во включаемых файлах — иначе
// отладка превратится в кромешный ад!
@include "mistake.php";
//Не используйте оператор @ перед функциями, написанными на РНР,
// если только нет 100%-й уверенности в том, что они работают
// корректно в любой ситуации!
@myOwnBigFunction() ;
Рекомендации, в каких случаях применение оператора подавления ошибок оправдано и относительно безопасно:
● в конструкциях if (@$_REQUEST[' key' ]) для проверки существования (и ненулевого значения) элемента массива;
● перед стандартными функциями РНР вроде fopen(), filemtime(), mysql_connect() и т. д., если далее идет проверка кода возврата и вывод сообщения об ошибке;
● в HTML-файлах со вставками PHP-кода, если очень лень писать много кавычек: <?=@$resuit [element] [field] ?> (такой вызов не породит ошибок, несмотря на отсутствие кавычек).
Во всех остальных случаях лучше несколько раз подумать, прежде чем применять оператор @. Чем меньше область кода, в которой он будет действовать, тем более надежной окажется программа. Поэтому не рекомендуется использовать @ перед include — это заблокирует проверку ошибок для очень большого фрагмента программы.
2. ПЕРЕХВАТ ОШИБОК. МЕТОД РЕГИСТРАЦИИ ОБРАБОТЧИКА ОШИБОК
В РНР версии 5 существуют два метода перехвата ошибок во время выполнения программы:
● регистрация обработчика ошибки.
● исключений;
РНР поддерживает средства, позволяющие "перехватывать" момент возникновения той или иной ошибки (или предупреждения) и вызывать при этом функцию, написанную пользователем.
Метод перехвата ошибок при помощи пользовательских функций далек от совершенства. Сделать с сообщением что-либо разумное, кроме как записать его куда-нибудь и завершить программу (при необходимости), не представляется возможным.
Метод исключений, который рассмотрен в п. 3, лишен этого недостатка, он достаточно сложен и практически не реализован на уровне стандартных функций РНР.
Пример использования обработчика ошибок приведен в листинге 2.1.
Листинг 2.1. Файл handler1.php
<?php ## Перехват ошибок и предупреждений.
// Определяем новую функцию-обработчик.
function myErrorHandler($errno, $msg, $file, $line) {
// Если используется @, ничего не делать.
if (error_reporting() == 0) return;
// Иначе - выводим сообщение.
echo '<div style="border-style:inset; border-width:2">';
echo "Произошла ошибка с кодом <b>$errno</b>!<br>";
echo "Файл: <tt>$file</tt>, строка $line.<br>";
echo "Текст ошибки: <i>$msg</i>";
echo "</div>";
}
// Регистрируем ее для всех типов ошибок.
set_error_handler("myErrorHandler", E_ALL);
// Вызываем функцию для несуществующего файла, чтобы
// сгенерировать предупреждение, которое будет перехвачено.
filemtime("spoon");
?>
Что бы ни произошло, программа всегда продолжает свою работу — просто в момент возникновения ошибочной ситуации вызывается функция-обработчик, а затем выполнение идет дальше. Именно по этой причине обработчик нельзя назвать кодом восстановления.
2.1 ФУНКЦИЯ set_error_handler
Функция
string set_error_handler(string $runcName [, int $errorTypes])
регистрирует пользовательский обработчик ошибок — функцию, которая будет вызвана при возникновении сообщений, указанных в $errorTypes типов (битовая маска, например, E_ALL ~ E_NOTICE).
Сообщения, не соответствующие маске $errorTypes, будут в любом случае обрабатываться встроенными средствами РНР, а не предыдущей установленной функцией-перехватчиком. Имя пользовательской функции передается в параметре $runcName. Если до этого был установлен какой-то другой обработчик, функция вернет его имя — с тем, чтобы его можно было позже восстановить. Пользовательский обработчик должен задаваться так, как показано в листинге 2.2.
Листинг 2.2. Файл handler0.php
<?php ## Перехват ошибок и предупреждений.
// Определяем новую функцию-обработчик.
function myErrorHandler($errno, $msg, $file, $line) {
echo '<div style="border-style:inset; border-width:2">';
echo "Произошла ошибка с кодом <b>$errno</b>!<br>";
echo "Файл: <tt>$file</tt>, строка $line.<br>";
echo "Текст ошибки: <i>$msg</i>";
echo "</div>";
}
// Регистрируем ее для всех типов ошибок.
set_error_handler("myErrorHandler", E_ALL);
// Вызываем функцию для несуществующего файла, чтобы
// сгенерировать предупреждение, которое будет перехвачено.
filemtime("spoon");
?>
Теперь при возникновении любой ошибки или даже предупреждения в программе будет вызвана функция myErrorHandier(). Ее аргументы получат значения, соответственно, номера ошибки, текста ошибки, имени файла и номера строки, в которой было сгенерировано сообщение.
К сожалению, не все типы ошибок могут быть перехвачены таким образом. Например, ошибки трансляции во внутреннее представление E_PARSE, а также E_ERROR немедленно завершают работу программы. Вызовы функции die()также не перехватываются.
Назначить функцию реакции на E_PARSE и E_ERROR все же можно. Дело в том, что перехватчик выходного потока скрипта, устанавливаемый функцией ob__start(), обязательно вызывается при завершении работы программы, в том числе в случае фатальной ошибки. Конечно, ему не передается сообщение об ошибке и ее код; он должен сам получить эти данные из "хвоста" выходного потока (например, используя функции для работы с регулярными выражениями).
В случае если пользовательский обработчик возвращает значение false (и только его!), считается, что ошибка не была обработана, и управление передается стандартному обработчику РНР (обычно он выводит текст ошибки в браузер). Все остальные возвращаемые значения (включая даже null или, что то же самое, в случае, если оператора return вообще нет), приводят к подавлению запуска стандартной процедуры обработки ошибок.
2.2 ФУНКЦИЯ restore_error_handler()
void restore_error_handler()
Когда вызывается функция set_error_handler(), предыдущее имя пользовательской функции запоминается в специальном внутреннем стеке РНР. Чтобы извлечь это имя и тут же его установить в качестве обработчика, применяется функция restore_error_handler().Пример:
// Регистрируем обработчик для всех типов ошибок.
set_error_handler("myErrorHandler", E_ALL);
// Включаем подозрительный файл.
include "suspicious_file.php";
// Восстанавливаем предыдущий обработчик.
restore_error_handler();
Необходимо следить, чтобы количество вызовов restore_error_handler() было в точности равно числу вызовов set_error_handler().Нельзя восстановить то, чего нет.
2.3 ПРОБЛЕМЫ С ОПЕРАТОРОМ @
Пользовательская функция перехвата ошибок вызывается вне зависимости от того, был ли использован оператор подавления ошибок в момент генерации предупреждения. Это очень неудобно: поставив оператор @ перед вызовом filemtime(), мы увидим, что результат работы скрипта нисколько не изменился: текст предупреждения по-прежнему выводится в браузер.
Для того чтобы решить проблему, воспользуемся полезным свойством оператора @: на время своего выполнения он устанавливает error_reporting, равным нулю. Значит, мы можем определить, был ли вызван оператор @ в момент "срабатывания" обработчика, по нулевому значению функции error_reporting() (при вызове без параметров она возвращает текущий уровень ошибок) — листинг 2.3.
Листинг 2.3. Файл handler.php
<?php ## Перехват ошибок и предупреждений.
// Определяем новую функцию-обработчик.
function myErrorHandler($errno, $msg, $file, $line) {
// Если используется @, ничего не делать.
if (error_reporting() == 0) return;
// Иначе - выводим сообщение.
echo '<div style="border-style:inset; border-width:2">';
echo "Произошла ошибка с кодом <b>$errno</b>!<br>";
echo "Файл: <tt>$file</tt>, строка $line.<br>";
echo "Текст ошибки: <i>$msg</i>";
echo "</div>";
}
// Регистрируем ее для всех типов ошибок.
set_error_handler("myErrorHandler", E_ALL);
// Вызываем функцию для несуществующего файла, чтобы
// сгенерировать предупреждение, которое будет перехвачено.
@filemtime("spoon");
?>
Теперь все работает корректно: предупреждение не отображается в браузере, т. к. применен оператор @.
2.4 ГЕНЕРАЦИЯ ОШИБОК
Функция
void trigger_error(string $message [, int $type])
предназначена для искусственной генерации сообщения об ошибки с указанным типом. В РНР существует несколько констант, чьи имена начинаются с E_USER_, которые можно использовать наравне с E_ERROR, E_WARNING и т. д., но только для персональных нужд. Именно с функцией trigger_error() они чаще всего и применяются. Вот эти константы:
E_ERROR
E_WARNING
E_USER_NOTICE
Как их использовать — целиком зависит от программиста: никаких ограничений не налагается.
int error_log(string $msg [,int $type=O] [,string $dest] [, string $extra_headers])
Выше мы рассматривали директивы log_errors и error_log, которые заставляют РНР записывать диагностические сообщения в файл, а не только выводить их в браузер. Функция error_log по своему смыслу похожа на trigger_error(), однако, она заставляет интерпретатор записать некоторый текст ($msg) в файл журнала (при нулевом $type и по умолчанию), заданный в директиве error_log. Основные значения аргумента $type, которые может принимать функция, перечислены ниже:
● $type = = 0
Записывает сообщение в системный файл журнала или в файл, заданный в директиве error_log.
● $type = = 1
Отправляет сообщение по почте адресату, чей адрес указан в $dest. При этом $extra_headers используется в качестве дополнительных почтовых заголовков.
● $type == 3
Сообщение добавляется в конец файла с именем $dest.
2.5 СТЕК ВЫЗОВОВ ФУНКЦИЙ
В РНР версии 4.3.0 и старше существует возможность проследить всю цепочку вызовов функций, начиная от главной программы и заканчивая текущей выполняемой процедурой.
Функция
list debug_backtrace()
возвращает большой список, в котором содержится информация о "родительских" функциях и их аргументах. Результат работы листинга 2.4 говорит сам за себя.
Листинг2.4. Файл trace.php
<?php ## Вывод дерева вызовов функции.
function inner($a) {
// Внутренняя функция.
echo "<pre>"; print_r(debug_backtrace()); echo "</pre>";
}
function outer($x) {
// Родительская функция.
inner($x*$x);
}
// Главная программа.
outer(3);
?>
После запуска этого скрипта будет напечатан примерно следующий результат (его чуть сжали):
Array (
[0] => Array (
[file] => z:\home\book\original\src\interpreter\trace.php
[line] => 6
[function] => inner
[args] => Array ([0] => 9)
)
[1] => Array (
[file] => z:\home\book\original\src\interpreter\trace.php
[line] => 8
[function] => outer
[args] => Array ([0] => 3)
)
)
Как видите, в массиве оказалась все информация о промежуточных вызовах функций.
Функцию удобно применять, например, в пользовательском обработчике ошибок. Тогда можно сразу же определить, в каком месте было сгенерировано сообщение и какие вызовы к этому привели, В крупных программах уровень вложенности функций может достигать нескольких десятков, поэтому, наверное, вместо print_r() стоит написать свой собственный код для вывода дерева вызовов в более компактной форме.
2.6 ПРИНУДИТЕЛЬНОЕ ЗАВЕРШЕНИЕ ПРОГРАММЫ
Функция
void exit ()
немедленно завершает работу сценария. Из нее никогда не происходит возврата. Перед окончанием программы вызываются функции-финализаторы.
void die(string $message)
Функция делает почти то же самое, что и exit (), только перед завершением работы выводит строку, заданную в параметре $message). Чаще всего ее применяют, если нужно напечатать сообщение об ошибке и аварийно завершить программу.
Полезным примером использования die () может служить такой код:
$filename = '/path/to/data-file';
$file = @fopen($filename, 'r')
or die("не могу открыть файл $ filename!");
Здесь мы ориентируемся на специфику оператора or — "выполнять" второй операнд только тогда, когда первый "ложен". Заметьте, что оператор || здесь применять нельзя — он имеет более высокий приоритет, чем =.
С использованием || последний пример нужно было бы переписать следующим образом:
$filename = '/path/to/data-file';
($file = fopen($filename, 'r'))
|| die("не могу открыть файл $filename!");
Согласитесь, громоздкость последнего примера практически полностью исключает возможность применения || в подобных конструкциях.
2.7 ФИНАЛИЗАТОРЫ
Разработчики РНР предусмотрели возможность указать в программе функцию-финализатор, которая будет автоматически вызвана, как только работа сценария завершится — неважно, из-за ошибки или легально. В такой функции мы можем, например, записать информацию в кэш или обновить какой-нибудь файл журнала работы программы. Что же нужно для этого сделать?
Во-первых, написать саму функцию и дать ей любое имя. Желательно также, чтобы она была небольшой и в ней не было ошибок, потому что сама функция, вполне возможно, будет вызываться перед завершением сценария из-за ошибки.
Во-вторых, зарегистрировать ее как финализатор, передав ее имя стандартной функции register_shutdown_function().
int register_shutdown_function(string $func)
Регистрирует функцию с указанным именем с той целью, чтобы она автоматически вызывалась перед возвратом из сценария. Функция будет вызвана как при окончании программы, так и при вызовах exit() или die(), а также при фатальных ошибках, приводящих к завершению сценария — например, при синтаксической ошибке.
Конечно, можно зарегистрировать несколько финальных функций, которые будут вызываться в том же порядке, в котором они регистрировались.
Правда, есть одно "но". Финальная функция вызывается уже после закрытия соединения с браузером клиента. Поэтому все данные, выведенные в ней через echo, теряются (во всяком случае, так происходит в Unix-версии РНР, а под Windows CGI-версия РНР и echo работают прекрасно). Так что лучше не выводить никаких данных в такой функции, а ограничиться работой с файлами и другими вызовами, которые ничего не направляют в браузер.
Последнее обстоятельство, к сожалению, ограничивает функциональность финализаторов: им нельзя поручить, например, вывод окончания страницы, если сценарий по каким-то причинам прервался из-за ошибки. Вообще говоря, надо заметить, что в РНР никак нельзя в случке ошибки в некотором запущенном коде проделать какие-либо разумные действия (кроме, разумеется, мгновенного выхода).
3. ПЕРЕХВАТ ОШИБОК. МЕТОД ИСКЛЮЧЕНИЙ
Механизм обработки исключений или просто "исключения" (exceptions) — это технология, позволяющая писать код восстановления после серьезной ошибки в удобном для программиста виде. С применением исключений перехват и обработка ошибок, наиболее слабая часть в большинстве программных систем, значительно упрощается.
Концепция исключений базируется на общей идее объектно-ориентированного программирования: данные должны обрабатываться в том участке программы, который имеет максимум сведений о том, как это делать. Если в некотором месте еще не до конца известно, какое именно преобразование должно быть выполнено, лучше отложить работу "на потом". С использованием исключений код обработки ошибки явно отделяется от кода, в котором ошибка может возникнуть.
Исключения также позволяют удобно передавать информацию о возникшей ошибке вниз по дереву (стеку) вызовов функций. Таким образом, код восстановления может находиться даже не в текущей процедуре, а в той, что ее вызывает.
3.1 БАЗОВЫЙ СИНТАКСИС
Исключение — это некоторое сообщение об ошибке вида "серьезная". При своей генерации оно автоматически передается в участок программы, который лучше всего "осведомлен", что же следует предпринять в данной конкретной ситуации. Этот участок называется обработчиком исключения.
Любое исключение в программе представляет собой объект некоторого класса, создаваемый, как обычно, оператором new. Этот объект может содержать различную информацию, например, текст диагностического сообщения, а также номер строки и имя файла, в которых произошла генерация исключения. Допустимо добавлять и любые другие параметры.
Прежде чем идти дальше, давайте рассмотрим простейший пример вызова обработчика (листинг 3.1). Заодно получим представление о синтаксисе исключений.
Листинг.3.1. Файл simple.php
<?php ## Простой пример использования исключений.
echo "Начало программы.<br>";
try {
// Код, в котором перехватываются исключения.
echo "Все, что имеет начало...<br>";
// Генерируем ("выбрасываем") исключение.
throw new Exception("Hello!");
echo "...имеет и конец.<br>";
} catch (Exception $e) {
// Код обработчика.
echo " Исключение: {$e->getMessage()}<br>";
}
echo "Конец программы.<br>";
?>
В листинге 3.1 приведен пример базового синтаксиса конструкции try...catch, применяемой для работы с исключениями.
Рассмотрим эту инструкцию подробнее:
● Код обработчика исключения помещается в блок инструкции catch (в переводе с английского — "ловить").
● Блок try (в переводе с английского — "попытаться") используется для того, чтобы указать в программе область перехвата. Любые исключения, сгенерированные внутри нее (и только они), будут переданы соответствующему обработчику.
● Инструкция throw используется для генерации исключения. Генерацию также называют возбуждением или даже выбрасыванием (или "вбрасыванием") исключения (от англ. throw — бросать). Как было замечено ранее, любое исключение представляет собой обычный объект РНР, который мы и создаем в операторе new.
● Обратите внимание на аргумент блока catch. В нем указано, в какую переменную должен быть записан "пойманный" объект-исключение перед запуском кода обработчика. Также обязательно задается тип исключения — имя класса. Обработчик будет вызван только для тех объектов-исключений, которые совместимы с указанным типом (например, для объектов данного типа).
Работа инструкции try...catch заключается в том, что одна часть программы "бросает" (throw) исключение, а другая — его "ловит" (catch).
3.2 ИНСТРУКЦИЯ throw
Инструкция throw не просто генерирует объект-исключение и передает его обработчику блока catch. Она также немедленно завершает работу текущего try-блока. Именно поэтому результат работы сценария из листинга 3.1 выглядит так:
Начало программы.
Все, что имеет начало...
Исключение: Hello!
Конец программы.
Как видите, за счет особенности инструкции throw наша программа подвергает серьезному скепсису тезис "Все, что имеет начало, имеет и конец" — она просто не выводит окончание фразы.
В этом отношении инструкция throw очень похожа на инструкции return, break и continue: они тоже приводят к немедленному завершению работы текущей функции или итерации цикла.
3.3 РАСКРУТКА СТЕКА
Самой важной и полезной особенностью инструкции throw является то, что ее можно использовать не только непосредственно в try-блоке, но и внутри любой функции, которая оттуда вызывается. При этом производится выход не только из функции, содержащей throw, но также и из всех промежуточных процедур. Пример — в листинге 3.2.
Листинг3.2.Файл stack.php
<?php ## Инструкция try во вложенных функциях.
echo "Начало программы.<br>";
try {
echo "Начало try-блока.<br>";
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-блока).
3.4 ИСКЛЮЧЕНИЯ И ДЕСТРУКТОРЫ
Деструктор любого объекта вызывается всякий раз, когда последняя ссылка на этот объект оказывается потерянной, например, программа выходит за границу области видимости переменной. Применительно к механизму обработки исключений это дает мощный инструмент — корректное уничтожение всех объектов, созданных до вызова 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-обработчик. Данное поведение также называют раскруткой стека.
3.5 ИСКЛЮЧЕНИЯ И set_error_handler()
В п.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.6 КЛАССИФИКАЦИЯ И НАСЛЕДОВАНИЕ
Обычно все серьезные ошибки в программе (и внутренние, и пользовательские) поддаются некоторой классификации. Механизм обработки исключений, помимо основного своего достоинства — возможности отделения кода обработки ошибки от кода ее генерации — имеет и еще один плюс. Он заключается в возможности перехвата исключений по их видовой принадлежности на основе иерархии классов. При этом каждое исключение может принадлежать одновременно нескольким видам, и перехватываться с учетом совпадения своего (или родительского) вида.
Листинг 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), оно также будет обработано.
К сожалению, в современной версии РНР реализация исключениями интерфейсов (а следовательно, и множественная классификация) не поддерживается. Точнее, можно создать класс-исключение, наследующий некоторый интерфейс, но попытка перехватить сгенерированное исключение по имени его интерфейса (а не по имени класса) не даст результата. Есть основания надеяться, что в будущих версиях РНР данное неудобство будет устранено.
3.7 БАЗОВЫЙ КЛАСС 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() возвращает то же самое, но в строковом представлении.
Оператор преобразования в строку _toString() выдает всю информацию, сохраненную в объекте-исключении. При этом используются все свойства объекта, а также вызывается getTraceAsString() для преобразования стека вызовов в строку. Результат, который генерирует метод, довольно интересен (листинг.3.6).
Листинг 3.6. Файл tostring.php
<?php ## Вывод сведений об исключении.
function test($n) {
$e = new Exception("bang-bang #$n!");
echo "<pre>", $e, "</pre>";
}
function outer() { test(101); }
outer();
?>
Выводимый текст будет примерно следующим:
exception 'Exception' with message 'bang-bang #101!' in tostring.php:3
Stack trace:
#0 tostring.php(6): test(101)
#1 tostring.php{7): outer()
#2 (main)
3.8 ИСПОЛЬЗОВАНИЕ ИНТЕРФЕЙСОВ
В РНР поддерживается только одиночное наследование классов: у одного и того же типа не может быть сразу двух "предков". Применение интерфейсов дает возможность реализовать множественную классификацию — отнести некоторый класс не к одному, а сразу к нескольким возможным типам.
Множественная классификация оказывается как нельзя кстати при работе с исключениями. С использованием интерфейсов вы можете создавать новые классы-исключения, указывая им не одного, а сразу нескольких "предков" (и, таким образом, классифицируя их по типам).
Использование интерфейсов вместе с исключениями возможно, начиная с РНР 5.0.1.
Предположим, у нас в программе могут возникать серьезные ошибки следующих основных видов:
● внутренние: детальная информация в браузере не отображается, но записывается в файл журнала. Внутренние ошибки дополнительно подразделяются на:
• файловые (ошибка открытия, чтения или записи в файл);
• сетевые (например, невозможность соединения с сервером);
● пользовательские: сообщения выдаются прямо в браузер.
Для классификации сущностей в программе удобно использовать интерфейсы. Давайте так и поступим по отношению к объектам-исключениям (листинг 3.7).
Листинг 3.7. Файл iface/interfaces.php
<?php ## Классификация исключений.
interface IException {}
interface IInternalException extends IException {}
interface IFileException extends IInternalException {}
interface INetException extends IInternalException {}
interface IUserException extends IException {}
?>
Обратите внимание, что интерфейсы не содержат ни одного метода и свойства, а используются только для построения дерева классификации.
Теперь, если в программе имеется некоторый объект-исключение, чей класс реализует интерфейс INetException, мы также сможем убедиться, что он реализует и интерфейс IInternalException:
if ($obj instanceof IlnternalException) echo "Это внутренняя ошибка.";
Кроме того, если мы будем использовать конструкцию catch (InternalException ...), то сможем перехватить любое из исключений, реализующих интерфейсы IFileException и INetException.
Мы также "на всякий случай" задаем одного общего предка у всех интерфейсов — lException. Вообще говоря, это делать не обязательно.
Интерфейсы, конечно, не могут существовать сами по себе, и мы не можем создавать объекты типов IFileException (к примеру) напрямую. Необходимо определить классы, которые будут реализовывать наши интерфейсы (листинг 3.8).
Листинг 3.8. Файл iface/exceptions.php
<?php ## Классы-исключения.
require_once "interfaces.php";
// Ошибка: файл не найден.
class FileNotFoundException extends Exception
implements IFileException {}
// Ошибка: ошибка доступа к сокету.
class SocketException extends Exception
implements INetException {}
// Ошибка: неправильный пароль пользователя.
class WrongPassException extends Exception
implements IUserException {}
// Ошибка: невозможно записать данные на сетевой принтер.
class NetPrinterWriteException extends Exception
implements IFileException, INetException {}
// Ошибка: невозможно соединиться с SQL-сервером.
class SqlConnectException extends Exception
implements IInternalException, IUserException {}
?>
Обратите внимание на то, что исключение типа NetPrinterWriteException реализует сразу два интерфейса. Таким образом, оно может одновременно трактоваться и как файловое, и как сетевое исключение, и перехватываться как конструкцией catch (IFileException ...), так и catch (InetException ...).
За счет того, что все классы-исключения обязательно должны наследовать базовый тип Exception, мы можем, как обычно, проверить, является ли переменная объектом-исключением, или она имеет какой-то другой тип:
if ($obj instanceof Exception) echo "Это объект-исключение.";
Рассмотрим теперь пример кода, который использует приведенные выше классы (листинг3.9).
Листинг 3.9. Файл iface/test.php
<?php ## Использование иерархии исключений.
require_once "exceptions.php";
try {
printDocument();
} catch (IFileException $e) {
// Перехватываем только файловые исключения.
echo "Файловая ошибка: {$e->getMessage()}.<br>";
} catch (Exception $e) {
// Перехват всех остальных исключений.
echo "Неизвестное исключение: <pre>", $e, "</pre>";
}
function printDocument() {
$printer = "//./printer";
// Генерируем исключение типов IFileException и INetException.
if (!file_exists($printer))
throw new NetPrinterWriteException($printer);
}
?>
Результатом работы этой программы (в случае ошибки) будет строчка:
Ошибка записи в файл //./printer.
3.9 БЛОКИ-ФИНАЛИЗАТОРЫ
Как мы знаем, инструкция throw заставляет программу немедленно покинуть охватывающий try-блок, даже если при этом будет необходимо выйти из нескольких промежуточных функций (и даже вложенных try-блоков, если они есть). Такой "неожиданный" выход иногда оказывается нежелательным, и программист хочет написать код — финализатор, который бы выполнялся, например, при завершении функции в любом случае — независимо от того, как именно был осуществлен выход из блока.
3.9.1 Неподдерживаемая конструкция try...finally
В языках программирования Java и Delphi для реализации кода-финализатора имеется очень удобная конструкция try...finally, призванная гарантировать выполнение некоторых действий в случае возникновения исключения или внезапного завершения функции по return. На РНР это можно было бы записать так:
function eatThis() { throw new Exception("bang-bang!"); } function hello() {
echo "Все, что имеет начало, ";
try {
eatThis () ;
} finally {
echo "имеет и конец.";
}
echo "this never prints!"; }
// Вызываем функцию, hello() ;
Семантика инструкции try...finally должна быть ясна: она гарантирует выполнение finally-блока, даже если внезапно будет осуществлен выход из try-блока.
К сожалению, Zend Engine 2, на которой построен РНР 5, пока не поддерживает конструкцию try...finally, так что приведенный выше код, скорее всего, откажется работать. Почему "скорее всего"? Да потому, что есть все основания полагать, что рано или поздно инструкция finally в РНР появится, поскольку она очень удобна. Возможно, что инструкция finally уже появилась.
3.9.2 "Выделение ресурса есть инициализация"
Как же быть в случае, если нам нужно написать код, который будет обязательно выполнен при завершении работы функции? Единственная на данный момент возможность добиться этого — помещение такого кода в деструктор некоторого класса и создание объекта этого класса непосредственно в функции. Мы знаем, что при выходе из процедуры РНР автоматически уничтожает все переменные-ссылки, созданные внутри тела процедуры. Соответственно, если ссылка на объект будет единственной, то вызовется деструктор его класса. В листинге 3.3 мы уже рассматривали такой подход.
В соответствии с терминологией Страуструпа данный подход называют "выделение ресурса есть инициализация". Это объясняется вот чем: обычно в finally-блоках программы производится "освобождение" некоторых объектов-ресурсов, "выделенных" до момента возникновения исключения. Вызов конструктора объекта — это его инициализация.
Если работу с любыми ресурсами в программе реализовать через объекты, то необходимость в finally-блоках просто не возникнет. В самом деле, программа будет сама следить, когда нужно освободить тот или иной ресурс (вызвать деструктор соответствующего объекта), и нам не придется задумываться о явном написании кода освобождения.
3.9.3 Перехват всех исключений
Поскольку любой класс-исключение произволен от класса Exception, мы можем написать один-единственный блок-обработчик для всех возможных исключений в программе:
echo "Начало программы.<br>";
try {
eatThis ();
}
catch (Exception $e)
{
echo "Неперехваченное исключение: ", $e;
}
echo "Конец программы.<br>";
Таким образом, если в функции eatThis() возникнет любая исключительная ситуация, и объект-исключение "выйдет" за ее пределы (т. е. не будет перехвачен внутри самой процедуры), сработает наш универсальный код восстановления (оператор echo).
Перехват всех исключений при помощи конструкции catch (Exception ...) позволяет нам обезопаситься от неожиданного завершения работы функции (или блока) и гарантировать выполнение некоторого кода в случае возникновения исключения. В этом отношении конструкция очень похожа на инструкцию finally, которой в РНР на данный момент нет.
К сожалению, неожиданные вызовы return в функции при этом не обрабатываются, и отследить их пока нельзя.
Рассмотрим пример функции, которую мы пытались написать выше с использованием try...finally. Фактически, листинг 3.10 иллюстрирует, как можно проэмулировать finally в программе на РНР.
Листинг 3.10. Файл catchall.php
<?php ## Перехват всех исключений.
// Пользовательское исключение.
class HeadshotException extends Exception {}
// Функция, генерирующая исключение.
function eatThis() { throw new HeadshotException("bang-bang!"); }
// Функция с кодом-финализатором.
function action() {
echo "Все, что имеет начало, ";
try {
// Внимание, опасный момент!
eatThis();
} catch (Exception $e) {
// Ловим ЛЮБОЕ исключение, выводим текст...
echo "имеет и конец.<br>";
// ...а потом передаем это исключение дальше.
throw $e;
}
}
try {
// Вызываем функцию.
action();
} catch (HeadshotException $e) {
echo "Извините, вы застрелились: {$e->getMessage()}";
}
?>
В результате работы программы в браузере будет выведен следующий текст:
Все, что имеет начало, имеет и конец.
Извините, вы застрелились: bang-bang!
Как видите, код-финализатор в функции action() срабатывает "прозрачно" для вызывающей программы: исключение типа HeadsnotException не теряется, а выходит за пределы функции за счет повторного использования throw внутри catch-блока.
Такая техника вложенного вызова throw называется повторной генерацией исключения. Обычно ее применяют в случае, когда внутренний обработчик не может полностью обработать исключение, и его нужно передать дальше, чтобы ошибка была проанализирована в более подходящем месте.
3.10 ТРАНСФОРМАЦИЯ ОШИБОК
Мы разделили все ошибки на два вида:
● "несерьезные" - диагностические сообщения; перехватываются при помощи set_error_handier();
● "серьезные" - невозможно продолжить нормальный ход работы кода, представлены исключениями.
Мы также отмечали, что, эти два вида ошибок не пересекаются и в идеале должны обрабатываться независимыми механизмами (ибо имеют различные подходы к написанию кода восстановления).
Известно, что в программировании любая ошибка может быть усилена, по крайней мере, без ухудшения качества кода. Например, если заставить РНР немедленно завершать работу скрипта не только при обнаружении ошибок класса E_ERROR и E_PARSE (перехват которых вообще невозможен), но также и при возникновении E_WARNING и даже E_NOTICE, программа станет более "хрупкой" к неточностям во входных данных. Но зато программист будет просто вынужден волей-неволей писать более качественный код, проверяющий каждую мелочь при своей работе. Таким образом, качество написания кода при "ужесточении" реакции на ошибку способно только возрасти, а это обычно является большим достоинством.
3.10.1 Серьезность "несерьезных" ошибок
Что касается увеличения "хрупкости" при ужесточении реакции на ошибки, то это слишком неопределенная формулировка. Часто даже нельзя заранее предсказать, насколько тот или иной участок кода чувствителен к неожиданным ситуациям.
Для примера рассмотрим сообщение класса E_WARNING, возникающее при ошибке открытия файла. Является ли оно фатальным, и возможно ли дальнейшее выполнение программы при его возникновении без каких-либо ветвлений? Однозначного ответа на этот вопрос дать нельзя.
Вот две крайние ситуации.
● Невозможность открытия файла крайне фатальна. Например, пусть скрипт открывает какой-нибудь файл, содержащий программный код, который необходимо выполнить (такие ситуации встречаются при модульной организации сценариев). При невозможности запуска этого кода вся дальнейшая работа программы может стать попросту бессмысленной.
● Невозможность открытия файла практически ни на что не влияет. К примеру, программа может записывать в этот файл информацию о том, когда она была запущена. Или даже более простой пример: скрипт просто проверяет, существует ли нужный файл, а если его нет, то создает новый пустой.
Рассмотрим теперь самое "слабое" сообщение, класса E_NOTICE, которое генерируется РНР, например, при использовании неинициализированной переменной. Часто такие ошибки считают настолько незначительными, что даже отключают реакцию на них в файле php.ini (error_reporting=E_ALL~E_NOTICE). Более того, именно такое значение error_reporting выставляется по умолчанию в дистрибутиве PHP.
Нетрудно опять привести два примера крайних ситуаций, когда E_NOTICE играет очень важную роль и, наоборот, ни на что не влияет (на примере использования переменной или ячейки массива, которой ранее не было присвоено значение).
Предположим, вы исполняете SQL-запрос для добавления новой записи в таблицу MySQL:
INSERT INTO table (id, parent_id, text)
VALUES (NULL, '$pid', 'Have you ever had a dream, that you were so sure was real?')
В переменной $pid хранится некоторый идентификатор, который должен быть обязательно числовым. Если эта переменная окажется неинициализированной (например, где-то в программе выше произошла опечатка), будет сгенерирована ошибка E_NOTICE, а вместо $pid подставится пустая строка. SQL-запрос же все равно останется синтаксически корректным. В результате в базе данных появится запись с полем parent_id, равным нулю (пустая строка '' без всяких предупреждений трактуется MySQL как 0). Это значение может быть недопустимым для поля parent_id (например, если оно является внешним ключом для таблицы table, т. е. указывает на другую "родительскую" запись с определенным ID). А раз значение недопустимо, то целостность базы данных нарушена, и это в дальнейшем вполне может привести к серьезным последствиям (заранее непредсказуемым) в других частях скрипта, причем об их связи с одним-единственным E_NOTICE, сгенерированным ранее, останется только догадываться.
● Теперь о том, когда E_NOTICE может быть безвредной. Вот пример кода:
cinput type="text" name "field"
value="<?=htmlspecialchars($_REQUEST['field'])?>">
Очевидно, что если ячейка $_REQUEST['field'] не была инициализирована (например, скрипт вызван путем набора его адреса в браузере и не принимает никаких входных данных), элемент формы должен быть пуст. Подобная ситуация настолько широко распространена, что обычно ставят @ перед обращением к элементу массива, или даже перед htmlspecialchars(). В этом случае сообщение будет точно подавлено.
3.10.2 Преобразование ошибок в исключения
Мы приходим к выводу, что ошибку любого уровня можно трактовать как "серьезную" (за исключением ситуации, когда перед выражением явно указан оператор @, подавляющий вывод всех ошибок. Для обработки же серьезных ошибок в РНР имеется прекрасное средство — исключения.
Пример. Решение, которое мы здесь рассмотрим, — библиотека для автоматического преобразования всех перехватываемых ошибок РНР (вроде E_WARNING, E_NOTICE и т. д.) в объекты-исключения одноименных классов. Таким образом, если программа не сможет, например, открыть какой-то файл, теперь будет сгенерировано исключение, которое можно перехватить в соответствующем участке программы. Листинг 3.11 иллюстрирует сказанное.
Листинг 3.11. Файл w2e_simple.php
<?php ## Преобразование ошибок в исключения.
require_once "lib/config.php";
require_once "PHP/Exceptionizer.php";
// Для большей наглядности поместим основной проверочный код в функцию.
suffer();
// Убеждаемся, что перехват действительно был отключен.
echo "<b>Дальше должно идти обычное сообщение PHP.</b>";
fopen("fork", "r");
function suffer() {
// Создаем новый объект-преобразователь. Начиная с этого момента
// и до уничтожения переменной $w2e все перехватываемые ошибки
// превращаются в одноименные исключения.
$w2e = new PHP_Exceptionizer(E_ALL);
try {
// Открываем несуществующий файл. Здесь будет ошибка E_WARNING.
fopen("spoon", "r");
} catch (E_WARNING $e) {
// Перехватываем исключение класса E_WARNING.
echo "<pre><b>Перехвачена ошибка!</b>\n", $e, "</pre>";
}
// В конце можно явно удалить преобразователь командой:
// unset($w2e);
// Но можно этого и не делать - переменная и так удалится при
// выходе из функции (при этом вызовется деструктор объекта $w2e,
// отключающий слежение за ошибками).
}
?>
Обратите внимание на заголовок catch-блока. Он может поначалу ввести в заблуждение: ведь перехватывать можно только объекты-исключения, указывая имя класса, но никак не числовое значение (E_WARNING — вообще говоря, константа РНР, числовое значение которой равно 2 — можете убедиться в этом, запустив оператор echo E_WARNING). Тем не менее ошибки нет: E_WARNING — это одновременно и имя класса, определяемого в библиотеке PHP_Exceptionizer.
Заметьте также, что для ограничения области работы перехватчика используется уже знакомая нам идеология: "выделение ресурса есть инициализация". А именно в том месте, с которого необходимо начать преобразование, мы помещаем оператор создания нового объекта PHP_Exceptionizer и запоминаем последний в переменной, а там, где преобразование следует закончить, просто уничтожаем объект-перехватчик (явно или, как в примере, неявно, при выходе из функции).
3.10.3 Код библиотеки PHP_Exceptionizer
Прежде чем продолжить описание возможностей перехвата, давайте рассмотрим код класса PHP_Exceptionizer, реализующего преобразование стандартных ошибок РНР в исключения (листинг.3.12).
Листинг 3.12. Файл lib/PHP/Exceptionizer.php
<?php ## Класс для преобразования ошибок PHP в исключения.
/**
* Класс для преобразования перехватываемых (см. set_error_handler())
* ошибок и предупреждений PHP в исключения.
*
* Следующие типы ошибок, хотя и поддерживаются формально, не могут
* быть перехвачены:
* E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR,
* E_COMPILE_WARNING
*/
class PHP_Exceptionizer {
// Создает новый объект-перехватчик и подключает его к стеку
// обработчиков ошибок PHP (используется идеология "выделение
// ресурса есть инициализация").
public function __construct($mask=E_ALL, $ignoreOther=false) {
$catcher = new PHP_Exceptionizer_Catcher();
$catcher->mask = $mask;
$catcher->ignoreOther = $ignoreOther;
$catcher->prevHdl = set_error_handler(array($catcher, "handler"));
}
// Вызывается при уничтожении объекта-перехватчика (например,
// при выходе его из области видимости функции). Восстанавливает
// предыдущий обработчик ошибок.
public function __destruct() {
restore_error_handler();
}
}
/**
* Внутренний класс, содержащий метод перехвата ошибок.
* Мы не можем использовать для этой же цели непосредственно $this
* (класса PHP_Exceptionizer): вызов set_error_handler() увеличивает
* счетчик ссылок на объект, а он должен остаться неизменным, чтобы в
* программе всегда оставалась ровно одна ссылка.
*/
class PHP_Exceptionizer_Catcher {
// Битовые флаги предупреждений, которые будут перехватываться.
public $mask = E_ALL;
// Признак, нужно ли игнорировать остальные типы ошибок, или же
// следует использовать стандартный механизм обработки PHP.
public $ignoreOther = false;
// Предыдущий обработчик ошибок.
public $prevHdl = null;
// Функция-обработчик ошибок PHP.
public function handler($errno, $errstr, $errfile, $errline) {
// Если error_reporting нулевой, значит, использован оператор @,
// и все ошибки должны игнорироваться.
if (!error_reporting()) return;
// Перехватчик НЕ должен обрабатывать этот тип ошибки?
if (!($errno & $this->mask)) {
// Если ошибку НЕ следует игнорировать...
if (!$this->ignoreOther) {
if ($this->prevHdl) {
// Если предыдущий обработчик существует, вызываем его.
$args = func_get_args();
call_user_func_array($this->prevHdl, $args);
} else {
// Иначе возвращаем false, что вызывает запуск встроенного
// обработчика PHP.
return false;
}
}
// Возвращаем true (все сделано).
return true;
}
// Получаем текстовое представление типа ошибки.
$types = array(
"E_ERROR", "E_WARNING", "E_PARSE", "E_NOTICE", "E_CORE_ERROR",
"E_CORE_WARNING", "E_COMPILE_ERROR", "E_COMPILE_WARNING",
"E_USER_ERROR", "E_USER_WARNING", "E_USER_NOTICE", "E_STRICT",
);
// Формируем имя класса-исключения в зависимости от типа ошибки.
$className = __CLASS__ . "_" . "Exception";
foreach ($types as $t) {
$e = constant($t);
if ($errno & $e) {
$className = $t;
break;
}
}
// Генерируем исключение нужного типа.
throw new $className($errno, $errstr, $errfile, $errline);
}
}
/**
* Базовый класс для всех исключений, полученных в результате ошибки PHP.
*/
abstract class PHP_Exceptionizer_Exception extends Exception {
public function __construct($no=0, $str=null, $file=null, $line=0) {
parent::__construct($str, $no);
$this->file = $file;
$this->line = $line;
}
}
/**
* Создаем иерархию "серьезности" ошибок, чтобы можно было
* ловить не только исключения с указанием точного типа, но
* и сообщения, не менее "фатальные", чем указано.
*/
class E_EXCEPTION extends PHP_Exceptionizer_Exception {}
class AboveE_STRICT extends E_EXCEPTION {}
class E_STRICT extends AboveE_STRICT {}
class AboveE_NOTICE extends AboveE_STRICT {}
class E_NOTICE extends AboveE_NOTICE {}
class AboveE_WARNING extends AboveE_NOTICE {}
class E_WARNING extends AboveE_WARNING {}
class AboveE_PARSE extends AboveE_WARNING {}
class E_PARSE extends AboveE_PARSE {}
class AboveE_ERROR extends AboveE_PARSE {}
class E_ERROR extends AboveE_ERROR {}
class E_CORE_ERROR extends AboveE_ERROR {}
class E_CORE_WARNING extends AboveE_ERROR {}
class E_COMPILE_ERROR extends AboveE_ERROR {}
class E_COMPILE_WARNING extends AboveE_ERROR {}
class AboveE_USER_NOTICE extends E_EXCEPTION {}
class E_USER_NOTICE extends AboveE_USER_NOTICE {}
class AboveE_USER_WARNING extends AboveE_USER_NOTICE {}
class E_USER_WARNING extends AboveE_USER_WARNING {}
class AboveE_USER_ERROR extends AboveE_USER_WARNING {}
class E_USER_ERROR extends AboveE_USER_ERROR {}
// Иерархии пользовательских и встроенных ошибок не сравнимы,
// т.к. они используются для разных целей, и оценить
// "серьезность" нельзя.
?>
Перечислим достоинства описанного подхода.
● Ни одна ошибка не может быть случайно пропущена или проигнорирована. Программа получается более "хрупкой", но зато качество и "предсказуемость" поведения кода сильно возрастают.
● Используется удобный синтаксис обработки исключений, гораздо более "прозрачный", чем работа с set_error_handler(). Каждый объект-исключение дополнительно содержит информацию о месте возникновения ошибки, а также сведения о стеке вызовов функций; все эти данные можно извлечь с помощью соответствующих методов класса Exception.
● Можно перехватывать ошибки выборочно, по типам, например, отдельно обрабатывать сообщения E_WARNING и отдельно — E_NOTICE.
● Возможна установка "преобразователя" не для всех разновидностей ошибок, а только для некоторых из них (например, превращать ошибки E_WARNING в исключения класса E_WARNING, но "ничего не делать" с E_NOTICE).
● Классы-исключения объединены в иерархию наследования, что позволяет при необходимости перехватывать не только ошибки, точно совпадающие с указанным типом, но также заодно и более "серьезные".
3.10.4 Иерархия исключений
Остановимся на последнем пункте приведенного выше списка. Взглянув еще раз в конец листинга 3.12, вы можете обнаружить, что классы-исключения объединены в довольно сложную иерархию наследования. Главной "изюминкой" метода является введение еще одной группы классов, имена которых имеют префикс Above. При этом более "серьезные" Above-классы ошибок являются потомками всех "менее серьезных". Например, AboveERROR, самая "серьезная" из ошибок, имеет в "предках" все остальные Above-классы, a AboveE_STRICT, самая слабая, не наследует никаких других Above-классов. Подобная иерархия позволяет нам перехватывать ошибки не только с типом, в точности совпадающим с указанным, но также и более серьезные.
Например, нам может потребоваться перехватывать в программе все ошибки класса E_USER_WARNING и более фатальные E_USER_ERROR. Действительно, если мы заботимся о каких-то там предупреждениях, то уж конечно должны позаботиться и о серьезных ошибках. Мы могли бы поступить так:
try {
// генерация ошибки
} catch (E_USER_WARNING $e) {
// код восстановления
} catch (E_USER_ERROR $e) {
// точно такой же код восстановления — приходится дублировать
}
Сложная иерархия исключений позволяет нам записать тот же фрагмент проще и понятнее (листинг3.13).
Листинг 3.13. Файл w2e_hier.php
<?php ## Иерархия ошибок.
require_once "lib/config.php";
require_once "PHP/Exceptionizer.php";
suffer();
function suffer() {
$w2e = new PHP_Exceptionizer(E_ALL);
try {
// Генерируем ошибку.
trigger_error("Damn it!", E_USER_ERROR);
} catch (AboveE_USER_WARNING $e) {
// Перехват ошибок: E_USER_WARNING и более серьезных.
echo "<pre><b>Перехвачена ошибка!</b>\n", $e, "</pre>";
}
}
?>
3.10.5 Фильтрация по типам ошибок
Использование механизма обработки исключений подразумевает, что после возникновения ошибки "назад ходу нет": управление передается в catch-блок, а нормальный ход выполнения программы прерывается. Возможно, вы не захотите такого поведения для всех типов предупреждений РНР. Например, ошибки класса E_NOTICE иногда не имеет смысла преобразовывать в исключения и делать их, таким образом, излишне фатальными.
Тем не менее, в большинстве случаев E_NOTICE свидетельствует о логической ошибке в программе и может рассматриваться, как тревожный сигнал программисту. Игнорирование таких ситуаций чревато проблемами при отладке, поэтому на практике имеет смысл преобразовывать в исключения и E_NOTICE тоже.
Вы можете указать в первом параметре конструктора PHP_Exceptionizer, какие типы ошибок необходимо перехватывать. По умолчанию там стоит E_ALL (т. е. перехватывать все ошибки и предупреждения), но вы можете задать и любое другое значение (например, битовую маску E_ALL ~ E_NOTICE ~ E_STRICT), если пожелаете.
Существует еще и второй параметр конструктора. Он указывает, что нужно делать с сообщениями, тип которых не удовлетворяет битовой маске, приведенной в первом параметре. Можно их либо обрабатывать обычным способом, т. е. передавать ранее установленному обработчику (false), либо же попросту игнорировать (true).
Напомним, что в РНР 5 функция set_error_handler() принимает второй необязательный параметр, в котором можно указать битовую маску "срабатывания" обработчика. А именно для тех типов ошибок, которые "подходят" под маску, будет вызвана пользовательская функция, а для всех остальных— стандартная, встроенная в РНР. Класс PHP_Exceptionizer ведет себя несколько по-другому: в случае несовпадения типа ошибки с битовой маской будет вызван не встроенный в РНР обработчик, а предыдущий назначенный (если он имелся). Таким образом, реализуется стек перехватчиков ошибок. В ряде ситуаций это оказывается более удобным.
3.10.6 Перспективы
По неофициальным данным, в РНР версии 5.1 (и старше) разработчики планируют реализовать встроенный механизм преобразования ошибок в исключения. Для этого, предположительно, будет использоваться инструкция declare, позволяющая задавать блоку программы различные свойства (в том числе, что делать при возникновении ошибки). Код перехвата может выглядеть, например, так:
// Включаем "исключительное" поведение ошибок в РНР.
declare(exception_map='+standard:streams:*') {
try {
//В действительности генерируется исключение, а не предупреждение.
fopen("spoon", 'r');
} catch (Exception $e) {
if ($e->getCode() = = 'standard:streams:E_NOENT ') {
echo "Ложка не существует!";
}
}
}
// При выходе из declare-блока предыдущие свойства восстанавливаются.
К сожалению, в РНР версии 5.0 ничего подобного нет. Проверьте, возможно, данная функциональность появилась в вашей версии интерпретатора (см. документацию на инструкцию declare по адресу http://php.net/dedare).
ЗАКЛЮЧЕНИЕ
Рассмотрена одна из самых важных и популярных при программировании задач — обработка ошибок в коде программы. Уточнено понятие термина "ошибка" и определена его роль в программировании, а также приведены различные классификации ошибочных ситуаций. Представлено описание понятия "исключение" и способы использования конструкции try...catch, а также описаны некоторые особенности ее работы в РНР. Описан механизм наследования и классификации исключений, использование которого может сильно сократить код программы и сделать его универсальным. Представлен код библиотеки, позволяющей обрабатывать многочисленные ошибки и предупреждения, генерируемые функциями РНР, как обыкновенные исключения.
Грамотный перехват ошибок с самого зарождения программирования считался трудной задачей. Механизм обработки исключений, хотя и упрощает ее, но все равно остается весьма сложным.
ЛИТЕРАТУРА
1. Скляр Д., Трахтенберг А. PHP. Сборник рецептов. – Пер. с англ. – СПб: Символ – Плюс, 2005. – 627 с., ил.
2. Котеров Д., Костарев А. PHP5 в подлиннике. – СПб: Символ – Плюс, 2005. – 1120 с., ил.
3. Дюбуа П. MySQL. Сборник рецептов. – Пер. с англ. - СПб: Символ – Плюс, 2004. – 1056 с., ил.
4. Томсон Лаура, Веллинг Люк. Разработка web – приложений на PHP и MySQL. – Пер. с англ. – СПб: ООО «ДиаСофтЮП», 2003. 672 с., ил.