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

Что такое исключения?

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

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

В официальной документации по PHP есть обширный раздел посвящённый исключениям, однако толком не сказано зачем они нужны. А нужны они для более удобной обработки ошибок. Разберём пример. Нам нужно вывести список сотрудников компании из файла с телефонами и должностями.


$staffFile = __DIR__ . '/staff.csv';
$staff = loadStaffFile($staffFile);

foreach ($staff as $user){
    echo '<b>ФИО: </b>' . $user[0] . ' - <b>тел.:</b> ' .  $user[1] . ' <b>должность:</b> ' . $user[2] . '<br/>';
}

/**
 * Загрузка файла сотрудников
 * @param $filePath
 * @return array
 */
function loadStaffFile($filePath){
    // Загружаем список сотрудников из файла
    $staff = fopen($filePath, 'r');
    $users = [];
    // Выводим предварительно считав строку при помощи fgetcsv
    while ($user = fgetcsv($staff, 1000, ';')){
        $users[] = $user;
    }

    return $users;
}



Вроде всё правильно? На самом деле нет. Файла может не существовать, к нему может не быть соответствующих доступов, внутри файла данные могут иметь не ту структуру которую мы ожидаем и т.д.

Самый простой способ добавить обработку ошибок прямо в функцию loadStaffFile(), однако это не верный подход.


/**
 * Загрузка файла сотрудников
 * @param $filePath
 * @return array
 */
function loadStaffFile($filePath){
    // Загружаем список сотрудников из файла
    if(!file_exists($filePath)){
        die('Файл ' . $filePath . ' не существует');
    }


	...

    return $users;
}

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

Давайте улучшим нашу функцию. Сделаем так, что функция будет выводить специальный массив с тремя параметрами:
  • status - статус ok, error
  • result - результат, массив или пустое значение
  • msg - сообщение, в основном для сообщения об ошибках

/**
 * Загрузка файла сотрудников
 * @param $filePath
 * @return array
 */
function loadStaffFile($filePath){
    $result = [
        'status' => '',
        'result' => '',
        'msg' => ''
    ];
    // Загружаем список сотрудников из файла
    if(!file_exists($filePath)){
        $result['status'] = 'error';
        $result['msg'] = 'Файл ' . $filePath . ' не существует';
    } else {
        $staff = fopen($filePath, 'r');
        $users = [];
        // Выводим предварительно считав строку при помощи fgetcsv
        while ($user = fgetcsv($staff, 1000, ';')){
            $users[] = $user;
        }

        $result['status'] = 'ok';
        $result['result'] = $users;
    }
    return $result;
}

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




$staffFile = __DIR__ . '/staff.csv';
$staff = loadStaffFile($staffFile);

if($staff['status'] == 'ok'){
    foreach ($staff['result'] as $user){
        echo '<b>ФИО: </b>' . $user[0] . ' - <b>тел.:</b> ' .  $user[1] . ' <b>должность:</b> ' . $user[2] . '<br/>';
    }
} elseif($staff['status']=='error'){
    echo 'Ошибка: ' . $staff['msg'] . '<br/>';
}

Уже лучше. Теперь клиентский код может по своему обрабатывать ошибки. Однако за это мы заплатили усложнением кода, к тому же добавляется дополнительная проверка статуса через if. Когда в вашем коде будет много таких функций, половина кода будет состоять из таких вот проверок if(...) { ... } else { ... }.

Выброс исключений

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


// Загружаем список сотрудников из файла
if(!file_exists($filePath)){
    throw new Exception('Файл ' . $filePath . ' не существует');
}

Исключение - это экземпляр стандартного класса Exception встроенного в PHP или его наследника. Этот объект содержит информацию о причинах ошибки. В php есть целый набор классов для работы с исключениями которые вы можете использовать в своих проектах https://www.php.net/manual/ru/spl.exceptions.php . 

Кстати в PHP 7 классом исключения может быть не обязательно класс унаследованный от Exception. Достаточно, чтобы ваш класс реализовывал интерфейс Throwable. Если внедрить исключения в наш пример, получим следующую историю.


