quillstack / server-request
The request object based on PSR-7: HTTP messages, and with the main goal: to be simple and fast.
Requires
- php: ^8.1
- psr/http-factory: ^1.0
- psr/http-message: ^1.0|^2.0
- quillstack/di: ^0.6
- quillstack/header-bag: ^0.7
- quillstack/http-request: ^0.6
- quillstack/parameter-bag: ^0.6
- quillstack/stream: ^0.8
- quillstack/uri: ^0.7
- quillstack/validator-interface: ^0.6
Requires (Dev)
- phpstan/phpstan: ^2.0
- quillstack/unit-tests: ^0.9
README
The request object based on PSR-7: Server Request. Full documentation: https://quillstack.org/server-request
A request built from what PHP hands the process — $_SERVER, $_GET, $_COOKIE, $_FILES
and the body — and answered for the way PSR-7 says it should be. Every part of it is a
parameter bag, so reading something which was
not sent is an answer rather than a notice.
Why this exists
A server request has two entirely different origins, and most implementations only take one
seriously. There is the one a web server hands over — $_SERVER, $_GET, $_FILES, a body on
php://input — and the one a test or a client makes up from a verb and an address. Building the
first from globals is fiddly and easy to get subtly wrong; building the second should be one
line.
This does both: ServerRequestFromGlobalsFactory for what arrived, and a PSR-17
ServerRequestFactory for everything else. A controller can also declare its own request
class — LoginRequest, CreateUserRequest — and be handed that instead, which is where
validation belongs and where a framework usually makes you go looking.
Requirements
- PHP 8.1 or newer
Installation
composer require quillstack/server-request
Usage
Building one from a method and an address
use Quillstack\ServerRequest\Factory\ServerRequestFactory; $request = (new ServerRequestFactory())->createServerRequest('GET', 'https://api.example.org/users/42?page=2');
$request->getMethod(); // 'GET' $request->getRequestTarget(); // '/users/42?page=2' $request->getHeaderLine('Host'); // 'api.example.org' $request->getQueryParams(); // ['page' => '2']
It is PSR-17, so anything expecting ServerRequestFactoryInterface can be handed one, and it
needs nothing to be built itself. The Host header is set from the address, with the port where
that port is not the usual one for the scheme.
Building one from what arrived
$request = $factory->createServerRequest(); $request->getMethod(); // 'GET' $request->getUri()->getPath(); // '/users/42' $request->getHeaderLine('accept'); // 'application/json' $request->getRequestTarget(); // '/users/42?page=2' $request->getQueryParams(); // ['page' => '2'] $request->getCookieParams(); $request->getParsedBody();
The query parameters come from $_GET rather than from the URI, which is what PSR-7 allows
and what everything else in PHP does — so a rewritten URL and the parameters PHP parsed cannot
disagree.
Attributes
What the router matched arrives here, and anything else a middleware wants to hand along:
$request = $request->withAttribute('id', '42'); $request->getAttribute('id'); // '42' $request->getAttribute('page', '1'); // '1' — nothing under that name
Every change hands back a copy, so a middleware adding one does not change the request anybody else is holding.
A request class of your own
A controller can be handed a request of its own type, so what an endpoint expects is a class rather than a convention:
$factory->setRequestClass(UserRequest::class);
The class has to exist; UnknownServerRequestClassException says so when it does not.
Uploaded files
foreach ($request->getUploadedFiles() as $name => $file) { $file->getClientFilename(); // 'photo.jpg' $file->getClientMediaType(); // 'image/jpeg' $file->getSize(); // 20481 $file->getError(); // UPLOAD_ERR_OK $file->moveTo('/var/www/uploads/photo.jpg'); }
A file can only be moved once — moving it again throws UploadedFileAlreadyMovedException,
because the second call would otherwise fail in a way nobody expects. getStream() after a
move throws for the same reason.
Technical documentation
| Class | What it is |
|---|---|
ServerRequest |
the request, implementing Psr\Http\Message\ServerRequestInterface |
Factory\ServerRequest\ServerRequestFactory |
builds one from given parameters |
Factory\ServerRequest\ServerRequestFromGlobalsFactory |
builds one from what PHP was given |
Factory\ServerRequest\GivenServerRequestFromGlobalsFactory |
the same, as a named class |
UploadedFiles\UploadedFile |
one uploaded file, implementing UploadedFileInterface |
UploadedFiles\UploadedFileFactory |
turns $_FILES into those |
| Exception | Thrown when |
|---|---|
RequiredParamFromGlobalsNotFoundException |
$_SERVER is missing something a request needs |
ServerRequestMethodNotKnownException |
the method is not one HTTP has |
UnknownServerRequestClassException |
the request class named does not exist |
ServerParamNotSetException |
a parameter the factory was told to use is not there |
UploadedFileNotUploadedException |
the file did not arrive as an upload |
UploadedFileAlreadyMovedException |
it has been moved once already |
UploadedFileNotMovedException |
moving it failed |
getRequestTarget() is the path and the query string, taken from the URI — the path is
already an absolute one, so nothing is added to the front of it.
Benchmark
Measured with quillstack/benchmark on one request built from a method and an address with a query string, a thousand times. All four produce the same method, target and Host header. Runs are interleaved, each figure is the median of five, and PHP is 8.5.7.
| Version | |
|---|---|
| quillstack/server-request | v0.8.0 |
| nyholm/psr7 | 1.8.2 |
| laminas/laminas-diactoros | 3.8.0 |
| guzzlehttp/psr7 | 2.13.0 |
| Per request | Relative | |
|---|---|---|
| nyholm/psr7 | 6.2 µs | 0.46× |
| quillstack/server-request | 13.4 µs | — |
| laminas/laminas-diactoros | 14.4 µs | 1.07× |
| guzzlehttp/psr7 | 15.0 µs | 1.12× |
nyholm/psr7 builds one in less than half the time, and it is worth saying plainly rather
than burying: it is the leanest PSR-7 there is, and if constructing requests in a loop is what
your application does, that is the one to use.
Where this one spends the difference is on the parts that make it a server request rather than a message: server parameters, query parameters, cookies and uploaded files each arrive in a parameter bag rather than a bare array, and the URI goes through a conformance-tested parser. At thirteen microseconds, a request that builds one of these spends thirteen microseconds on it.
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/router — what matches one to a controller
- quillstack/uri — what takes the address apart
- quillstack/parameter-bag — what holds the parameters
- quillstack/response — what goes back
License
MIT. See LICENSE.