Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save codedokode/65d43ca5ac95c762bc1a to your computer and use it in GitHub Desktop.

Select an option

Save codedokode/65d43ca5ac95c762bc1a to your computer and use it in GitHub Desktop.
Исключения в PHP

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

Если ты изучаешь ООП, ты наверняка натыкался на исключения. В мануале PHP описаны команды try/catch/throw и finally (доступна начиная с PHP 5.5), но не объясняется толком как их использовать. Чтобы разобраться с этим, надо узнать почему они вообще были придуманы.

А придуманы они были чтобы сделать удобную обработку ошибок.

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

$file = './users.csv';

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

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

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

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

function loadUsersFromFile($file) {
    // Файла не существует — ошибка
    if (!file_exists($file)) {
        die("Ошибка: файл $file не существует\n");
    }

    ....
}

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

Что же, давай улучшим код и переделаем функцию, заставив при ошибке ее возвращать значение false и выставлять текст ошибки в переменной (значок & обозначает что переменная передается не как копия, а по ссылке, и функция может изменить исходную переменную, подробнее: передача по ссылке, мануал)

function loadUsersFromFile($file, &$errorText) {
    // Файла не существует — ошибка
    if (!file_exists($file)) {
        $errorText = "Ошибка: файл $file не существует";
        return false;
    }

    ....
}

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

....
// Загружаем список пользователей в массив
$error = '';
$users = loadUsersFromFile($file, $error); 
if ($users === false) {
    // Выводим текст ошибки
    die("Не удалось вывести список пользователей из-за ошибки: {$error}\n");
}

...

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

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

В качестве решения проблемы были придуманы искючения. В случае ошибки код выбрасывает исключение командой throw:

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

Исключение — это объект встроенного в PHP класса Exception (мануал по Exception) или его наследника (ты можешь делать свои классы исключений, например для разных видов ошибок). Объект исключения содержит подробности о причинах ошибки.

Исключение выбрасывается в случае возникновения нештатной ситуации.

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

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

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

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

set_exception_handler(function (Exception $exception) {
    // Функция будет вызвана при возникновении исключения        
});

Этот способ ловит неперехваченные исключения во всей программе, нам он не нужен, потому перейдем к более продвинутой структурной обработке исключений. Она реализуется с помощью try/catch:

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

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

В PHP5.5 и выше добавлен блок finally. Команды из этого блока будут выполнены после любого из блоков — в случае если исключения не произойдет и в случае если оно произойдет.

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

class FileErrorException extends Exception { }

Выкидывать его в throw

throw new FileErrorException("Файл не существует");

И ловить в catch:

catch (FileErrorException $e) {
    ....
}

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

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

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

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

Для этой проблемы есть решение. Можно установить общий обработчик ошибок, и в нем выкидывать исключение. Все это делается в несколько строчек с помощью встроенного в PHP класса ErrorException (мануал):

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

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

Этот код превращает любые ошибки и предупреждения PHP в исключения.

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

Чтобы расширение для работы с базами данных PDO использовало исключения, надо установить соответствующий параметр (рекомендуется):

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

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

Плохие вещи, которые делать не стоит

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

catch (Exception $e)

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

Не надо скрывать исключения. В 99% этот код, игнорирующий исключение, неправильный:

catch (Exception $e) {
    // ничего не делаем
}

Не надо располагать try/catch и throw на одном уровне — в этом случае проще написать if:

try {
    ... 
    throw new Exception(...);
    ...
} catch (Exception $e) {
    ...
}
@codedokode
Copy link
Author

О, первый комментарий! Приятно. Увы, гитхаб никак не сообщает мне об оставленных комментариях так что вопросы лучше писать напрямую на почту (codedokode на gmail.com).

Exception — класс, который обрабатывает ошибку;

Не совсем. Exception - это не класс, обрабатывающий ошибку, а хранящий информацию о ней. Когда мы выкидываем исключение, мы создаем объект с информацией об ошибке, например:

throw new Exception("some text");

или так (тут мы сохраняем в объект дополнительные подробности):

$e = new ConnectException('DNS lookup failed',  'example.com');
throw $e;

В catch слово Exception это фильтр, задающий какие виды исключений ловит данный блок, он значит "класс Exception или любой класс, унаследованный от него" - в соответствии с принципом Лисков. Вместо имени класса можно также указать имя интерфейса - будут ловиться все исключения реализующие этот интерфейс. По сути имя класса тут работает как условие, показывающее, поймает исключение этот блок или нет. Так как все исключения наследуются от Exception, указание этого слова значит фактически "ловить любые исключения".

Блоков catch может быть несколько - в этом случае при исключении каждый из них проверяется по очереди, но выполниться может максимум один.

$e — объект класса Exception; то есть, по сути, это $e = new Exception;.

Не совсем, тут указывается переменная, в которую надо поместить пойманный объект исключения. Мы выкидываем исключение с помощью команды throw ...., а с помощью catch мы ловим это исключение и помещаем пойманный объект в переменную. Исключение это объект любого класса унаследованного от Exception, можно создавать свои классы, например чтобы хранить там какую-то дополнительную информацию об ошибке или добавлять методы.

Ну и дам ссылку на краткий мануал по исключениям: http://php.net/manual/ru/language.exceptions.php

@Dok11
Copy link

Dok11 commented May 2, 2016

Доходчиво написано, спасибо!
Пишите еще :)

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

И, кстати, нашел пару опечаток:

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

И:

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

@codedokode
Copy link
Author

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

Можете посоветовать что-то почитать для повышения качества PHP-кода?

Есть книга "Совершенный код", про ООП-паттерны можно почитать Фаулера "Шаблоны проектирования корпоративных приложений", но это конечно не самая простая книга для понимания. Ее лучше читать параллельно с изучением фреймворков вроде Симфони 2.

@Kison
Copy link

Kison commented Oct 8, 2016

Отличная статья ), было бы здорово еще добавить информацию про возможность использования вложенных try блоков.

@vnzhlv-zh
Copy link

Спасибо, полезно!

@delarean
Copy link

delarean commented Mar 2, 2018

Мануал :
Замечание:
Классы PHP не могут напрямую реализовать интерфейс Throwable. Вместо этого они могут наследовать подкласс Exception.

В PHP7 определение расширено: исключения не обязаны наследоваться от Exception, это может быть любой класс, реализующий интерфейс Throwable, а так как сам Exception его реализует, то его наследники по-прежнему будут работать

Пытаюсь понять кому верить )) .

@delarean
Copy link

delarean commented Mar 2, 2018

А статья очень крутая ,спасибо !

@gartes
Copy link

gartes commented Jul 24, 2018

спасибо!!!

@medialuki
Copy link

Спасибо!

@nakaa7
Copy link

nakaa7 commented Oct 2, 2018

Спасибо! Наконец-то понял как работают исключения! Очень хорошо написано!

@codedokode
Copy link
Author

@den82721

Верить стоит мануалу. Действительно, в PHP7 добавили интерфейс Throwable, но реализовать его пока не разрешают. Я добавил это уточнение в новой версии урока (ссылка есть вверху). Эта же страница содержит старую версию урока и не будет обновляться.

@pavarov
Copy link

pavarov commented May 17, 2020

Спасибо! Хорошая статья.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment