Ответы
Контроллер возвращает ответ, а не пишет в поток руками. Чаще всего это
ResponseEntity — данные вместе с HTTP-кодом. Фреймворк сам подберёт формат,
выставит заголовки и отправит всё одинаково под любым рантаймом.
Что такое ответ и зачем
Ответ — это то, что уходит клиенту: код состояния, заголовки и тело.
Проблема. Собирать его вручную — значит в каждом обработчике выставить статус,
сериализовать данные, не забыть Content-Type и Content-Length, закрыть
соединение. Кода немного, но он транспортный: к задаче эндпоинта отношения не
имеет, повторяется везде и под Swoole с PHP-FPM пишется по-разному. Забытый
заголовок при этом проявится не ошибкой, а странным поведением у клиента.
Решение. Верните из метода объект ответа. Он знает, каким кодом отвечать, как сериализовать тело и какие заголовки нужны, а отправкой занимается фреймворк — одинаково при любом способе запуска.
Что можно вернуть
return ResponseEntity::ok($data); // объект ответа — обычный путь
return $this->service->all(); // просто данные — обернутся в 200
throw new ResponseException(...); // ошибка — станет ответом самаПервый вариант нужен, когда важны код или заголовки; второй — когда это обычная успешная выдача. Третий разобран в Обработке ошибок.
`null` — это не пустой ответ
Метод, вернувший null или объявленный как void, не отправляет ничего:
фреймворку нечего сериализовать, и клиент остаётся ждать. Если тело не нужно,
возвращайте ResponseEntity::noContent() — это корректный 204.
ResponseEntity — данные
Основной тип ответа для API. Фабрика задаёт код, аргумент становится телом.
Готовые коды
| Фабрика | Код | Когда |
|---|---|---|
ok($body) |
200 | Успешная выдача |
created($body) |
201 | Ресурс создан |
accepted($body) |
202 | Принято в обработку, результата пока нет |
noContent() |
204 | Сделано, отвечать нечем |
badRequest($body) |
400 | Клиент прислал ерунду |
unauthorized($body) |
401 | Не представился |
forbidden($body) |
403 | Представился, но нельзя |
notFound($body) |
404 | Нет такого |
conflict($body) |
409 | Состояние не позволяет |
unprocessable($body) |
422 | Понятно, но невыполнимо |
internalError($body) |
500 | Сломались мы |
Тело у всех необязательно: ResponseEntity::notFound() вернёт код без тела.
Любой код и заголовки
use Flytachi\Winter\Base\HttpCode;
return ResponseEntity::status(HttpCode::IM_A_TEAPOT)->body($data);
return ResponseEntity::ok($data)
->header('X-Request-Id', $requestId)
->header('X-Total-Count', (string) $total);HttpCode — перечисление всех кодов, так что подсказка редактора избавляет от
магических чисел. Вызовы body() и header() цепляются.
Чтение уже собранного ответа
| Метод | Что вернёт |
|---|---|
getCode() |
Код как HttpCode |
getBody() |
Тело до сериализации |
getHeaders() |
Добавленные заголовки |
Пригождается в after() у middleware — например, чтобы завернуть тело в общий
конверт, не трогая обработчики. См. Middleware.
Согласование формата
Как сериализовать тело, решает тип тела, а для структур — ещё и заголовок
Accept клиента.
| Тело | Что уходит |
|---|---|
| Массив или объект | JSON или XML — по Accept, по умолчанию JSON |
Строка, число, bool |
Всегда text/plain; charset=utf-8 |
null или код 204 |
Пустое тело |
Заголовок Accept |
Формат ответа |
|---|---|
application/json |
JSON |
application/xml |
XML |
text/html, */* или заголовка нет |
JSON |
То есть return ResponseEntity::ok('pong') отдаст текст, а не JSON-строку — на
это стоит обратить внимание, если клиент разбирает ответ как JSON безусловно.
Объект, у которого есть метод toArray(), перед сериализацией проходит через
него — сущности и DTO отдаются как есть, разворачивать их вручную не нужно.
Можно вернуть просто массив
Не-Sendable значение роутер обернёт сам: return ['key' => 'value'] равносильно
ResponseEntity::ok(['key' => 'value']). Для быстрых эндпоинтов это самый
короткий путь; объект ответа нужен, когда важны код или заголовки.
ResponseFile — файлы и выгрузки
Собирает файл в памяти и отдаёт клиенту. Подходит для того, что вы только что сформировали: отчёта, выгрузки, сгенерированного документа.
| Фабрика | Что принимает |
|---|---|
csv($rows, $fileName) |
Массив строк — соберёт CSV |
json($data, $fileName) |
Массив или готовую строку |
xml($data, $fileName) |
SimpleXMLElement, объект, массив или скаляр |
txt($text, $fileName) |
Текст |
binary($bytes, $fileName) |
Произвольные байты |
file($absolutePath) |
Путь к файлу на диске; имя и MIME определит сам |
Имя файла обязательно у всех, кроме file().
#[GetMapping('report')]
public function report(): ResponseFile
{
return ResponseFile::csv($this->service->rows(), 'report.csv');
}Настройка отдачи
return ResponseFile::csv($rows, 'export.csv')
->attachment() // диалог сохранения
->inline() // показать в браузере
->maxAge(3600) // Cache-Control: public, max-age=3600
->header('X-Source', 'generated');По умолчанию binary и csv уходят как вложение, остальные — на просмотр.
Каждый ответ этого типа выставляет Content-Length и отключает сжатие
(Content-Encoding: identity): иначе объявленная длина разойдётся с фактической, и
клиент получит обрезанный файл.
ResponseStreamFile — большие файлы
ResponseFile::file() читает файл в память целиком, а значит упирается в лимит
памяти воркера — общий на все одновременные запросы. Для видео, архивов и дампов
берите потоковую отдачу: файл уходит с диска напрямую, минуя кучу PHP.
return ResponseStreamFile::open('/var/media/video.mp4'); // просмотр
return ResponseStreamFile::open('/var/export/dump.sql')->attachment(); // скачиваниеЭто не просто «отдать файл» — ответ ведёт себя как полноценный файловый сервер:
| Возможность | Что даёт клиенту |
|---|---|
HTTP Range → 206 |
Перемотку видео и докачку прерванной загрузки |
ETag и Last-Modified |
Ответ 304, когда файл не менялся |
If-Range |
Безопасное продолжение докачки, если файл успел измениться |
Отключить частичную отдачу — ->acceptRanges(false): сервер объявит
Accept-Ranges: none и проигнорирует Range. Нужно там, где важна атомарность
выдачи: подсчёт скачиваний, одноразовые ссылки.
Что выбрать
ResponseFile::file() |
ResponseStreamFile::open() |
|
|---|---|---|
| Читается в память | целиком | нет |
Range и 206 |
нет | да |
Условный запрос и 304 |
нет | да |
| Для чего | небольшие файлы | большие файлы и медиа |
HTML — ResponseView
Серверный рендеринг шаблонов с макетами и данными — отдельная тема, разобранная на странице Представления.
return ResponseView::render('layouts/main', 'users/index', ['users' => $users]);Запросы HEAD
На HEAD фреймворк отдаёт те же статус и заголовки, что отдал бы на GET, включая
Content-Length, но без тела — подавление происходит централизованно, каждому типу
ответа заботиться об этом не нужно.
Отдельный маршрут не нужен
Обработчик, объявленный через #[GetMapping], отвечает и на HEAD — роутер
подставляет его сам, как того требует спецификация HTTP. Отдельный маршрут заводят
только если HEAD должен обрабатываться иначе, чем GET; такой маршрут имеет
приоритет.
Если пути нет ни под GET, ни под HEAD, ответ прежний — 404 или 405 со
списком доступных методов.
Свои ответы — Sendable
Все типы ответа объединяет один интерфейс. Реализуйте его, если нужен формат, которого нет из коробки:
<?php
namespace Main;
use Flytachi\Winter\Kernel\Http\Contracts\HttpRequest;
use Flytachi\Winter\Kernel\Http\Contracts\HttpResponse;
use Flytachi\Winter\Kernel\Http\Response\Sendable;
class IcsResponse implements Sendable
{
public function __construct(private string $calendar, private string $fileName) {}
public function send(HttpResponse $response, HttpRequest $request): void
{
$response->status(200);
$response->header('Content-Type', 'text/calendar; charset=utf-8');
$response->header('Content-Disposition', "attachment; filename=\"{$this->fileName}\"");
$response->end($this->calendar);
}
}Вернули такой объект из метода — фреймворк вызовет send() и больше ни во что не
вмешается.
Метод получает и запрос тоже — чтобы можно было посмотреть на Accept,
обработать Range или ответить 304. Не нужен — просто не используйте аргумент.
Через HttpResponse доступны четыре операции: status(), header(), end() и
sendfile(). За этим интерфейсом стоит либо Swoole, либо PHP-FPM, поэтому ваш
ответ работает при любом способе запуска без единой правки.
Встроенные реализации: ResponseEntity, ResponseFile, ResponseStreamFile,
ResponseView.
Дальше
- Обработка ошибок — ответы, которые получаются из исключений
- Представления — серверный HTML через
ResponseView - Контроллеры — где эти объекты возвращаются
- Запросы и привязка параметров — входная сторона цикла