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

Ответы

Контроллер возвращает объект ответа, а не пишет в поток руками. Чаще всего это ResponseEntity — данные с HTTP-кодом. Фреймворк сам сериализует его в нужный формат и отправляет; для файлов и HTML есть отдельные типы.

Данные ResponseEntityФайлы ResponseFile / ResponseStreamFileКонтракт Sendable

Что такое ответ и зачем

Ответ — это то, что уходит клиенту: HTTP-код, заголовки и тело.

Проблема. Если формировать ответ вручную (выставить статус, сериализовать данные, записать заголовки, закрыть соединение), код замусоривается транспортными деталями, а под FPM и Swoole они ещё и разные. Легко забыть Content-Type или перепутать код.

Решение. Верните из метода объект ответа — ResponseEntity для данных, ResponseView для HTML, ResponseFile для файлов. Фреймворк сам выставит код, согласует формат с клиентом и отправит ответ одинаково под любым рантаймом. Об этом и раздел.

ResponseEntity — данные

Основной тип ответа для API. Статические фабрики задают HTTP-код, тело сериализуется автоматически:

php
return ResponseEntity::ok($data);               // 200
return ResponseEntity::created($data);          // 201
return ResponseEntity::accepted($data);         // 202
return ResponseEntity::noContent();             // 204
return ResponseEntity::badRequest('message');   // 400
return ResponseEntity::unauthorized();          // 401
return ResponseEntity::forbidden();             // 403
return ResponseEntity::notFound('message');     // 404
return ResponseEntity::conflict($data);         // 409
return ResponseEntity::unprocessable($data);    // 422
return ResponseEntity::internalError();         // 500

Произвольный код и заголовки — билдер-стилем:

php
use Flytachi\Winter\Base\HttpCode;

return ResponseEntity::status(HttpCode::ACCEPTED)->body($data);
return ResponseEntity::ok($data)->header('X-Request-Id', $requestId);

Согласование формата

Когда тело — массив или объект, формат выбирается по заголовку Accept клиента:

Accept Формат
application/json JSON
application/xml XML
text/html или */* / отсутствует JSON (дефолт API)

Скаляры (string/int/float/bool) всегда уходят как text/plain. Объект с методом toArray() конвертируется перед согласованием.

Можно вернуть просто массив

Если вернуть не-Sendable значение, роутер обернёт его сам: return ['key' => 'value'] эквивалентно ResponseEntity::ok(['key' => 'value']). Возврат null или void закрывает соединение без тела (для явного 204 лучше noContent()).

ResponseFile — файлы и выгрузки

Готовые фабрики под частые форматы. Все выставляют Content-Length и отключают сжатие, чтобы не повредить содержимое:

php
use Flytachi\Winter\K2\Http\Response\ResponseFile;

ResponseFile::binary($data, 'report.bin');     // произвольные байты
ResponseFile::txt($content, 'readme.txt');     // текст
ResponseFile::csv($rows, 'export.csv');        // массив строк → CSV
ResponseFile::json($array, 'data.json');       // массив или готовая строка
ResponseFile::xml($data, 'feed.xml');          // SimpleXML/stdClass/array/скаляр
ResponseFile::file('/var/report.pdf');         // файл с диска, MIME авто

Билдер настраивает отдачу:

php
return ResponseFile::csv($rows, 'export.csv')
  ->attachment()      // Content-Disposition: attachment (диалог скачивания)
  ->inline()          // Content-Disposition: inline (показать в браузере)
  ->maxAge(3600)      // Cache-Control: public, max-age=3600
  ->header('X-Source', 'generated');

По умолчанию скачиваются (attachment) binary и csv; остальные — inline.

ResponseStreamFile — большие файлы

ResponseFile::file() читает файл в память целиком. Для больших файлов (видео, аудио, архивы, дампы) используйте ResponseStreamFile — он стримит с диска (zero-copy на Swoole) и работает как корректный файловый сервер:

php
use Flytachi\Winter\K2\Http\Response\ResponseStreamFile;

return ResponseStreamFile::open('/var/media/video.mp4');                 // inline
return ResponseStreamFile::open('/var/export/report.pdf')->attachment(); // скачать

Он поддерживает HTTP Range (частичная отдача 206 — перемотка <video>, докачка), условные запросы (304 по ETag/Last-Modified) и валидаторы If-Range. Отключить Range на эндпоинте: ->acceptRanges(false).

ResponseFile::file() ResponseStreamFile::open()
Читается в память целиком нет (стрим)
HTTP Range / 206 нет да
Условный GET / 304 нет да
Для чего мелкие файлы большие файлы, медиа

HTML — ResponseView

Серверный рендеринг PHP-шаблонов — отдельная тема со своими хелперами и макетами. См. страницу Представления.

Свои ответы — Sendable

Sendable — общий контракт всех типов ответа. Реализуйте его для полностью кастомной отдачи:

php
use Flytachi\Winter\K2\Http\Response\Sendable;
use Flytachi\Winter\K2\Http\Contracts\HttpRequest;
use Flytachi\Winter\K2\Http\Contracts\HttpResponse;

class CsvResponse implements Sendable
{
  public function __construct(private string $csv, private string $filename) {}

  public function send(HttpResponse $response, HttpRequest $request): void
  {
      $response->status(200);
      $response->header('Content-Type', 'text/csv; charset=utf-8');
      $response->header('Content-Disposition', "attachment; filename=\"{$this->filename}\"");
      $response->end($this->csv);
  }
}

Встроенные Sendable: ResponseEntity, ResponseFile, ResponseStreamFile, ResponseView.


Под капотом

Как объект ответа превращается в байты на проводе. Для повседневной работы знать не обязательно.

  • Единый контракт HttpResponse. Каждый ответ пишет через абстракцию (SwooleResponse / FpmResponse) с методами status(), header(), end(), sendfile(). Поэтому один и тот же код ответа работает под Swoole и FPM.
  • send(HttpResponse, HttpRequest). Метод получает и запрос — чтобы согласовать формат, обработать Range или условный GET. Тем, кому запрос не нужен, просто игнорируют аргумент.
  • HEAD — централизованно. Тело для HEAD подавляет адаптер HttpResponse, а не каждый ответ: те же статус и заголовки (включая Content-Length), но без тела. Роутер при этом не маппит HEAD на GET — нужен явный маршрут.

Дальше