PSR-17: HTTP Factories
debt(d5/e3/b3/t5)
Closest to 'specialist tool catches it' (d5) — deptrac and phpstan (listed in detection_hints.tools) can detect direct instantiation of concrete PSR-7 classes via architecture rules, but it's not a default lint.
Closest to 'simple parameterised fix' (e3) — quick_fix says inject factory interfaces instead of instantiating concrete classes; it's a pattern replacement but typically touches constructors and call sites across a component.
Closest to 'localised tax' (b3) — applies to HTTP-handling code; the factory abstraction is a small persistent commitment for the HTTP layer but doesn't shape the whole system.
Closest to 'notable trap' (t5) — the misconception (confusing PSR-17 as an extension of PSR-7 rather than a companion factory standard) is a well-documented gotcha that most PHP devs eventually learn.
Also Known As
TL;DR
Explanation
PSR-17 provides factory interfaces for every PSR-7 type: RequestFactoryInterface, ResponseFactoryInterface, ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface, and UriFactoryInterface. By accepting a PSR-17 factory rather than hard-coding new GuzzleHttp\Psr7\Response(), library code works with any PSR-7 implementation. Particularly valuable in middleware and framework internals that construct HTTP messages. Implementations include nyholm/psr7 (fastest, smallest) and guzzlehttp/psr7. PSR-17 completes the HTTP abstraction layer begun by PSR-7 and extended by PSR-15.
Common Misconception
Why It Matters
Common Mistakes
- Instantiating PSR-7 objects directly (new GuzzleHttp\Psr7\Request()) — tightly couples to one library.
- Not injecting factories — hardcoded factory calls prevent swapping PSR-7 implementations.
- Confusing PSR-7 (HTTP message interfaces) with PSR-17 (factory interfaces for creating them).
- Not using a discovery package — manually resolving which factory implementation is installed.
Code Examples
// Direct PSR-7 instantiation — coupled to Guzzle:
$request = new GuzzleHttp\Psr7\Request('GET', $url);
// PSR-17 factory — implementation-agnostic:
public function __construct(private RequestFactoryInterface $factory) {}
$request = $this->factory->createRequest('GET', $url);
// Swap Guzzle for Nyholm PSR-7 by changing one DI binding
// PSR-17 HTTP Factories — create PSR-7 message objects
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
// Inject factories rather than instantiating directly
class OrderController {
public function __construct(
private ResponseFactoryInterface $responseFactory,
private StreamFactoryInterface $streamFactory,
) {}
public function show(int $id): ResponseInterface {
$order = Order::findOrFail($id);
$body = $this->streamFactory->createStream(json_encode($order));
return $this->responseFactory->createResponse(200)
->withHeader('Content-Type', 'application/json')
->withBody($body);
}
}
// Implementations: nyholm/psr7, guzzlehttp/psr7, laminas-diactoros