PSR-7: HTTP Message Interface
debt(d5/e5/b5/t7)
Closest to 'specialist tool catches' (d5), phpstan and deptrac can detect concrete class type-hints instead of PSR-7 interfaces, and the immutability misuse (ignoring return of withHeader) is catchable by static analysis but often slips past defaults.
Closest to 'touches multiple files / significant refactor' (e5), swapping concrete request/response classes for PSR-7 interfaces and introducing PSR-17 factories typically requires changes across controllers, middleware, and DI configuration in one component.
Closest to 'persistent productivity tax' (b5), PSR-7 applies across web/api/cli contexts and shapes how middleware, handlers, and HTTP plumbing are written throughout the app; immutability discipline is an ongoing tax but not architecture-defining since it's an interop standard.
Closest to 'serious trap' (t7), the misconception that PSR-7 objects are mutable contradicts how most framework request objects work elsewhere — $request->withHeader() silently does nothing if you don't capture the return value, a behaviour opposite to typical PHP object semantics.
Also Known As
TL;DR
Explanation
PSR-7 defines interfaces for HTTP messages: RequestInterface, ServerRequestInterface (incoming requests with parsed body, cookies, uploaded files), ResponseInterface, and supporting types (UriInterface, StreamInterface, UploadedFileInterface). All PSR-7 objects are immutable — methods that modify state return new instances (withHeader(), withBody()). This enables safe middleware pipelines where each layer receives and returns message objects. Implementations include Guzzle PSR-7, Nyholm PSR-7 (lightest), and Laminas Diactoros. PSR-7 is the foundation of PSR-15 middleware.
Common Misconception
Why It Matters
Common Mistakes
- Not using the return value of withHeader(), withBody(), etc — PSR-7 messages are immutable; modifications return new instances.
- Type-hinting against concrete classes (GuzzleHttp\Psr7\Request) instead of the PSR-7 interfaces.
- Forgetting that getQueryParams() returns parsed query params but getParsedBody() returns POST data — different methods.
- Not using a PSR-17 factory to create PSR-7 objects — direct instantiation couples to one library.
Code Examples
// Mutating immutable PSR-7 object — original unchanged:
$response->withHeader('Content-Type', 'application/json'); // Returns new instance!
// $response is unchanged — the new instance is discarded
// Correct:
$response = $response->withHeader('Content-Type', 'application/json');
return $response->withBody($stream);
// PSR-7 — immutable HTTP message objects
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
// All mutating methods return NEW instances — original unchanged
function addJsonHeader(ResponseInterface $response): ResponseInterface {
return $response->withHeader('Content-Type', 'application/json');
// ^^ returns new object with header added
}
// Read request data
public function handle(ServerRequestInterface $request): ResponseInterface {
$body = (string) $request->getBody();
$data = json_decode($body, true);
$userId = $request->getAttribute('user_id');
$query = $request->getQueryParams();
$cookies = $request->getCookieParams();
}