try {
    $staffFile = __DIR__ . '/staff.csv';
    $staff = loadStaffFile($staffFile);

    foreach ($staff as $user){
        echo '<b>ФИО: </b>' . $user[0] . ' - <b>тел.:</b> ' .  $user[1] . ' <b>должность:</b> ' . $user[2] . '<br/>';
    }
} catch (Exception $e){
    echo 'Ошибка: ' . $e->getMessage() . '<br/>';
}


/**
 * Загрузка файла сотрудников
 * @param $filePath
 * @return array
 */
function loadStaffFile($filePath){

    // Загружаем список сотрудников из файла
    if(!file_exists($filePath)){
        throw new Exception('Файл ' . $filePath . ' не существует');
    }

    $staff = fopen($filePath, 'r');
    $users = [];
    // Выводим предварительно считав строку при помощи fgetcsv
    while ($user = fgetcsv($staff, 1000, ';')){
        $users[] = $user;
    }

    return $users;
}

Обратите внимание что клиентский код нужно обернуть в блоки try { ... } catch(Exception $e) { ... } В случае выброса исключения отработает блок catch { ... } в котором вы можете как-то по своему обработать ошибку.

Ещё один плюс о котором хочется упомянуть в разрезе исключений, это то, что ошибки можно пробрасывать "наверх" в случае если функции вызываются иерархично. Сделаем простой пример, форма для отправки числа от 0 и более и цепочку функций которые будут что-то делать с этим числом. Каждая функция будет проверять входной параметр на какое-то условие и в случае ошибки выбрасывать исключение. Функции вынесены в отдельный файл functions.php.


<?
include_once 'functions.php';
?>
<!doctype html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Работа с исключениями</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" rel="stylesheet"
          crossorigin="anonymous">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-12">
            <h1>Работа с исключениями</h1>
            <form action="/exception/form.php" method="get">
                <div class="mb-3">
                    <input class="form-control" type="text" name="num" value=""
                           placeholder="Введите значение от 0 до 100 000">
                </div>
                <button type="submit" class="btn btn-primary">Отправить</button>
            </form>
            <br/>
            <? //вариант с исключением
            if ($_GET['num'] !== NULL) {
                echo '<span style="background-color: #CCC; padding: 5px; line-height: 1.5;">';
                try {
                    echo 'Результат: ' . level1($_GET['num']);
                } catch (Exception $e) {
                    echo 'Ошибка: <b>' . $e->getMessage() . '</b>';
                }
                echo '</span><br/><br/>';
            }
            ?>
        </div>
    </div>
</div>

</body>
</html>





И сами функции.


function level1($a){
    if(!$a){
        throw new Exception('Значение не может быть пустым');
    }
    return level2($a);
}

function level2($a){
    if(!is_numeric($a)){
        throw new Exception('Переменная должна быть числом');
    }

    return level3($a);
}

function level3($a){
    if($a == 0){
        throw new Exception('Нельзя делить на ноль!');
    }
    $a = 100 / $a;
    return level4($a);
}

function level4($a){
    $result = $a * 4;
    if($result > 500) {
        throw new Exception('Результат ' . $result . ' не может быть больше 500');
    }

    return $result;
}

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

Исключения в иерархическом вызове

Создание пользовательских исключений

Как уже говорилось выше в PHP есть возможность создать свои собственные классы исключения, для этого нужно расширить стандартный класс Exception или реализовать интерфейс Throwable. Для чего это может потребоваться? Например разные типы исключений можно ловить разными блоками catch { ... } и по разному обрабатывать. Рассмотрим пример.


/**
 * Class CustomException
 * Собственный тип исключений
 */
Class CustomException extends Exception {
    /* ... */
}


try {
    $staffFile = __DIR__ . '/staff.csv';
    $staff = loadStaffFile($staffFile);

    foreach ($staff as $user){
        echo '<b>ФИО: </b>' . $user[0] . ' - <b>тел.:</b> ' .  $user[1] . ' <b>должность:</b> ' . $user[2] . '<br/>';
    }
} catch (Exception $e){
    echo 'Ошибка: ' . $e->getMessage() . '<br/>';
} catch (CustomException $e){ //Отдельный блок catch для поимки нашего пользовательского исключения CustomException
    echo 'Пользовательское исключение: ' . $e->getMessage() . '<br/>';
}


