Why a Custom HTTP Layer?
PHP has no shortage of HTTP clients, but each one exposes a different API. Switching from Guzzle to Symfony’s HttpClient means rewriting every call site. This package solves that problem by placing a thin abstraction over the driver of your choice:- Framework agnostic — the same request code works in Laravel, Symfony, or vanilla PHP without modification.
- Pluggable drivers — switch between cURL, Guzzle, Symfony, or your own driver with a single configuration change.
- Streaming first — first-class support for streamed responses, which is essential for LLM token-by-token output and server-sent events.
- Middleware pipeline — add retry logic, circuit breakers, idempotency keys, or debug logging without touching driver internals.
- Immutable value objects — requests, responses, and configuration are all immutable. Every
with*()call returns a new instance.
Core Types
The package is built around a handful of focused types:| Type | Role |
|---|---|
CanSendHttpRequests | Top-level transport contract |
HttpClient | Default implementation of CanSendHttpRequests |
HttpRequest | Immutable request value object (URL, method, headers, body, options) |
PendingHttpResponse | Deferred execution wrapper returned by send() |
HttpResponse | Buffered or streamed response value object |
HttpClientBuilder | Explicit composition entry point for building clients |
HttpClientConfig | Typed driver configuration (timeouts, chunk sizes, error handling) |
Request Lifecycle
Every request follows the same path through the system:- You create an
HttpRequestwith a URL, method, headers, and body. - You pass it to
HttpClient::send(), which returns aPendingHttpResponse. - The pending response is lazy — no network call happens until you call
get()(for buffered responses) orstream()(for streamed responses). - The driver executes the request through the middleware pipeline and returns an
HttpResponse.
Architecture
The package follows a layered architecture:HttpClient is the public entry point. It delegates to HttpClientRuntime, which wires together the driver, middleware stack, and event dispatcher.
Middleware layer. A pipeline of HttpMiddleware implementations that can inspect or modify requests before they reach the driver and responses after they come back. Middleware is processed in order for requests and in reverse order for responses.
Driver layer. Drivers implement CanHandleHttpRequest and adapt a specific HTTP library to the common interface. The bundled drivers are:
| Driver | Library | Dependency |
|---|---|---|
CurlDriver | PHP cURL extension | None (built-in) |
GuzzleDriver | Guzzle HTTP | guzzlehttp/guzzle |
SymfonyDriver | Symfony HttpClient | symfony/http-client |
MockHttpDriver | Built-in test double | None |
Immutability
All core types are immutable. When you call awith*() method on a request, response, config, or client, you receive a new instance. Always reassign the result:
Pooling
Concurrent request execution has been extracted to its own package. Seepackages/http-pool for HttpPool, PendingHttpPool, and HttpPoolBuilder. The request and response collection types (HttpRequestList and HttpResponseList) remain in this package.