PSR-7 HTTP message implementation with additional utilities

Maintainers

Package info

github.com/modufolio/Psr7

pkg:composer/modufolio/http

Transparency log

Statistics

Installs: 667

Dependents: 4

Suggesters: 0

Stars: 0

Open Issues: 0

v0.2.0 2026-08-27 15:27 UTC

This package is not auto-updated.

Last update: 2026-08-27 15:28:25 UTC


README

CI License: MIT codecov

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