/**
 * Загрузка файла сотрудников
 * @param $filePath
 * @return array
 */
function loadStaffFile($filePath){

    // Загружаем список сотрудников из файла
    if(!file_exists($filePath)){
        throw new Exception('Файл ' . $filePath . ' не существует');
    }

    $staff = fopen($filePath, 'r');
    $users = [];
    // Выводим предварительно считав строку при помощи fgetcsv
    while ($user = fgetcsv($staff, 1000, ';')){
        $users[] = $user;
    }

    //Бросаем кастомное исключение собственного класса
    if(count($users) > 0 && count($users) < 5) {
        throw new CustomException('Слишком мало пользователей');
    }

    return $users;
}

Теперь наш класс CustomException позволяет нам выбрасывать исключение определённого типа, которое будет обрабатываться особым образом в отдельном блоке catch { ... } если он есть. Таким образом мы можем разделять обработку ошибок в зависимости от их типа, например "Ошибки безопасности", "Критические ошибки", "Предупреждения" и т.д. что-то из этих ошибок можно логировать, что-то показывать в виде специального кода понятного только администратору сайта. Так можно скрыть некоторую информацию от злоумышленников.   

Блок Finally

В конструкции try { ... } catch { ... } можно добавлять третий блок finally { ... } который будет выполнятся всегда, вне зависимости от того было ли выброшено исключение или нет. Допустим файл с пользователями с которым мы работаем в наших примерах загружается к нам извне и нам нужно переместить его в папку "Обработано" в конце операции.




try {
    $staffFile = __DIR__ . '/staff.csv';
    $staff = loadStaffFile($staffFile);

    foreach ($staff as $user){
        echo '<b>ФИО: </b>' . $user[0] . ' - <b>тел.:</b> ' .  $user[1] . ' <b>должность:</b> ' . $user[2] . '<br/>';
    }
} catch (Exception $e){
    echo 'Ошибка: ' . $e->getMessage() . '<br/>';
} finally {

    /**
     * Код который будет выполнятся всегда, вне зависимости
     * от того было выброшено исключение или нет
     */

    //Переносим файл в папку "Обработано"
    rename($staffFile, __DIR__ . '/processed/file_' . date('d_m_Y_h_i_s') . '.csv');
}

Пример не очень хороший, но суть я думаю понятна. В блок finally например можно добавить код исполнения события, что-то вроде EventManager::ExecuteEvent('afterUserFileUpload') или записи в лог. 

Заключение

В заключении хочется сказать что механизм исключений в PHP позволяет вам организовать гибкую систему обработки ошибок, сделать код чище и понятнее. Они позволят сделать поведение приложения более прозрачным а отладку быстрой. Желаю удачи!
Полезная статья?
(Голосов: 5, Рейтинг: 3.68)
Вам также могут понравиться
Английский для программистов

Английский для программистов

Почему IT-специалисту необходимо освоить английский язык? Разбираем в статье.

Как подключить CSS и JS файлы к шаблону 1С Битрикс

Как подключить CSS и JS файлы к шаблону 1С Битрикс

Как правильно подключать стили и скрипты к шаблону 1С Битрикс.

Генерация оглавления статьи

Генерация оглавления статьи

В статье рассмотрен пример функции для генерации оглавления статьи блога или новости


Комментарии
Защита от автоматических сообщений
CAPTCHA
Введите слово на картинке
24.04.2024 | Сергей

Пользовательские классы ведь не могут реализовать интерфейс Throwable

Комментировать | 0  
Защита от автоматических сообщений
CAPTCHA
Введите слово на картинке
Закрыть
24.04.2024 | Александр Андреев

Вы правы, добавлю это уточнение в статью. Я имел ввиду что нужно наследовать Exception который в свою очередь реализует Throwable.

Комментировать | 0  
Защита от автоматических сообщений
CAPTCHA
Введите слово на картинке
Закрыть