Рефераты
 

Обработка ошибок в коде программ РНР

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 с., ил.

Страницы: 1, 2


© 2010 BANKS OF РЕФЕРАТ