modufolio / http
PSR-7 HTTP message implementation with additional utilities
Requires
- php: ^8.2
- psr/http-factory: ^1.0
- psr/http-message: ^2.0
Requires (Dev)
- php-http/psr7-integration-tests: ^1.5
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^10.5 || ^11
This package is not auto-updated.
Last update: 2026-08-27 15:28:25 UTC
README
PSR-7 HTTP message implementation with additional utilities for building HTTP applications.
Conformance is verified against the php-http/psr7-integration-tests suite in addition to this package's own tests.
Installation
composer require modufolio/http
Requirements
- PHP 8.2 or higher
- PSR-7 HTTP Message interfaces (
psr/http-message^2.0) - PSR-17 HTTP Factory interfaces (
psr/http-factory^1.0)
Features
This package provides a complete PSR-7 implementation including:
- Request/Response: Full PSR-7 HTTP message implementation
- ServerRequest: Server-side request handling with superglobal parsing and automatic body parsing
- Uri: URI parsing and manipulation
- Stream: Stream implementation for request/response bodies
- UploadedFile: File upload handling with validation
- Emitter: Response emitters for sending HTTP responses
- ServerRequestCreator: Factory for creating ServerRequest from globals
- ServerRequestCreatorFactory: Static factory for creating ServerRequestCreator instances
- Static Response Helpers: Convenient methods for creating common HTTP responses (JSON, HTML, redirects, etc.)
- Body Parsers: Built-in parsers for JSON, XML, and form data with extensible parser registration
Usage
Creating a Response
use Modufolio\Psr7\Http\Response; $response = new Response(200, ['Content-Type' => 'application/json'], '{"message":"Hello"}');
Creating a ServerRequest from globals
ServerRequestCreator takes the four PSR-17 factories it needs as constructor
arguments. ServerRequestCreatorFactory wires them up for you:
use Modufolio\Psr7\Http\Factory\ServerRequestCreatorFactory; $creator = ServerRequestCreatorFactory::create(); $request = $creator->fromGlobals();
To supply your own factories, construct it directly:
use Modufolio\Psr7\Http\Factory\Psr17Factory; use Modufolio\Psr7\Http\ServerRequestCreator; $psr17Factory = new Psr17Factory(); $creator = new ServerRequestCreator( $psr17Factory, // ServerRequestFactoryInterface $psr17Factory, // UriFactoryInterface $psr17Factory, // UploadedFileFactoryInterface $psr17Factory // StreamFactoryInterface );
Working with Streams
use Modufolio\Psr7\Http\Stream; $stream = Stream::create('Hello World'); echo $stream->getContents(); // "Hello World"
Static Response Helpers
use Modufolio\Psr7\Http\Response; // Create JSON responses $response = Response::json(['message' => 'Hello World'], 200); // Create HTML responses $response = Response::html('<h1>Hello World</h1>'); // Create redirects $response = Response::redirect('https://example.com', 302); // Create an empty 204 response $response = Response::empty(); // Create error responses $response = Response::unauthorized('Invalid credentials'); $response = Response::unavailable('Down for maintenance'); $response = Response::tooManyRequests('Rate limit exceeded');
Response::json() accepts either an array to encode or a pre-encoded JSON
string, and throws JsonException if a string is not valid JSON.
Body Parsing
ServerRequest::getParsedBody() parses the request body lazily, based on the
Content-Type header rather than the request method. Parsers ship for
application/json, application/xml, text/xml and
application/x-www-form-urlencoded. Media types with a structured syntax
suffix (RFC 6839) fall back to the parser for that suffix, so
application/vnd.api+json is handled by the JSON parser.
use Modufolio\Psr7\Http\Factory\ServerRequestCreatorFactory; $creator = ServerRequestCreatorFactory::create(); $request = $creator->fromGlobals(); $parsedBody = $request->getParsedBody(); // Content-Type helpers (not part of PSR-7) $request->getContentType(); // "application/json; charset=utf-8" $request->getMediaType(); // "application/json"
Reading the body to parse it rewinds the stream afterwards, so
$request->getBody()->getContents() still returns the full raw body.
A body set explicitly with withParsedBody() is always returned as-is and is
never re-parsed, including when it is null or an empty array.
Errors
The JSON parser fails loud rather than returning null, so a body that
announces itself as JSON and then isn't can be told apart from an absent body:
| Condition | Result |
|---|---|
| Malformed JSON | throws JsonException |
| Nesting deeper than 50 levels | throws JsonException |
| Body larger than 1MB | throws PayloadTooLargeException |
| Empty or whitespace-only body | null |
Valid JSON that is not an array (5, "x") |
null |
Handle these where you can turn them into a response:
use Modufolio\Psr7\Http\Exception\PayloadTooLargeException; use Modufolio\Psr7\Http\Response; try { $data = $request->getParsedBody(); } catch (PayloadTooLargeException) { return Response::json(['error' => 'Payload too large'], 413); } catch (\JsonException) { return Response::json(['error' => 'Malformed JSON'], 400); }
The XML and form parsers stay fail-soft and return null on unparseable
input: libxml reports partial successes and recoverable errors in ways that do
not map cleanly onto a single "this was invalid" signal.
Registering a custom parser
$request->registerMediaTypeParser('text/csv', function (string $input): array { return array_map( static fn (string $line): array => str_getcsv($line, ',', '"', '\\'), explode("\n", trim($input)) ); }); $rows = $request->getParsedBody();
Parsers must return an array, an object, or null; anything else raises a
RuntimeException. Registered parsers are carried over to requests derived
with withHeader(), withAttribute() and the other with*() methods.
Note that registerMediaTypeParser() mutates the request rather than
returning a modified copy.
Emitting Responses
use Modufolio\Psr7\Http\Response; use Modufolio\Psr7\Http\Emitter; $response = Response::json(['status' => 'success']); $emitter = new Emitter(); $emitter->emit($response); // Sends headers and body to client
Development
composer test # run the test suite composer test:coverage # run with an HTML coverage report composer stan # run PHPStan
The test suite is split into two PHPUnit suites, which can be run independently:
vendor/bin/phpunit --testsuite Http # unit tests vendor/bin/phpunit --testsuite Integration # PSR-7 conformance tests
License
MIT