Обработка ошибок
В Winter ошибку не нужно «ловить и превращать в ответ» вручную. Вы бросаете типизированное исключение из любого места — контроллера, сервиса, middleware — а фреймворк сам сопоставляет его с HTTP-кодом, форматом и уровнем лога.
Что такое обработка ошибок и зачем
Обработка ошибок — превращение сбоя (не найдено, нет прав, упала БД) в корректный HTTP-ответ.
Проблема. Если оборачивать каждый вызов в try/catch и вручную формировать
ответ, получаются разнобой в кодах и формате, дублирование и риск утечки
стектрейса в прод. А ещё легко залогировать «ожидаемую» ошибку 404 как критическую.
Решение. Бросьте подходящее исключение — фреймворк перехватит его на верхнем уровне, выберет HTTP-код, согласует формат с клиентом (JSON/XML/HTML) и запишет лог нужного уровня. Ответы единообразны, а стектрейс виден только в DEBUG. Об этом и раздел.
Как бросать
Все ошибки-исключения бросаются одинаково — конструктором или статическим
хелпером ::throw(). Их поймает роутер:
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 |
Провал валидации (бросается автоматически) |
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'); // 500Error удобен, когда на месте вызова неизвестно, ошибка клиента это или сервера —
он сам выбирает уровень лога по 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:
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) - Middleware —
MiddlewareExceptionи прерывание запроса - Ответы — согласование формата, общее с ошибками