php-http-psr
SkillCommunicationUse when building framework-agnostic PHP HTTP code — PSR-7/15/17/18 messages, middleware, factories, clients. Do NOT use for Laravel HTTP or Symfony HttpFoundation (not PSR-7).
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the php-http-psr skill
What this skill tells your AI
The instructions your AI receives, as published by fusengine/agents in plugins/php-expert/skills/php-http-psr/SKILL.md and read by ahel’s review.
Includes guidance on reference implementations (nyholm/psr7, guzzlehttp/psr7, laminas-diactoros) and how to choose between them, plus a complete middleware-pipeline template.
Do NOT use this skill for Laravel's own HTTP layer (route to laravel-expert instead) or for Symfony's HttpFoundation component, which is a different, non-PSR-7 HTTP model — bridge the two via symfony/psr-http-message-bridge when needed (see references/implementations.md).
PHP HTTP — PSR-7 / 15 / 17 / 18
Agent Workflow (MANDATORY)
Before ANY implementation, spawn 3 agents in parallel, one Agent call each with a name:
- fuse-ai-pilot:explore-codebase - Detect the PSR-7 implementation already in use (composer.json)
- fuse-ai-pilot:research-expert - Verify current interface versions on php-fig.org + Packagist
- mcp__context7__query-docs - Check the chosen implementation's factory API
After implementation, run fuse-ai-pilot:sniper for validation.
Overview
These four PSR standards let libraries handle HTTP without coupling to any framework. Code depends on interfaces (Psr\Http\*); a concrete implementation is injected.
| Standard | Package | Provides |
|---|---|---|
| PSR-7 | psr/http-message | Immutable HTTP messages: MessageInterface, RequestInterface, ResponseInterface, ServerRequestInterface, StreamInterface, UriInterface, UploadedFileInterface |
| PSR-15 | psr/http-server-handler + psr/http-server-middleware | RequestHandlerInterface, MiddlewareInterface — the middleware pipeline |
| PSR-17 | psr/http-factory | Factories that create PSR-7 objects without naming a concrete class |
| PSR-18 | psr/http-client | ClientInterface::sendRequest() — send a PSR-7 request, get a PSR-7 response |
Critical Rules
- Messages are immutable - Every
with*()returns a NEW instance; the original is unchanged.$r->withHeader(...)alone is a no-op — reassign. - Depend on interfaces, not classes - Type-hint
ResponseInterface, inject aResponseFactoryInterface. NEVERnew Response()inside reusable code. - Streams are NOT immutable -
StreamInterfacewraps a real resource; use read-only streams for requests/responses. - PSR-18 4xx/5xx are NOT errors - A client MUST return them as normal responses; it throws only on transport failure (
NetworkExceptionInterface) or malformed requests (RequestExceptionInterface). - Headers are case-insensitive -
getHeaderLine('foo')==getHeaderLine('FOO'); original case is preserved ingetHeaders().
Architecture
src/
├── Http/
│ ├── Middleware/ # implements Psr\Http\Server\MiddlewareInterface
│ │ ├── ErrorHandlerMiddleware.php
│ │ └── AuthMiddleware.php
│ ├── Handler/ # implements Psr\Http\Server\RequestHandlerInterface
│ │ └── Dispatcher.php # the pipeline runner
│ └── Client/ # wraps a Psr\Http\Client\ClientInterface
└── interfaces/ # your own contracts (SOLID)
→ See middleware-pipeline.md for a complete runnable pipeline
Reference Guide
Concepts
| Topic | Reference | When to Consult |
|---|---|---|
| Messages | psr7-messages.md | Reading/building requests, responses, streams, URIs |
| Middleware | psr15-middleware.md | Building a middleware + handler pipeline |
| Factories | psr17-factories.md | Creating PSR-7 objects implementation-agnostically |
| HTTP Client | psr18-client.md | Sending outbound requests, exception handling |
| Implementations | implementations.md | Choosing nyholm / guzzle / laminas + PSR-18 clients |
Templates
| Template | When to Use |
|---|---|
| middleware-pipeline.md | Building a PSR-15 dispatcher with middleware queue |
Quick Reference
Immutable update (reassign!)
$response = $response
->withStatus(201)
->withHeader('Content-Type', 'application/json');
Minimal middleware
final class AuthMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (!$request->hasHeader('Authorization')) {
return $this->responseFactory->createResponse(401);
}
return $handler->handle($request);
}
}
PSR-17 factory (no concrete class named)
$request = $requestFactory->createRequest('GET', 'https://api.example.com');
$request = $request->withBody($streamFactory->createStream('{"ping":true}'));
→ See psr17-factories.md for all six factory interfaces
Best Practices
DO
- Inject a
ResponseFactoryInterfaceso middleware never names a concrete class - Put an exception-catching middleware FIRST in the queue (PSR-15 recommendation)
- Use
nyholm/psr7when you want a lightweight, strict PSR-7 + PSR-17 in one package - Reassign the result of every
with*()call
DON'T
- Mutate a message in place —
with*()returns a new object - Treat a 404/500 response from a PSR-18 client as an exception
- Confuse Symfony
HttpFoundationwith PSR-7 — they are different; bridge viasymfony/psr-http-message-bridge - Hardcode
new GuzzleHttp\Psr7\Response()in library code — depend on the factory
Signals
- GitHub stars
- 25
- Forks
- 4
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
php-http-psr- Source
- github.com/fusengine/agents