kambo/httpmessage
kambo/httpmessage is a PHP package for working with HTTP messages in an object-oriented way. It helps you build, inspect, and manipulate requests, responses, headers, and bodies consistently, making HTTP handling cleaner and easier to reuse across applications.
kambo/httpmessage is a bare PSR-7 implementation — no service provider, no facade, no config file, no Laravel auto-discovery. Installing it does nothing on its own; you only feel it when something in your dependency tree demands a Psr\Http\Message\RequestInterface or ResponseInterface.
composer require kambo/httpmessage
Where to look first
vendor/kambo/httpmessage/composer.json — check the psr/http-message constraint. This package predates PSR-17/PSR-18, so it almost certainly pins ^1.0. If any other package in your app needs psr/http-message:^2.0, Composer will refuse to resolve and this package is a non-starter for that project.Kambo\HttpMessage\ — expect the usual five: Request, Response, Stream, Uri, UploadedFile, plus a ServerRequest. Read the actual constructor signatures from the source; they are not guaranteed to match the canonical ($method, $uri, $headers, $body, $version) ordering used by Guzzle/Nyholm.*FactoryInterface implementation, plan on writing thin adapters yourself.First use case
The realistic entry point is "a library handed me an interface, not an object." For example, a PSR-15 middleware stack, an OAuth2 server implementation, or an HTTP client decorator that type-hints PSR-7. Smoke-test it in Tinker:
$response = new Kambo\HttpMessage\Response(200, ['Content-Type' => 'application/json']);
$response->getBody()->write(json_encode(['ok' => true]));
echo $response->getStatusCode(); // 200
echo $response->getHeaderLine('Content-Type');
echo (string) $response->getBody(); // {"ok":true}
If that round-trips cleanly, the package does its one job.
Treat it as an adapter layer, not a framework citizen. Laravel speaks Illuminate\Http\Request/Response (Symfony HttpFoundation). Kambo speaks PSR-7. Any use of this package is a translation at the boundary.
// PSR-7 response -> Laravel response (controller return value)
public function show(ResponseInterface $psrResponse): \Illuminate\Http\Response
{
return response(
(string) $psrResponse->getBody(),
$psrResponse->getStatusCode(),
$psrResponse->getHeaders() // note: values are string[]
);
}
Inbound: Laravel request -> PSR-7. The standard tool is symfony/psr-http-message-bridge, but its PsrHttpFactory constructor wants PSR-17 factories. Since Kambo likely ships none, write a small factory class that wraps the Kambo constructors and implements Psr\Http\Message\RequestFactoryInterface, StreamFactoryInterface, and UploadedFileFactoryInterface. Register it as a singleton so the bridge can be injected:
// app/Providers/AppServiceProvider.php
$this->app->singleton(\Psr\Http\Message\RequestFactoryInterface::class, \App\Http\KamboRequestFactory::class);
$this->app->singleton(\Psr\Http\Message\StreamFactoryInterface::class, \App\Http\KamboStreamFactory::class);
$this->app->singleton(\Symfony\Bridge\PsrHttpMessage\HttpFoundationFactoryInterface::class, ...);
$this->app->singleton(\Symfony\Bridge\PsrHttpMessage\PsrHttpFactory::class);
Immutability discipline. Every with* method returns a clone. The pattern is always reassign:
$request = $request
->withMethod('POST')
->withHeader('Authorization', 'Bearer ' . $token)
->withBody($stream);
Never assume $request->withHeader(...) mutated $request in place — that is the single most common source of "my header/body never arrived" bugs.
Outbound calls. Guzzle already returns PSR-7 responses, so you rarely need Kambo for outbound traffic. Where it earns its keep is when you need to construct the request yourself and hand it to a client that takes a RequestInterface:
$request = new Kambo\HttpMessage\Request('GET', new Kambo\HttpMessage\Uri('https://api.example.com/v1/items'));
$response = $client->sendRequest($request); // PSR-18 client
Testing. A lightweight, dependency-free PSR-7 stub is genuinely useful in unit tests — you can assert on what your code did to a message without booting Guzzle:
$fake = new Kambo\HttpMessage\Response(200, [], '{"id":1}');
$handler = new YourService($fake);
$this->assertSame('application/json', $handler->contentTypeOf());
Decorators for logging. Because everything is an interface, wrap a Kambo message in your own ResponseInterface decorator that logs on getBody()/getStatusCode(). That's the clean extension point — don't subclass and override with* methods, since the clone will lose your subclass's state.
psr/http-message 2.0 added native return types to the interfaces. A 2018-era implementation almost certainly does not satisfy ^2.0. Pin "psr/http-message": "^1.0" and accept that you're blocking upgrades for the rest of the tree. Run composer why psr/http-message before committing to this.Type $x = null) deprecations, and for __toString() implementations that don't handle exceptions — PHP 7.4+ forbids throwing from __toString().write(), the pointer sits at the end. getContents() returns an empty string. Either call rewind() first or cast: (string) $response->getBody(). Kambo's Stream::__toString() may or may not rewind for you — verify before relying on it.getHeaders() shape. getHeader() returns string[], getHeaderLine() returns a comma-joined string, getHeaders() returns string[][]. Passing getHeaders() straight into Laravel's response() works; passing getHeader() does not.withUri($uri, $preserveHost = false) is the only way the Host header is re-derived. If you build a request and then swap the URI, the old host sticks unless you pass true or don't preserve it.ServerRequest::getUploadedFiles() should return UploadedFileInterface[] with nested arrays mirroring name="form[file]". Flat implementations silently break multi-file forms.var_dump or json_encode a PSR-7 message — bodies are streams and won't serialize. Always inspect via getStatusCode(), getHeaders(), and (string) $body.RequestInterface, ResponseInterface) rather than the concrete Kambo classes, so you can swap in nyholm/psr7 later without touching call sites.nyholm/psr7 (tiny, actively maintained, PSR-17 compliant) or guzzlehttp/psr7 (already present if you use Guzzle). Choose kambo/httpmessage only when it's already transitively installed and you need one more message object, or when the "zero extra dependencies" trade-off genuinely matters. The absence of PSR-17 factories alone usually costs more in glue code than the package saves.How can I help you explore Laravel packages today?