Web basics

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.

Data ResponseEntityFiles ResponseFile / ResponseStreamFileContract Sendable

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

php
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 itself

The 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

php
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().

php
#[GetMapping('report')]
public function report(): ResponseFile
{
  return ResponseFile::csv($this->service->rows(), 'report.csv');
}

Tuning delivery

php
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.

php
return ResponseStreamFile::open('/var/media/video.mp4');                  // inline
return ResponseStreamFile::open('/var/export/dump.sql')->attachment();   // download

This 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.

php
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:

main/IcsResponse.php
<?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