Responses
A response is a status, headers and a body. In Winter you return an object that knows all three, and the framework sends it — identically whichever way the application was started.
What a response is and why
A response is what reaches the client: a status code, headers and a body.
The problem. Assembling it by hand means that in every handler you set the
status, serialise the data, remember Content-Type and Content-Length, and close
the connection. It is not much code, but it is transport code: it has nothing to do
with what the endpoint is for, it repeats everywhere, and it is written differently
under Swoole and PHP-FPM. A forgotten header does not show up as an error, but as
odd behaviour on the client.
The solution. Return a response object from the method. It knows which status to answer with, how to serialise the body and which headers are needed; sending is the framework’s job.
What you can return
return ResponseEntity::ok($data); // a response object — the usual way
return $this->service->all(); // plain data — wrapped into a 200
throw new ResponseException(...); // an error — becomes a response by itselfThe first form is for when the status or the headers matter; the second for an ordinary successful payload. The third is covered in Error handling.
`null` is not an empty response
A method returning null, or declared void, sends nothing: the framework has
nothing to serialise and the client is left waiting. If you do not need a body,
return ResponseEntity::noContent() — a proper 204.
ResponseEntity — data
The main response type for APIs. The factory sets the status; the argument becomes the body.
Ready-made statuses
| Factory | Status | When |
|---|---|---|
ok($body) |
200 | A successful payload |
created($body) |
201 | A resource was created |
accepted($body) |
202 | Accepted for processing, no result yet |
noContent() |
204 | Done, nothing to say |
badRequest($body) |
400 | The client sent nonsense |
unauthorized($body) |
401 | Did not identify itself |
forbidden($body) |
403 | Identified, but not allowed |
notFound($body) |
404 | No such thing |
conflict($body) |
409 | The current state does not permit it |
unprocessable($body) |
422 | Understood, but cannot be done |
internalError($body) |
500 | We broke |
The body is optional everywhere: ResponseEntity::notFound() returns a status with
no body.
Any status, plus headers
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 is an enum of every status, so editor completion saves you from magic
numbers. body() and header() chain.
Reading a response you already built
| Method | What it returns |
|---|---|
getCode() |
The status as an HttpCode |
getBody() |
The body before serialisation |
getHeaders() |
The headers added so far |
Useful in a middleware’s after() — for example to wrap the body in a common
envelope without touching the handlers. See Middleware.
Format negotiation
How the body is serialised is decided by the body type, and for structures also
by the client’s Accept header.
| Body | What is sent |
|---|---|
| An array or an object | JSON or XML — by Accept, JSON by default |
A string, a number, a bool |
Always text/plain; charset=utf-8 |
null, or status 204 |
An empty body |
Accept header |
Response format |
|---|---|
application/json |
JSON |
application/xml |
XML |
text/html, */* or no header |
JSON |
So return ResponseEntity::ok('pong') sends text, not a JSON string — worth
noting if the client parses everything as JSON unconditionally.
An object with a toArray() method is passed through it before serialisation —
entities and DTOs go out as they are, with no manual unwrapping.
You can return a plain array
The router wraps a non-Sendable value for you: return ['key' => 'value'] is the
same as ResponseEntity::ok(['key' => 'value']). For quick endpoints that is the
shortest path; the response object is for when the status or headers matter.
ResponseFile — files and downloads
Assembles a file in memory and sends it. Suited to something you have just produced: a report, an export, a generated document.
| Factory | What it takes |
|---|---|
csv($rows, $fileName) |
An array of rows — assembles CSV |
json($data, $fileName) |
An array or a ready string |
xml($data, $fileName) |
SimpleXMLElement, an object, an array or a scalar |
txt($text, $fileName) |
Text |
binary($bytes, $fileName) |
Arbitrary bytes |
file($absolutePath) |
A path on disk; name and MIME are worked out for you |
The file name is required everywhere except file().
#[GetMapping('report')]
public function report(): ResponseFile
{
return ResponseFile::csv($this->service->rows(), 'report.csv');
}Tuning delivery
return ResponseFile::csv($rows, 'export.csv')
->attachment() // save dialog
->inline() // show in the browser
->maxAge(3600) // Cache-Control: public, max-age=3600
->header('X-Source', 'generated');By default binary and csv go out as attachments; the rest are shown inline.
Every response of this type sets Content-Length and disables compression
(Content-Encoding: identity): otherwise the declared length would disagree with
the actual one and the client would receive a truncated file.
ResponseStreamFile — large files
ResponseFile::file() reads the file into memory in full, which means it runs into
the worker’s memory limit — shared by every concurrent request. For video, archives
and dumps use streaming: the file goes straight from disk, bypassing the PHP heap.
return ResponseStreamFile::open('/var/media/video.mp4'); // inline
return ResponseStreamFile::open('/var/export/dump.sql')->attachment(); // downloadThis is more than “send a file” — the response behaves like a proper file server:
| Capability | What the client gets |
|---|---|
HTTP Range → 206 |
Seeking in video and resuming an interrupted download |
ETag and Last-Modified |
A 304 when the file has not changed |
If-Range |
Safe continuation of a download if the file did change |
Partial delivery is switched off with ->acceptRanges(false): the server announces
Accept-Ranges: none and ignores Range. That is needed where delivery must be
atomic: download counting, one-time links.
Which to choose
ResponseFile::file() |
ResponseStreamFile::open() |
|
|---|---|---|
| Read into memory | in full | no |
Range and 206 |
no | yes |
Conditional request and 304 |
no | yes |
| For what | small files | large files and media |
HTML — ResponseView
Server-side template rendering with layouts and data is a topic of its own, covered on the Views page.
return ResponseView::render('layouts/main', 'users/index', ['users' => $users]);HEAD requests
For HEAD the framework sends the same status and headers it would send for GET,
Content-Length included, but without a body — the suppression happens centrally,
so no response type has to care about it.
No separate route needed
A handler declared with #[GetMapping] answers HEAD as well — the router
substitutes it, as the HTTP specification requires. A separate route is only for
when HEAD must behave differently from GET; such a route takes priority.
If the path exists under neither GET nor HEAD, the answer is the usual one — a
404, or a 405 listing the methods that are available.
Your own responses — Sendable
Every response type shares one interface. Implement it when you need a format that does not ship with the framework:
<?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);
}
}Return such an object from a method and the framework calls send() and interferes
with nothing else.
The method receives the request too — so you can look at Accept, handle
Range or answer 304. If you do not need it, ignore the argument.
HttpResponse offers four operations: status(), header(), end() and
sendfile(). Behind that interface stands either Swoole or PHP-FPM, so your
response works under either without a single change.
The built-in implementations are ResponseEntity, ResponseFile,
ResponseStreamFile and ResponseView.
Next
- Error handling — the responses exceptions turn into
- Views — server-side HTML through
ResponseView - Controllers — where these objects are returned
- Requests — the incoming side of the cycle