Kodokon kodokon.com

Исключения: иерархия, finally, глобальный обработчик

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

9 мин · 3 вопросов

Открыть этот урок в Kodokon

Начиная с PHP 7 всё, что выбрасывается, реализует Throwable, и веток здесь две: Error (сбои движка и типизации, вроде TypeError) и Exception (сбои приложения). Error ты ловишь только на границах программы. Со стороны приложения SPL даёт готовую иерархию: LogicException сигнализирует об ошибке разработчика, которую надо исправить; RuntimeException - о непредвиденной ситуации во время выполнения, которую надо обработать. Профессиональный рефлекс: выводи свои бизнес-исключения из этих базовых классов, по одному на каждую опознаваемую ситуацию.

PHP
<?php

declare(strict_types=1);

abstract class AppException extends RuntimeException
{
}

final class UserNotFound extends AppException
{
    public static function withEmail(
        string $email
    ): self {
        return new self("No user for $email");
    }
}

try {
    throw UserNotFound::withEmail('lea@example.com');
} catch (UserNotFound $e) {
    echo $e->getMessage();
}
Бизнес-иерархия: ты ловишь UserNotFound, AppException или RuntimeException - в зависимости от уровня.

try/catch оправдан, только если ты знаешь, что делать с ошибкой - иначе дай ей всплыть. Контроль уточняют три инструмента: множественный catch (JsonException | ValueError $e) объединяет одинаковую обработку; параметр previous связывает исходную причину, когда ты переводишь техническое исключение в бизнесовое; finally выполняется в любом случае - при успехе, при исключении, даже при досрочном return - и идеально подходит для освобождения ресурса.

PHP
<?php

declare(strict_types=1);

final class PayloadInvalid extends RuntimeException
{
}

function decode(string $json): mixed
{
    try {
        return json_decode(
            $json,
            true,
            flags: JSON_THROW_ON_ERROR,
        );
    } catch (JsonException $e) {
        throw new PayloadInvalid(
            'Invalid JSON payload',
            previous: $e,
        );
    } finally {
        echo "decode() finished\n";
    }
}

try {
    decode('{broken');
} catch (PayloadInvalid $e) {
    echo $e->getPrevious()?->getMessage();
}
Перевод исключения: техническая причина остаётся доступной через getPrevious().

В конце цепочки конструкцию замыкают два глобальных обработчика. set_exception_handler перехватывает всё, что не было обработано: структурированное логирование, обобщённый ответ 500 - и при этом ни в коем случае не раскрывает пользователю сообщение или трассировку. set_error_handler превращает устаревшие ошибки (warnings, notices) в ErrorException, объединяя два мира: всё становится исключением, всё идёт одним и тем же путём обработки.

PHP
<?php

declare(strict_types=1);

set_error_handler(function (
    int $severity,
    string $message,
    string $file,
    int $line,
): bool {
    throw new ErrorException(
        $message,
        0,
        $severity,
        $file,
        $line,
    );
});

set_exception_handler(function (Throwable $e): void {
    error_log($e->getMessage());
    http_response_code(500);
    echo 'A technical error occurred.';
});

trigger_error('Legacy warning', E_USER_WARNING);
echo 'Never reached';
Предупреждение становится исключением, которое никто не поймал: отвечает глобальный обработчик, после чего скрипт останавливается.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Что общего у Error и Exception в современном PHP?
    • Error наследуется от Exception
    • Оба реализуют интерфейс Throwable
    • Оба зарезервированы для движка PHP
  2. Когда выполняется блок finally?
    • Только если исключение не было выброшено
    • Только после catch
    • В любом случае: при успехе, при пойманном или непойманном исключении, при досрочном return
  3. Для чего нужен параметр previous у исключения?
    • Чтобы сохранить исходное исключение, когда ты переводишь его в исключение более высокого уровня
    • Чтобы исключение не всплывало по стеку вызовов
    • Чтобы задать исключение, которое будет выброшено при следующем сбое