Сразу говорю: поклонникам такой глубокой старины, как PHP4, обработка исключений не светит, т.к. работает только на PHP5.
Чего такого уникального и полезного несет обработка исключений? По сути, ничего. Все что оно позволяет сделать, можно реализовать и старыми методами. Другое дело, что исключения позволяют делать это гораздо быстрее и элегантнее.
Сначала кратко рассмотрим технические аспекты, которые вы можете более подробно узнать из
документации. Классический пример:
try
{
if (!
mysql_connect('xxx',
'xxx',
'xxx')) {
throw
new Exception
('Не коннектиццо');
}
if (!
mysql_select_db('xxx')) {
throw
new Exception
('Законектились, но не нашли БД');
}
} catch
(Exception
$e) {
print $e->
getMessage();
exit();
}
mysql_query('...');
В примере осуществляется подключение к базе данных и выполнение запроса. Так же при этом производится обработка исключительных ситуаций. Исключительная ситуация, это ситуация при которой дальнейшая корректная работа программы невозможна, без специальной обработки этой ситуации. В контексте приведенного примера исключительной ситуацией является невозможность подключения к БД, при этом, как понимаете, дальнейшее использование mysql_query() вряд ли будет корректным.
В примере осуществляется генерация (бросок) и перехват исключения. Генерация осуществляется с помощью инструкции:
throw new Exception('Не коннектиццо'); // Создание объекта класса Exception и выброска его в качестве исключения
Перехват осуществляется с помощью блоков try-catch. Если выполняемый внутри блока try код выбрасывает исключение, дальнейшее выполнение передается блоку catch, который осуществляет обработку.
В случае невозможности подключения к серверу, выбрасывается исключение и управление, минуя попытку выбора базы, передается блоку catch. Обработчик исключения банален: вывод сообщения и завершение сценария. Если блок try отработал нормально (подключились и выбрали базу), управление не попадает в catch, а сразу идет дальше - к выполнению запроса.
Можно не использовать блоки try-catch:
if (!
mysql_connect('xxx',
'xxx',
'xxx')) {
throw
new Exception
('...');
}
Данное исключение не будет обработано и вызовет завершение сценария с выводом сообщения.
Смысл всего этого
Классический пример он на то и классический, что абсолютно ничего не объясняет. Все тоже самое гораздо легче делается старым дедовским методом:
Однако, важнейшим свойством перехвата исключений является то, что внутри блока try могут быть вызовы функций. И все исключения выброшенные внутри них, могут быть перехвачены этим блоком (если не были перехвачены внутри этих самых функций).
Например, займемся любимым делом всех php-программистов - напишем собственный класс для работы с БД. Ну и первым методом у него, конечно, будет подключение к базе.
class DB
{
/**
* Подключение к базе данных
* Параметры подключения жестко заданы в теле метода
*
* @exception Exception невозможно подключение или выбор базы
*/
public static function connect
()
{
self::
$linkId =
mysql_connect('xxx',
'xxx',
'xxx');
if (self::
$linkId ===
false) {
throw
new Exception
('Нет соединения');
}
if (!
mysql_select_db('xxx', self::
$linkId)) {
throw
new Exception
('Нет базы');
}
return true;
}
/**
* Идентификатор подключения
*
* @var Resource
*/
private static $linkId;
}
Класс DB, это библиотека. Мы её напишем, задокумментируем и положим в папочку для библиотек, либо выложим в интернет для бесплатного использования всеми, кто пожелает. А может платного.
Данная библиотека может быть использована в совершенно различных системах, сайтах и т.п. Мы не можем внутри неё вызвать die() - с какой стати библиотека будет убивать весь сценарий? Мы так же не можем осуществить практически никакой другой обработки - вывода сообщения, попытки подключиться под другими параметрами, записи в журнал логов. Потому что мы не знаем структуры системы в которой этот класс будет использован.
Да мы и не обязаны ничего этого делать. Наше дело подключиться к базе. Не получилось - выкидываем исключение. Как его обрабатывать, это дело вызывающего сценария. Например:
try
{
DB::
connect();
} catch
(Exception
$e) {
print 'Хм, ошибка вышла';
exit();
}
Отличие от разбора результата
Раньше (да часто и сейчас) подобные вещи реализовывались путем возвращения определенного значения сигнализирующего об ошибке (обычно false) и разбора его в вызвавшем коде.
Однако, исключения по сравнению с данным методом имеют следующие преимущества:
1. Это более правильно :) . Результат функции, это результат функции, а ошибка, это ошибка. Функция может возвращать любое значение (например, unserialize) и какое из них считать ошибочным непонятно. Так же простой false не несет никакой информации об ошибки, если такая информация может понадобиться, приходится прибегать к передаваемым по ссылке аргументам (см. errno и errstr в
fSockOpen()).
2. При использовании метода с false программист
обязан каждый раз проверять результаты подобных функций и обрабатывать ошибки. Если программист забыл или поленился (а забывают и ленятся программисты регулярно), то последствия могут быть печальными. При использовании же исключений ситуация прямо противоположная. Исключительные ситуации в случае по умолчанию приводят к завершению сценария с выводом сообщения об ошибке. И только в том конкретном случае, где разработчик озаботился обработкой ошибки, возможна дальнейшая работа.
3. Разматывание стека функций
Разматывание стека функций
Как должно быть известно, функции могут вызывать другие функции, те в свою очередь третьи и т.п. При этом в программе образуется
стек функций. В случае возникновения исключения вне блока try, текущая функция завершается и управление передается обратно в вызвавшую. Если вызов функции происходил в блоке try - происходит обработка, если вне - данная функция также завершается и управление передается уже в вызвавшую её. Таким образом стек функций
разматывается назад в поисках блока-перехватчика (try). Если домотали до самого верха, не встретив try, происходит обработка по умолчанию - завершение сценария с выводом сообщения об ошибке.
Допустим, пишем мы дальше наш класс DB и задумываемся: а зачем нам явно вызывать метод connect()? И лишнее действие и с базой мы не так активно работаем - бывают сценарии, где вообще база не нужна и подключение будет лишней тратой времени и ресурсов. Подключение нам нужно только при запросе, так давайте его в запрос и вынесем:
/**
* Выполнение запроса
*
* @param string $query запрос
* @return resource ответ
* @exception Exception нет коннекта
* @exception Exception ошибка в запросе
*/
public static function query
($query)
{
if (!self::
$linkId) {
self::
connect();
// Подключаемся, если еще не подключены
}
$res =
mysql_query($query);
if (mysql_errno()) {
throw
new Exception
('Error "'.
(htmlSpecialChars($query)).
'":'.
htmlSpecialChars(mysql_error()));
}
return $res;
}
Здесь еще один throw - генерация исключения при неверном запросе.
А что произойдет при неудачном подключении? Метод connect() выбросит исключение, которое не будет обработано в нем самом (нет try). Connect() завершится и управление вернется в вызвавший его query(). Т.к. здесь так же нету try, управление выйдет и отсюда и передастся коду, вызвавшему query().
Таким образом query() не приходится заморачиваться над обработкой исключений в вызываемых функциях. При использовании false-результата, код query должен был быть таким:
if (!self::$linkId) {
if (!self::connect()) {
return false;
}
}
В то же время код, вызвавший query может быть тоже неосновным - вызываемым другим методом, которому так же не интересны произошедшие ошибки. Да например, это метод selectRow того же класса DB, выбирающий строку по заданному id и вызывающий для этого query(). Добавьте сюда необходимость идентификации типа ошибки и сохранения дополнительной информации о ней и получите столько лишнего геморроя, сколько вам и не снилось. С помощью же исключений все решается элементарно.
Пользовательские типы исключений
В примерах мы использовали в качестве объекта-исключения объект встроенного класса Exception. Однако мы можем наследовать от него свой класс и использовать его. Одно из преимуществ такого подхода - возможность обработки различных типов исключений отдельно друг от друга.
Наследуем (без определения дополнительных методов) два класса исключений - ошибка подключения и ошибка в запросе:
class ExceptionDBConnect extends Exception {}
class ExceptionDBQuery extends Exception {}
Теперь при неудачном подключении будем кидать исключение определенного типа:
self::
$linkId =
mysql_connect('xxx',
'xxx',
'xxx');
if (self::
$linkId ===
false) {
throw
new ExceptionDBConnect
('');
}
При ошибочном запросе, соответственно, используем throw new ExceptionDBQuery()
Допустим, причина ошибки при запросе может быть одна - синтаксическая ошибка в SQL. Данную ошибку не следует перехватывать, т.к. она должна быть отображена программисту и исправлена еще на этапе разработки. А вот невозможность подключения к БД - вполне возможная ошибка уже на этапе действующего проекта. Мы должны по этому поводу вывести какое-то сообщение посетителям и, возможно, послать письмо админу.
try {
DB::query('...');
} catch (ExceptionDBConnect $e) {
// Обработка ошибки подключения
}
Таким образом мы перехватываем исключения типа ExceptionDBConnect, все же другие исключения не обрабатываются, они либо вызывают завершение сценария, либо проваливаются ниже, если есть куда.
Можно и по другому:
try {
DB::query('...');
} catch (ExceptionDBConnect $e) {
// Обработка ошибки подключения
} catch (ExceptionDBQuery $e) {
// Обработка ошибки в запросе
} catch (Exception $e) {
// Обработка всех общих исключений,
// а так же всех типов исключений наследуемых от Exception.
// Т.е. абсолютно всех исключений, не обработанных в предыдущих блоках catch.
}
Кроме перехватов различных типов исключений, мы можем дополнить свой класс исключения дополнительной функциональностью. Методы реализованные в стандартном классе Exception можно найти в
доке. При генерации исключения при ошибочном запросе, нам приходилось формировать строку из запроса и ошибки, чтобы вывести наиболее полную информацию. Сделаем это более удобно:
class ExceptionDBQuery
extends Exception
{
public function __construct
($query,
$errNo,
$errString) {
$this->
query =
$query;
$this->
errNo =
$errNo;
$this->
errString =
$errString;
parent::__construct
('',
0);
}
public function getQuery
() { return $this->
query;
}
public function getErrNo
() { return $this->
errNo;
}
public function getErrString
() { return $this->
errString;
}
public function __toString
()
{
return
'Ошибка в запросе "'.
htmlSpecialChars($this->
query).
'". '.
'Код ошибки: '.
$this->
errNo.
'. '.
'Описание: "'.
htmlSpecialChars($this->
errString).
'".';
}
private $query;
private $errNo;
private $errString;
}
Генерируем исключение теперь следующим образом:
При обработке исключения данного типа, мы можем получить строку запроса и информацию об ошибках с помощью методов типа $e->getQuery(). Метод __toString() формирует стандартное сообщение об ошибке.
Перехват всех исключений в программе
Можно вести журнал всех фатальных исключений в программе. Однако, вместо того, чтобы обрабатывать каждое возможное исключение, писать лог и завершать сценарий, можно просто поставить перехват исключений на всю программу:
/**
* Исполняемый сценарий
*/
try {
/* Изначальный код сценария */
require_once('...');
require_once('...');
$app = new Application();
$app->run();
} catch (Exception $e) {
// Логирование ошибки
}
Или мы хотим в случае неверного запроса к базе выводить сообщение в специальной табличке. Можно обрабатывать каждый запрос, а можно:
try {
// Код программы
} catch (ExceptionDBQuery $e) {
// Вывод информации
}
Использование исключений
Исключения впервые были реализованы в компилируемых языках. Там они не получили слишком широкого применения, т.к. требуют генерации большого количества дополнительного кода и снижают скорость выполнения. В интерпретируемых языках, типа PHP, на скорость выполнения исключения практически не влияют. Однако, в PHP они используются все-таки не так сильно, как этого заслуживают.
Гораздо активнее исключения использует, например, Python, практически на каждом шагу. Например, функция открытия файла в нем не возвращает никаких false при ошибке, а генерирует исключение. И правильно - в случае отсутствия файла сценарий обычно завершается и только, если программист решил явно обработать подобную ситуацию возможно продолжение программы.