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.
composer require modufolio/http- 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)
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
use Modufolio\Psr7\Http\Response;
$response = new Response(200, ['Content-Type' => 'application/json'], '{"message":"Hello"}');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
);use Modufolio\Psr7\Http\Stream;
$stream = Stream::create('Hello World');
echo $stream->getContents(); // "Hello World"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.
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.
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.
$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.
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 clientcomposer test # run the test suite
composer test:coverage # run with an HTML coverage report
composer stan # run PHPStanThe 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 testsMIT