ФорумПрограммированиеПыхнуть хотите?F.A.Q. → Обработка исключений в PHP

Обработка исключений в PHP

  • vasa_c

    Сообщения: 3131 Репутация: N Группа: в ухо

    Spritz Авг. 14, 2007, 12:50 п.п.

    Сразу говорю: поклонникам такой глубокой старины, как 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('…');
    }

    Данное исключение не будет обработано и вызовет завершение сценария с выводом сообщения.


    Смысл всего этого

    Классический пример он на то и классический, что абсолютно ничего не объясняет. Все тоже самое гораздо легче делается старым дедовским методом:
    mysql_connect('xxx', 'xxx', 'xxx') or die('Ошибка');


    Однако, важнейшим свойством перехвата исключений является то, что внутри блока 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) и разбора его в вызвавшем коде.


    if (mysql_connect() === false) {
    die('Ошибка');
    }
    mysql_query('…');


    Однако, исключения по сравнению с данным методом имеют следующие преимущества:

    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;

    }


    Генерируем исключение теперь следующим образом:

    if (mysql_errno()) {
    throw new Exception($query, mysql_errno(), mysql_error());
    }


    При обработке исключения данного типа, мы можем получить строку запроса и информацию об ошибках с помощью методов типа $e->getQuery(). Метод __toString() формирует стандартное сообщение об ошибке.


    Перехват всех исключений в программе

    Можно вести журнал всех фатальных исключений в программе. Однако, вместо того, чтобы обрабатывать каждое возможное исключение, писать лог и завершать сценарий, можно просто поставить перехват исключений на всю программу:


    /**
    * Исполняемый сценарий
    */
    try {
    /* Изначальный код сценария */
    require_once('…');
    require_once('…');
    $app = new Application();
    $app->run();
    } catch (Exception $e) {
    // Логирование ошибки
    }


    Или мы хотим в случае неверного запроса к базе выводить сообщение в специальной табличке. Можно обрабатывать каждый запрос, а можно:

    try {
    // Код программы
    } catch (ExceptionDBQuery $e) {
    // Вывод информации
    }



    Использование исключений

    Исключения впервые были реализованы в компилируемых языках. Там они не получили слишком широкого применения, т.к. требуют генерации большого количества дополнительного кода и снижают скорость выполнения. В интерпретируемых языках, типа PHP, на скорость выполнения исключения практически не влияют. Однако, в PHP они используются все-таки не так сильно, как этого заслуживают.
    Гораздо активнее исключения использует, например, Python, практически на каждом шагу. Например, функция открытия файла в нем не возвращает никаких false при ошибке, а генерирует исключение. И правильно - в случае отсутствия файла сценарий обычно завершается и только, если программист решил явно обработать подобную ситуацию возможно продолжение программы.
  • SVat

    Сообщения: 23 Репутация: N Группа: Кто попало

    Spritz Июнь 6, 2012, 10:56 д.п., спустя 1757 дней 22 часа 6 минут


    Кроме перехватов различных типов исключений, мы можем дополнить свой класс исключения дополнительной функциональностью. Методы реализованные в стандартном классе 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;

    }


    Генерируем исключение теперь следующим образом:

    if (mysql_errno()) {
    throw new Exception($query, mysql_errno(), mysql_error());
    }



    Опечатка! Должно быть:

    if (mysql_errno()) {
        throw new ExceptionDBQuery($query, mysql_errno(), mysql_error());
    }

Пожалуйста, авторизуйтесь, чтобы написать комментарий!