Основы веб-разработки

Обработка ошибок

В Winter ошибку не нужно «ловить и превращать в ответ» вручную. Вы бросаете типизированное исключение из любого места — контроллера, сервиса, middleware — а фреймворк сам сопоставляет его с HTTP-кодом, форматом и уровнем лога.

Диспетчер ExceptionWrapperСвои обработчики #[AdviceException]Формат по Accept

Что такое обработка ошибок и зачем

Обработка ошибок — превращение сбоя (не найдено, нет прав, упала БД) в корректный HTTP-ответ.

Проблема. Если оборачивать каждый вызов в try/catch и вручную формировать ответ, получаются разнобой в кодах и формате, дублирование и риск утечки стектрейса в прод. А ещё легко залогировать «ожидаемую» ошибку 404 как критическую.

Решение. Бросьте подходящее исключение — фреймворк перехватит его на верхнем уровне, выберет HTTP-код, согласует формат с клиентом (JSON/XML/HTML) и запишет лог нужного уровня. Ответы единообразны, а стектрейс виден только в DEBUG. Об этом и раздел.

Как бросать

Все ошибки-исключения бросаются одинаково — конструктором или статическим хелпером ::throw(). Их поймает роутер:

php
use Flytachi\Winter\K2\Http\Response\ResponseException;
use Flytachi\Winter\Base\HttpCode;

throw new ResponseException('User not found', HttpCode::NOT_FOUND);
ResponseException::throw('Forbidden', HttpCode::FORBIDDEN);   // статический хелпер

// С дополнительным заголовком:
throw (new ResponseException('Rate limit exceeded', HttpCode::TOO_MANY_REQUESTS))
  ->withHeader('Retry-After', '60');

Иерархия исключений

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

Исключение Код по умолчанию Уровень лога Когда
ResponseException 400 warning Ожидаемая HTTP-ошибка
ClientError 409 warning Доменная ошибка по вине вызывающего
ServerError 500 error Непредвиденный сбой инфраструктуры/приложения
Error 520 авто по коду Когда неизвестно, 4xx это или 5xx
KernelError 500 emergency Нарушение инварианта самого ядра
MiddlewareException 401 warning Прерывание из middleware
ValidationException 422 warning Провал валидации (бросается автоматически)
php
use Flytachi\Winter\K2\Exception\ClientError;
use Flytachi\Winter\K2\Exception\ServerError;

throw new ClientError('Email already taken');              // 409
ClientError::throw('Email already taken', HttpCode::UNPROCESSABLE_ENTITY);

throw new ServerError('Payment gateway timeout');          // 500

Error удобен, когда на месте вызова неизвестно, ошибка клиента это или сервера — он сам выбирает уровень лога по HTTP-коду (4xx → warning, 5xx → error).

Что происходит по умолчанию

Без своих обработчиков каждое исключение обрабатывает ExceptionResponseBase:

  • HTTP-код — из getCode() исключения, с откатом на 500, если код не является валидным HTTP-статусом.
  • Формат (JSON / XML / HTML) — по заголовку Accept, как у ResponseEntity.
  • DEBUG=true — богатая HTML-страница со стектрейсом или JSON с полным трейсом.
  • Прод — минимальный ответ с code + message.
  • ValidationException — автоматически добавляет карту errors в тело.

Стектрейс — только в DEBUG

Полный трейс и отладочные метаданные попадают в ответ только при DEBUG=true. В проде (DEBUG=false) клиент видит лишь код и сообщение — внутренности не утекают.

Свои обработчики — #[AdviceException]

Чтобы отдать особый формат для конкретного исключения, создайте класс с атрибутом #[AdviceException(...)], наследующий ExceptionResponseBase:

main/Exception/DomainExceptionHandler.php
use Flytachi\Winter\K2\Http\Response\AdviceException;
use Flytachi\Winter\K2\Http\Response\ExceptionResponseBase;

#[AdviceException(MyDomainException::class)]
class DomainExceptionHandler extends ExceptionResponseBase
{
  protected function contentData(): array
  {
      return [
          'code'   => $this->throwable->getCode(),
          'error'  => 'domain_error',
          'detail' => $this->throwable->getMessage(),
          'errors' => $this->validationRequests(),  // [] если это не ValidationException
      ] + $this->debugData();
  }
}

Обработчик можно назначить на несколько типов сразу (#[AdviceException(NotFoundException::class, GoneException::class)]) или сделать catch-all без аргументов (#[AdviceException]) — он сработает последним. Регистрировать вручную не нужно: обработчики находит тот же сканер, что и маршруты.

Точки переопределения

Метод Для чего
contentData(): array Тело JSON/XML для не-HTML ответов
contentHtml(): string HTML-тело для Accept: text/html

Доступные хелперы: $this->throwable, $this->httpCode, validationRequests(), debugData(), addHeader().


Под капотом

Как исключение находит свой обработчик и уровень лога. Для повседневной работы знать не обязательно.

  • Все ошибки всплывают в ExceptionWrapper::wrap() (шаг 13 конвейера Router::handle()), который направляет их в самый специфичный подходящий обработчик.
  • Порядок разрешения: сначала обработчики с указанными классами исключений (побеждает самый специфичный), затем catch-all без аргументов, и наконец дефолтный ExceptionResponseBase.
  • Уровень лога определяется типом: если исключение реализует ExceptionLogLevel (все исключения K2 — да), берётся его собственный уровень; всё остальное (\RuntimeException, \LogicException…) логируется как error.
  • Автонастройка. ExceptionWrapper конфигурируется автоматически при Router::resolve() / fromScan(); обработчики обнаруживаются лениво при первой ошибке.

Дальше

  • Валидация — источник ValidationException (422)
  • MiddlewareMiddlewareException и прерывание запроса
  • Ответы — согласование формата, общее с ошибками