quillstack / response
The response object based on PSR-7: HTTP messages, and with the main goal: to be simple and fast.
Requires
- php: ^8.1
- ext-json: *
- psr/http-factory: ^1.0
- psr/http-message: ^1.0|^2.0
- quillstack/di: ^0.6
- quillstack/header-bag: ^0.7
- quillstack/stream: ^0.8
- quillstack/validator-interface: ^0.6
Requires (Dev)
- phpstan/phpstan: ^2.0
- quillstack/unit-tests: ^0.9
README
The response object based on PSR-7: Response. Full documentation: https://quillstack.org/response
A response is written as a class: what it carries is one method, and the status is where the class says it is. That way an endpoint's answer is a thing with a name rather than an array assembled somewhere in a controller.
Why this exists
A response in an API is nearly always the same shape: a status, a content type, and an object turned into JSON. Every PSR-7 implementation makes you assemble that by hand each time, because they are written for everything HTTP can carry rather than for the one thing an API sends.
So a response here is a class you write once and name — UserResponse, NotFoundResponse — and
send() says what it carries. The status code and its reason phrase come from a table checked
against RFC 9110, and a status code this library has never heard of is refused rather than
answered with an empty phrase, because in an application that is a typo rather than a
decision. A response arriving from somewhere else is the other case, and
quillstack/http-client says so by overriding it.
Requirements
- PHP 8.1 or newer
Installation
composer require quillstack/response
Usage
A response of your own
Extend Response and say what it carries:
use Quillstack\Response\Response; final class UserResponse extends Response { private string $id = ''; public function setId(string $id): self { $this->id = $id; return $this; } public function send(): array { return ['id' => $this->id]; } }
$response = (new UserResponse())->setId('42'); $response->getStatusCode(); // 200 $response->getReasonPhrase(); // 'OK' json_encode($response); // {"id":"42"}
Saying what happened
The status comes from the constructor, so a response which means something other than success says so where it is defined rather than where it is used:
use Quillstack\HeaderBag\HeaderBag; use Quillstack\Response\Response; use Quillstack\Response\StatusCode; final class NotFoundResponse extends Response { public function __construct(?HeaderBag $headerBag = null) { parent::__construct(StatusCode::NOT_FOUND, '', $headerBag ?? new HeaderBag()); } public function send(): array { return ['error' => ['status' => $this->getStatusCode(), 'message' => $this->getReasonPhrase()]]; } }
The reason phrase is found from the code, so 404 is Not Found without anybody writing it
down twice. Passing one explicitly overrides it.
Headers
Every change hands back a copy, so the response you were given stays as it was:
$response = (new UserResponse()) ->withHeader('Content-Type', 'application/json') ->withAddedHeader('Set-Cookie', 'a=1'); $response->getHeaderLine('content-type'); // 'application/json'
Building one from a factory
$factory->setResponseClass(UserResponse::class); $response = $factory->createResponse(StatusCode::CREATED);
Technical documentation
AbstractResponse implements Psr\Http\Message\ResponseInterface and JsonSerializable;
Response is the class to extend, and send() is the one method to write.
| Method | Answers |
|---|---|
send(): array |
what this response carries — the one thing you write |
getStatusCode(): int / withStatus($code, $reasonPhrase = '') |
the status |
getReasonPhrase(): string |
found from the code where none was given |
getHeaders(), getHeader(), getHeaderLine(), hasHeader() |
headers, through quillstack/header-bag |
withHeader(), withAddedHeader(), withoutHeader() |
a copy with them changed |
getBody() / withBody() |
the body, as a PSR-7 stream |
getProtocolVersion() / withProtocolVersion() |
the HTTP version |
StatusCode names every status this package knows — 44 of them, from CONTINUE (100) to
HTTP_VERSION_NOT_SUPPORTED (505) — and StatusCode::REASON_PHRASES maps each to its
phrase.
| Exception | Thrown when |
|---|---|
UnknownResponseCodeException |
the status is not one of them |
UnableToFindReasonPhraseException |
there is no phrase for that code |
UnknownResponseClassException |
the factory is given a class which does not exist |
All extend ResponseException.
Benchmark
Measured with quillstack/benchmark on one JSON response — a status, a content type and a twenty-two byte body — built a thousand times. All four produce the same status, phrase, header and body. Runs are interleaved, each figure is the median of five, and PHP is 8.5.7.
| Version | |
|---|---|
| quillstack/response | v0.8.0 |
| nyholm/psr7 | 1.8.2 |
| laminas/laminas-diactoros | 3.8.0 |
| guzzlehttp/psr7 | 2.13.0 |
| Per response | Relative | |
|---|---|---|
| quillstack/response | 2.86 µs | — |
| nyholm/psr7 | 4.27 µs | 1.5× |
| laminas/laminas-diactoros | 7.79 µs | 2.7× |
| guzzlehttp/psr7 | 8.31 µs | 2.9× |
Most of that gap is the body: this one keeps a string as a string, where the others write it
into a php://temp resource — the same difference measured in
quillstack/stream.
What the numbers do not say: all three of the others will carry any body PHP can open — a socket, a compressed resource, a file handle — and construct from any of them. This is built for the response an API sends, which is a status and some JSON.
Tests
composer test
composer test:coverage
composer stan
The rest of Quillstack
This is one component of Quillstack, a PHP framework which is as simple to use as it is strict about what it does.
- quillstack/serializer — what decides which fields go
- quillstack/stream — what carries the body
- quillstack/header-bag — the headers underneath
- quillstack/framework — where a response is answered with
License
MIT. See LICENSE.