# Как использовать исключения в PHP

Если ты изучаешь ООП, то наверняка слышал про *исключения*.  В [мануале PHP](http://php.net/manual/ru/language.exceptions.php) описаны конструкции `try`/`catch`/`throw` и `finally` (доступна только в PHP  5.5 и выше), но не объясняется, как их использовать и зачем они придуманы. 

А придуманы они были как удобный способ обработки ошибок. 

Для примера представим, что мы пишем программу для вывода списка пользователей из файла на экран.  Допустим, код выглядит как-то так: 

```php
$file = './users.csv';

// Загружаем список пользователей из файла в массив
$users = loadUsersFromFile($file); 

// Выводим
foreach ($users as $user) {
    echo "{$user['name']} набрал {$user['score']} очков\n";
}
```

Все ли тут верно?  Не все.  Мы забыли сделать обработку ошибок.  Файла может не существовать, к нему может не быть доступа, данные в нем могут быть в неверном формате.  Хорошая программа, разумеется, должна обрабатывать такие ситуации и выводить соответствующее сообщение.  

Самый плохой (но простой) вариант — поместить код обработки и вывода ошибки в функцию `loadUsersFromFile()`:

```php 
/**
 * Загружает список пользователей из файла и возвращает
 * массив с ними.  При ошибке выводит сообщение и завершает
 * программу. 
 */
function loadUsersFromFile(string $file): array {
    // Файла не существует — ошибка
    if (! file_exists($file)) {
        die("Ошибка: файл $file не существует\n");
    }

    ....
}
```

Этот вариант плохой, так как тот, кто вызывает функцию `loadUsersFromFile`, не может никак повлиять на обработку ошибок.  Может быть, он хочет при ошибке вывести другое сообщение или хочет попробовать поискать файл в другом месте.  Но наша функция просто завершает программу, не оставляя никакого выбора.  

Правильнее будет разделить ответственность: функция лишь возвращает информацию об ошибке, а тот, кто её вызвал - решает, что с ней делать.  

Для этого мы можем переделать функцию, чтобы она возвращала массив из 2 элементов: если данные загружены, то элемент `success` содержит `true`, а элемент `users` содержит массив пользователей.  Если же произошла ошибка, то в `success` будет находиться `false`, а в элементе `error` - текст ошибки. 

```php
/**
 * Загружает данные пользователей из файла.  Возвращает
 * массив с ключами: 
 * 
 * - success - true при успехе, false при ошибке
 * - error - присуствует в случае ошибки, содержит текст ошибки
 * - users - присутствует в случае успеха, содержит массив с данными
 */
function loadUsersFromFile(string $file): array {
    // Файла не существует — ошибка
    if (! file_exists($file)) {
        return [
            'success'   =>  false,
            'error'     =>  "файл $file не существует"
        ];
    }

    .... загружаем информацию о пользователях ....

    return [
        'success'   =>  true,
        'users'    =>  $users
    ];
}
```

Конечно, мы должны поменять и код, который вызывает функцию: 

```php
....
// Загружаем список пользователей в массив
$result = loadUsersFromFile($file); 

// можно еще писать if (! $result['success'])
if ($result['success'] === false) {  
    // Выводим текст ошибки
    die("Не удалось вывести список пользователей из-за ошибки: {$result['error']}\n");
}

$users = $result['users'];
...
```

Код стал гораздо лучше.  Теперь можно обрабатывать ошибки так, как хочется.  Но за это нам пришлось заплатить увеличением объема кода: мы должны после каждого вызова функции писать `if` и проверять, успешно ли она выполнилась.  Когда функций много, и каждая может вернуть ошибку, код начинает наполовину состоять из таких проверок.  

## Выбрасываем исключение

В качестве альтернативного подхода были придуманы исключения.  Если функция по какой-то причине не может выполнить свою работу, то она создает объект исключения, содержащий подробности ошибки и выбрасывает его командой `throw`:

```php
if (! file_exists($file)) {
    throw new Exception("Ошибка: файл $file не существует");
}
```

Исключение — это объект любого класса, который [реализует встроенный в PHP интерфейс](../php/interfaces.md) [Throwable](http://php.net/manual/en/class.throwable.php) (реализует - значит, содержит описанные в интерфейсе методы).  Предполагается, что для каждого типа ошибки создаются свои классы исключений, что позволяет потом их различать.  

До PHP7, класс исключения надо было унаследовать от встроенного в PHP класса [Exception](http://php.net/manual/ru/class.exception.php), а в новой версии такого требования больше нет.  Однако, напрямую реализовать интерфейс `Throwable` все равно нельзя, и придется создавать свои исключения на основе стандартного класса.  Также, в PHP есть другие классы исключений, которые могут пригодиться: <http://php.net/manual/ru/spl.exceptions.php>

Объект исключения хранит в себе подробности ошибки.  В простейшем случае это текст ошибки, но можно делать дополнительные поля и методы.  Например, исключение FileReadException, описывающее ошибку чтения из файла, может содержать поле с именем файла.  Объект исключения обычно неизменен: все значения полей задаются конструктором в момент создания. 

В сообщении стоит указать все подробности ошибки.  Например, при ошибке чтения файла полезно указать имя файла.  Это упростит отладку кода. 

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

Вот пример класса-исключения.  Он унаследован от встроенного в PHP класса: 

```php
/**
 * Ошибка при чтении файла конфигурации
 */
class ConfigException extends \Exception
{
    // Ничего не добавляем
}
```

Исключение по умолчанию (если оно не перехватывается) выходит из всех вызовов функций до самого верха и завершает программу, выводя сообщение об ошибке (но если в конфигурации PHP стоит display_errors = off, то сообщение не выводится).  Таким образом, если ты не перехватываешь исключения, то все равно увидишь причину ошибки (а если у тебя установлено расширение `xdebug`, то еще и стектрейс — цепочку вызовов функций, внутри которых оно произошло).  Код становится проще: 

```php
// Если тут произойдет исключение, оно само завершит программу
// потому у нас нет необходимости проверять результат
$users = loadUsersFromFile($file);
```

Таким образом, каждая функция либо возвращает результат, либо выбрасывает исключение.  

Вот пример, показывающий работу исключений: 

```php
function a() 
{
    echo "В начале a\n";
    b();
    echo "В конце a\n";
}

function b()
{
    echo "В начале b\n";
    throw new Exception("Ошибка в функции b()");
    echo "В конце b\n";
}

// Функция a() вызывает b(), которая выбрасывает исключение.  Исключение выходит
// из функции b() наверх в функцию a(), выходит из нее и, оказавшись на верхнем 
// уровне, завершает программу сообщением об ошибке. 
a();
echo "В конце программы\n";
```

Если запустить эту программу ([код на ideone](https://ideone.com/yfzVAg)), то она выведет: 

```
В начале a
В начале b
PHP Fatal error:  Uncaught Exception: Ошибка в функции b() in /home/XMoMDv/prog.php:13
```

В данном примере кода исключение мгновенно выйдет из всех вложенных вызовов функций и завершит программу.  И это правильно, так как незачем выполнять программу, где есть ошибки.  Это называется [принципом fail fast (статья на Хабре)](http://habrahabr.ru/post/218325/). 

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

## Поведение PHP по умолчанию

Если в программе возникло исключение, и мы его не перехватили, то оно завершает программу.  Если программа была запущена в командной строке, то PHP выведет содержащийся в исключении текст ошибки в консоль вместе с *стектрейсом* (распечаткой стека) - списком вызовов функций, которые привели к исключению. 

Если же PHP используется в веб-сервере, то его поведение зависит от настройки [display_errors](http://php.net/manual/ru/errorfunc.configuration.php#ini.display-errors).  Если она равна 1 или `on`, то при исключении в браузер будет выведен текст исключения и распечатка стека.  Это полезно на машине разработчика, но недопустимо использовать на реальных серверах - чтобы злоумышленник не мог извлечь какую-то информацию об устройстве приложения.  На них ставится настройка display_errors = 0 (или `off`) и при исключении показывается просто пустая страница.  Также, в обоих случаях текст исключения записывается в *лог (журнал) ошибок* веб-сервера.  Журнал помогает разработчику увидеть, какие ошибки происходили на сайте за последнее время. 

Белая страница - это не очень красиво, и ее могут проиндексировать поисковые роботы.  Потому обычно в веб-приложениях ловят все исключения на верхнем уровне и показывают «страницу ошибки», а для роботов добавляют специальный HTTP-код ответа, говорящий о проблеме.  Ниже будет описано, как это сделать. 

## Ловим исключения

Если мы хотим как-то обработать ошибку, то мы можем "ловить" выброшенные исключения.  Например, при ошибке скачивании файла по сети можно сделать паузу и повторить попытку.  

Перехватывать исключения можно двумя способами: неструктурно и структурно. ** Неструктурно** — это когда мы задаем обработчик исключений в начале программы: 

```php
set_exception_handler(function (Throwable $exception) {
    // Функция будет вызвана, если исключение не будет
    // поймано и завершит программу. 
    // 
    // Она может записать исключение в журнал и вывести 
    // страницу ошибки. 
    error_log($exception->__toString());

    header("HTTP/1.0 503 Temporary unavailable");
    header("Content-type: text/plain; charset=utf-8");
    echo "Извините, на сайте произошла ошибка.\n";
    echo "Попробуйте перезагрузить страницу.\n";
});
```

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

Обрати внимание, мы указали в функции-обработчике, что аргумент `$exception` реализует интерфейс `Throwable`.  До PHP7 надо было указывать в тайп-хинте базовый класс исключений - `Exception`.  Чтобы код работал в обоих версиях PHP, придется отказаться от тайп-хинта. 

**Структурная** обработка исключений - это отлов исключений определенных типов в указанном месте кода.  Она реализуется с помощью конструкции `try`/`catch`:

```php
try {
    // В try пишется код, в котором мы хотим перехватывать исключения
    $users = loadUsersFromFile(...);
    ....
} catch (LoadUsersException $e) {
    // В catch мы указываем, исключения каких классов хотим ловить. 
    // В данном случае мы ловим исключения класса LoadUsersException и его 
    // наследников, то есть только те, которые выбрасывает наша функция
    // Блоков catch может быть несколько, для разных классов

    die("Ошибочка: {$e->getMessage()}\n");
}
```

В этом примере мы ловим исключения класса `LoadUsersException` или его наследника, возникшее внутри блока `try`.  Остальные исключения не перехватываются.  

В PHP5.5 добавлен блок `finally`.  Команды из этого блока будут выполнены после любого из блоков (`try` или `catch`) — в случае, если исключения не произойдет и в случае если оно произойдет.  Это можно использовать для освобождения каких-то ресурсов.  Например, если код внутри `try` открыл файл на чтение, мы можем поставить команду закрытия в `finally` и она будет выполнена в любом случае. 

Если в `catch` указать `Exception` (или `Throwable` для PHP7), то он будет ловить все типы исключений.  Это, как правило, плохая идея, так как мы хотим обрабатывать только определенные типы ошибок и не знаем, что делать с другими.  Чтобы перехватывать только нужные нам исключения, надо сделать свой класс на основе встроенного в PHP `Exception`: 

```php
/** Ошибка загрузки данных о пользователях */
class LoadUsersException extends Exception { }
```

При ошибке мы создаем объект и выкидываем его с помощью `throw`

```php
throw new LoadUsersException("Файл $file не существует");
```

Такое исключение можно будет поймать с помощью `catch`: 

```php
catch (LoadUsersException $e) {
    .... обрабатываем ошибку ...
}
```

Мы можем рассматривать исключения как часть интерфейса функции (часть правил работы с этой функцией).  Есть аргументы, которые мы даем на вход, есть результат, который она возвращает, и есть исключения, которые она может выкинуть при ошибке.  И использование исключений позволяет пользователю функции (тому кто ее вызвал) решить, что делать в случае ошибки. 

Мы не обязаны всегда использовать исключения.  Можно использовать и явный возврат ошибок, если это удобнее. 

## Поддержка исключений везде

По умолчанию многие встроенные в PHP функции не выбрасывают исключения при ошибке, а используют «классическую» систему обработки ошибок.  При ошибке эти функции не выбрасывают исключение, а возвращают определенное значение - например, `false`, и также генерируют сообщение (есть несколько типов сообщений - notice, warning, error, [они перечислены в мануале](https://www.php.net/manual/ru/errorfunc.constants.php)), которое может записываться в лог ошибок и выводиться на экран (или не записываться и не выводиться, в зависимости от настроек PHP).  Эта «классическая» система обработки ошибок частично [описана в мануале PHP](https://www.php.net/manual/ru/book.errorfunc.php).  Что делать с сообщениями об ошибках, определяют [настройки вроде `display_errors`, `log_errors` и `error_reporting`](https://www.php.net/manual/ru/errorfunc.configuration.php) в php.ini. 

Ну например, функция чтения файла в память `file_get_contents` использует «классическую» систему, и ошибка чтения файла или его отсутствие не вызывает выброс исключения и завершение программы, а просто приводит к тому, что функция вернет `false`.  Потому ты обязан ставить `if` после каждого вызова: 

```php
// Если файла нет, функция просто вернет false, и программа продолжит выполняться
$content = file_get_contents('file.txt');
if ($content === false) {
    ... обрабатываем ситуацию, когда из-за ошибки не удалось прочесть файл...
    ... или файла не существует ...
}
```

Многие разработчики забывают или ленятся проверять результат выполнения функции, и пишут некорректные программы, которые даже в случае ошибки продолжают выполнение.  Это опасно, так как программа, в которой возникла ошибка, скорее всего будет дальше работать неправильно и выведет неточную информацию.  Допустим, программа берет из базы сумму денег на счету пользователя, что-то меняет в ней и сохраняет ее обратно в базу.  Если мы опечатаемся в имени переменной, может получиться так, что в базу мы запишем ноль.  Хотя PHP и выведет предупреждение о несуществующей переменной, он не прерывает выполнения программы в этом случае. 

Для этой проблемы есть решение.  PHP позволяет установить функцию-обработчик ошибок (она вызывается при любой ошибке PHP, например обращении к несуществующей переменной или невозможности чтения файла), и в нем выкидывать исключение.  Таким образом, любая ошибка или предупреждение приведут к выбросу исключения.  Все это делается в несколько строчек с помощью встроенного в PHP класса `ErrorException` ([мануал](http://php.net/manual/ru/class.errorexception.php)): 

```php
set_error_handler(function ($errno, $errstr, $errfile, $errline ) {
    // Не выбрасываем исключение если ошибка подавлена с 
    // помощью оператора @
    if (! error_reporting()) {
        return;
    }

    throw new ErrorException($errstr, $errno, 0, $errfile, $errline);
});
```

Этот код превращает любые «классические» ошибки и предупреждения PHP в исключения.  Некоторые современные фреймворки (Slim, Symfony, Laravel) включают в себя такой код. 

## Исключения и PDO

По умолчанию функции из расширения для работы с базами данных PDO при ошибках не вырасывают исключения и не генерируют сообщения, а просто явно возращают значение вроде `false`.  Чтобы функции PDO выбрасывали исключения (например при попытке выполнить неправильно написанный SQL запрос), надо установить соответствующий параметр (рекомендуется).  Без него ты будешь должен после вызова каждой функции проверять результат с помощью `if`: 

```php  
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
```

Мануал: http://php.net/manual/ru/pdo.error-handling.php

## Исключения и mysqli

По умолчанию библиотека mysqli при ошибках не выбрасывает исключений и не генерирует сообщений, а просто возвращает `false`.  Таким образом, после каждого действия ты должен проверять результат с помощью `if`, и сам делать вывод текста ошибки, что видно в примерах кода в мануале: http://php.net/manual/ru/mysqli.query.php

К счастью, ее можно переключить на выброс исключений при ошибках. [ В мануале описано](http://php.net/manual/en/mysqli-driver.report-mode.php) поле `report`, которое задает опции обработки ошибок:

```php
$driver->report_mode = MYSQLI_REPORT_STRICT | MYSQLI_REPORT_ERROR;
```

## Так делать не надо

Не стоит ловить все исключения без разбора: 

```php
catch (Exception $e)
```

Лучше создать свой класс исключений и ловить только его объекты. 

Нельзя скрывать информацию об ошибках.  В 99% такой код, игнорирующий исключение, неправильный:

```php    
catch (Exception $e) {
    echo "Произошла ошибка";
    die();
}
```

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

Не надо располагать `try`/`catch` и `throw` на одном уровне, внутри одной и той же функции — в этом случае код может быть проще, если использовать обычные конструкции вроде `if`.  Обычно `throw` и `catch` располагают в разных функциях:

```php
try {
    ... 
    throw new Exception(...);
    ...
} catch (Exception $e) {
    ...
}
```

## Страница ошибки в веб-приложениях

По умолчанию при непойманном исключении PHP завершает скрипт.  Если опция `display_errors` в php.ini равна 1, то PHP выводит подробности об исключении, а если она равна 0, то в браузере отображается просто белая страница.  Также, PHP записывает информацию об исключении в лог ошибок сервера.  

Очевидно что оба варианта не годятся для использования на продакшен («боевом») сервере: обычные пользователи не должны видеть непонятные надписи на английском, и не должны смотреть на пустую страницу и гадать, в чем дело.  А хакеры не должны видеть подробности об устройстве твоего приложения.  Более того, PHP не выдает при ошибке HTTP код 500, который говорит роботам (вроде Гугла или Яндекса) что на странице ошибка и индексировать ее не надо.  Разработчики PHP выбрали неудачное поведение по умолчанию. 

Потому на боевом сервере надо ставить `display_errors` в 0, а в приложении делать свою страницу ошибки. 

Что будет, если просто не ловить исключение: 

- информация пишется в лог (ок)
- на компьютере разработчика выводятся подробности (ок)
- пользователь видит белую страницу или подробности ошибки (плохо)
- отдается HTTP код 200 (плохо)

Как надо обрабатывать исключения: 

- записать информацию в журнал ошибок, например, функцией [error_log](http://php.net/manual/en/function.error-log.php)
- показать пользователю заглушку ("сайт временно недоступен, вот контакты администратора")
- на заглушке выставить [HTTP код ответа 503](https://ru.wikipedia.org/wiki/%D0%A1%D0%BF%D0%B8%D1%81%D0%BE%D0%BA_%D0%BA%D0%BE%D0%B4%D0%BE%D0%B2_%D1%81%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D1%8F_HTTP#503) для роботов
- на компьютере разработчика (при display_errors = 1) можно показать подробности и стектрейс

Для реализации страницы ошибки можно либо обернуть в `try/catch` код в FrontController, либо установить свой обработчик исключений через `set_exception_handler`.  Не забудь записать информацию в лог с помощью `error_log($e->__toString())` - иначе ты не узнаешь об ошибках которые происходят у пользователей твоего приложения.  

Разумеется, страницу ошибки надо протестировать - выкинуть в коде исключение и убедиться, что страница показывается, HTTP код выдается, а исключение пишется в лог ошибок. 

Если ты используешь фреймворк, возможно, в нем все это уже реализовано.  Современные фреймворки, такие как Slim, Yii, Symfony, выводят заглушку при непойманном исключении.  Старые - не выводят. 

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

Иногда мы ловим исключение, создаем на его основе новое исключение и выбрасываем дальше.  Например, функция чтения конфига обнаруживает ошибку чтения файла FileException, и создает исключение чтения конфига ConfigException.  В такой ситуации в новом исключении можно сохранить ссылку на предыдущее с помощью аргумента previous, который есть в конструкторе стандартного класса \Exception. 

## Ссылки

Механизм исключений существует и в других языках в таком же виде: Java, C++, Ruby, Javascript и многих других.  Статья в вики: 

https://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%BA%D0%B0_%D0%B8%D1%81%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%BD%D0%B8%D0%B9 (написано не очень понятно)

Также, про исключения можно почитать в книгах:

- Мэтт Зандстра «PHP: Объекты, шаблоны, методики программирования»
- Джордж Шлосснейгл «Профессиональное программирование на PHP»
