Урок по треку PHP 6 мин

Исключения и ошибки в PHP

Throwable, Error и Exception, finally, ErrorException, свои исключения.

В PHP есть и исключения, и «старые» ошибки уровня E_WARNING. На собесе проверяют иерархию Throwable, работу finally и стратегию обработки ошибок в приложении.

Иерархия

Throwable
├── Error: TypeError, ValueError, ArithmeticError (DivisionByZeroError), UnhandledMatchError, ...
└── Exception: ErrorException, JsonException, RuntimeException, LogicException (InvalidArgumentException, DomainException), ...

С PHP 7 ошибки движка (вызов метода у null, неверный тип аргумента) стали Error. catch (Exception $e) их не ловит, для всего вместе catch (Throwable $e), но только на верхнем уровне.

try, catch, finally

try {
    $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException | ValueError $e) {     // несколько типов (7.1)
    throw new ImportFailed('Битый файл', 0, $e); // previous сохраняет причину
} finally {
    fclose($handle);
}

finally выполняется всегда, даже после return. Если в finally есть свой return, он перекрывает результат и даже глотает исключение, так делать не надо. С PHP 8.0 переменную можно не писать: catch (JsonException), а throw стал выражением: $x ?? throw new Exception().

Ошибки, которые не исключения

Многие функции сообщают о проблеме предупреждением и возвращают false: file_get_contents, fopen. try/catch их не поймает. Решение во всех фреймворках:

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

Оператор @ только подавляет вывод, ошибку он не исправляет.

Свои исключения

class BillingException extends RuntimeException {}

final class InsufficientFunds extends BillingException
{
    public static function forAmount(int $amount): self
    {
        return new self("Недостаточно средств для списания $amount");
    }
}

Иерархия по модулям позволяет ловить «всё из биллинга» или конкретный случай.

Глобальная обработка

  • set_exception_handler: для непойманных исключений, после него скрипт завершается.
  • register_shutdown_function + error_get_last(): для фатальных ошибок вроде нехватки памяти.
  • Во фреймворках: Handler в Laravel, событие kernel.exception в Symfony. Они логируют, отправляют в Sentry и рендерят ответ.

В продакшене display_errors=Off, log_errors=On: стек и пути не должны попадать к пользователю.

Ожидаемые ошибки

Валидация и бизнес-отказы это не баги. Их возвращают как ошибки валидации (422) или доменные исключения, которые обработчик превращает в понятный ответ. Неожиданные ошибки дают 500 и алерт.

Частые ошибки

  • catch (Exception) и удивление, что TypeError пролетел.
  • Пустой catch, проглатывающий проблему.
  • Потеря причины без $previous.
  • return в finally.
  • json_decode без JSON_THROW_ON_ERROR и проверки результата.

Что спросят на собесе

  • Чем Error отличается от Exception?
  • Как работает finally с return?
  • Как превратить предупреждения в исключения?
  • Как вы организуете обработку ошибок в приложении?
  • Как поймать фатальную ошибку?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.