berlioz/http-message
berlioz/http-message offers PSR-7 compliant HTTP message classes for PHP: immutable requests, responses, URIs, streams, and uploaded files. It works seamlessly with Laravel and other frameworks, giving developers a clean, standards-based foundation for handling HTTP requests and responses.
Install the package directly, or let it come in transitively with berlioz/http-core:
composer require berlioz/http-message
First place to look: src/Http/Message/ in the monorepo (README.md and the HttpFactory class). Since this is a pure PSR-7/PSR-17 component with zero framework dependencies, you can read the whole surface area in one sitting — it is factory + message classes + streams + body parsers.
The one object you need: Berlioz\Http\Message\HttpFactory implements all six PSR-17 factory interfaces (RequestFactoryInterface, ResponseFactoryInterface, ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface, UriFactoryInterface). Type-hint against the PSR interfaces in your own code, and instantiate HttpFactory only at the composition root:
use Berlioz\Http\Message\HttpFactory;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
$factory = new HttpFactory();
$response = $factory->createResponse(200)
->withHeader('Content-Type', 'application/json');
$stream = $factory->createStream(json_encode(['ok' => true]));
return $response->withBody($stream);
First use case to try: build a ServerRequest from globals, read the parsed body, and return a JSON Response. That single loop exercises factories, streams, body parsing, and header immutability — i.e. 90% of what you'll do day-to-day. Once that works, drop the same factory into a PSR-15 middleware stack or a PSR-18 client call and you've validated interoperability.
1. Factory-as-service, interfaces everywhere. Register HttpFactory in the container (Berlioz's ServiceContainer auto-wires by reflection, so binding the six PSR-17 interfaces to the one concrete class is a two-line provider). Every controller, middleware, and listener then depends on ResponseFactoryInterface / StreamFactoryInterface rather than the concrete class. This keeps your code swappable with nyholm/psr7, guzzlehttp/psr7, etc.
2. Controller return conventions in http-core. You rarely build responses by hand inside a controller. HttpApp's ControllerHandler maps return values: ResponseInterface passes through, null/empty → 204, scalar → 200, anything else → JSON. Know this map, because it decides when you must return a ResponseInterface (custom headers, non-200 statuses, streaming) and when you can just return $array;.
3. Immutable message pipelines. PSR-7 messages are immutable, so the idiomatic Berlioz middleware pattern is to return the new instance, never mutate in place:
final class JsonErrorMiddleware implements MiddlewareInterface
{
public function __construct(private ResponseFactoryInterface $factory, private StreamFactoryInterface $streams) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
return $handler->handle($request);
} catch (\Throwable $e) {
return $this->factory->createResponse(500)
->withHeader('Content-Type', 'application/json')
->withBody($this->streams->createStream(json_encode(['error' => $e->getMessage()])));
}
}
}
4. Body parsing is pluggable. http-message ships body parsers so $request->getParsedBody() is useful without hand-rolling json_decode. For content types the default parsers don't cover (e.g. application/x-ndjson, vendor +json suffixes), register a custom parser rather than decoding inside the controller — that keeps parsing testable and applied uniformly to every route.
5. Streaming and file responses. Use StreamFactoryInterface::createStreamFromFile() / createStreamFromResource() instead of file_get_contents() when serving downloads or proxying large payloads. Pair with UploadedFileFactoryInterface for tests, so you can exercise upload handling without a real HTTP request.
6. Testing. Because there's no framework coupling, your unit tests can construct requests directly:
$request = (new HttpFactory())->createServerRequest('POST', '/api/items')
->withHeader('Content-Type', 'application/json')
->withBody((new HttpFactory())->createStream('{"name":"widget"}'));
This is the biggest ergonomic win — you can assert on middleware and controllers with zero boot of Core, HttpApp, or the router.
7. Interop with the client. berlioz/http-client (PSR-18) and this package share the same PSR-7 message classes, so responses from Session/client calls can be returned directly from a controller by http-core, or fed into HtmlSelector for scraping. That symmetry is a real workflow win when writing proxy/aggregator endpoints.
$response->withHeader(...) returns a new object; the old one is untouched. In middleware chains, forgetting $request = $request->withAttribute(...) silently drops your data. Always reassign.StreamInterface has a cursor; reading it twice returns an empty string unless you rewind(). If two middlewares inspect the raw body, the second sees nothing. Either cache the parsed body via withParsedBody() or rewind explicitly.withBody() does not rewind. After $response->withBody($stream), the stream position is wherever you left it. If you wrote to it, rewind() before returning or the client gets an empty body.getHeaderLine('Content-Type') === $arrayKey. Use the message API, never getHeaders()[$key].ResponseFactoryInterface. Bind the concrete class in DI when you need the extra methods; otherwise stick to interfaces.berlioz.* touches it. If you're debugging body-parser or stream behaviour, look at how the container wires HttpFactory in http-core, not at config.default.json.DebugHandler captures request/response data for the debug toolbar, but it hooks in at http-core. When dumping messages during local dev, json_encode($request) won't work — use the __toString() for headers plus (string) $body for content.nyholm/psr7 or guzzlehttp/psr7 have larger communities, more StackOverflow answers, and identical PSR-17 behaviour. The only Berlioz-specific reason to adopt it is that http-core, http-client, and html-selector are typed against it.declare(strict_types=1) required. If you contribute or extend, match the license header block and one-class-per-file convention — the monorepo enforces it in review.How can I help you explore Laravel packages today?