thingston / psr18
Implementation of PSR-18 standard for sending HTTP requests and receiving HTTP responses.
Requires
- php: >=8.5
- ext-curl: *
- psr/http-client: ^1.0
- psr/log: ^3.0
- thingston/psr17: ^1.0
Requires (Dev)
- ext-xdebug: *
- friendsofphp/php-cs-fixer: ^3.89
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^13.1
Provides
This package is auto-updated.
Last update: 2026-06-28 14:52:58 UTC
README
Implementation of PSR-18 standard for sending HTTP requests and receiving HTTP responses.
Requirements
- PHP 8.5 or newer
- PHP cURL extension
Installation
composer require thingston/psr18
Quick start
<?php use Thingston\Psr17\RequestFactory; use Thingston\Psr18\Client; $request = (new RequestFactory())->createRequest('GET', 'https://api.example.com/data'); $response = (new Client())->sendRequest($request); echo $response->getStatusCode(); // 200 echo (string) $response->getBody();
Configuration
Client accepts optional named parameters for every concern. All policy parameters default to null (disabled):
use Thingston\Psr18\AuthPolicy; use Thingston\Psr18\CircuitBreakerPolicy; use Thingston\Psr18\Client; use Thingston\Psr18\CookieJar; use Thingston\Psr18\Curl\RequestOptions; use Thingston\Psr18\Curl\ResponseOptions; use Thingston\Psr18\ProxyOptions; use Thingston\Psr18\RedirectPolicy; use Thingston\Psr18\RetryPolicy; use Thingston\Psr18\TimeoutPolicy; use Thingston\Psr18\TlsPolicy; $client = new Client( requestOptions: new RequestOptions([RequestOptions::USER_AGENT => 'my-app/1.0']), responseOptions: new ResponseOptions([ResponseOptions::ENCODING => '']), retryPolicy: new RetryPolicy(maxRetries: 3, delayMilliseconds: 200), redirectPolicy: new RedirectPolicy(followRedirects: true, maxRedirects: 10), tlsPolicy: new TlsPolicy(verifyPeer: true, verifyHost: true), timeoutPolicy: new TimeoutPolicy(connectTimeoutMs: 5_000, transferTimeoutMs: 30_000), authPolicy: AuthPolicy::bearer('my-token'), proxyOptions: new ProxyOptions('http://proxy.internal:3128'), cookieJar: new CookieJar(), circuitBreaker: new CircuitBreakerPolicy(failureThreshold: 5, resetTimeoutSeconds: 60), logger: $psr3Logger, middleware: [$myMiddleware], );
Redirect policy
Controls whether and how the client follows HTTP redirects. By default the client does not follow redirects unless a RedirectPolicy is provided.
use Thingston\Psr18\Client; use Thingston\Psr18\RedirectPolicy; // Follow up to 10 redirects (default) $client = new Client(redirectPolicy: new RedirectPolicy()); // Follow redirects while preserving the HTTP method on all redirect types $client = new Client(redirectPolicy: new RedirectPolicy(followRedirects: true, preserveMethod: true)); // Disable redirect following — caller receives the 3xx response $client = new Client(redirectPolicy: new RedirectPolicy(followRedirects: false));
| Parameter | Type | Default | Effect |
|---|---|---|---|
$followRedirects |
bool |
true |
Sets CURLOPT_FOLLOWLOCATION |
$maxRedirects |
int |
10 |
Sets CURLOPT_MAXREDIRS |
$preserveMethod |
bool |
false |
Sets CURLOPT_POSTREDIR = CURL_REDIR_POST_ALL |
RedirectPolicy::resolveLocation(string $location, RequestInterface $request): string is a utility for resolving relative Location header values when handling redirects manually.
Retry policy
Retries are opt-in and default to idempotent methods only (GET, HEAD, OPTIONS, PUT, DELETE, TRACE). The default failure set retries common transient cURL errors and HTTP 408, 425, 429, 500, 502, 503, 504.
use Thingston\Psr18\Client; use Thingston\Psr18\RetryPolicy; // Constant 200 ms delay, up to 3 retries $client = new Client( retryPolicy: new RetryPolicy(maxRetries: 3, delayMilliseconds: 200), ); // Exponential backoff: 100 ms → 200 ms → 400 ms, capped at 2 000 ms $client = new Client( retryPolicy: new RetryPolicy( maxRetries: 4, delayMilliseconds: 100, backoffMultiplier: 2.0, maxDelayMilliseconds: 2_000, ), ); // Retry only on GET and HEAD, and only on 503 $client = new Client( retryPolicy: new RetryPolicy( maxRetries: 3, methods: ['GET', 'HEAD'], statusCodes: [503], ), );
| Parameter | Type | Default | Effect |
|---|---|---|---|
$maxRetries |
int |
0 |
Maximum retry attempts |
$delayMilliseconds |
int |
100 |
Base delay between retries |
$backoffMultiplier |
float |
1.0 |
1.0 = constant; 2.0 = exponential doubling |
$maxDelayMilliseconds |
int |
0 |
Cap on computed delay; 0 = no cap |
$methods |
string[] |
GET,HEAD,OPTIONS,PUT,DELETE,TRACE |
HTTP methods eligible for retry |
$statusCodes |
int[] |
408,425,429,500,502,503,504 |
HTTP status codes that trigger a retry |
$errorCodes |
int[]|null |
transient cURL errors | cURL error codes that trigger a retry; null = built-in defaults |
When the server returns a Retry-After header on a retryable response, the client honours it (capped at 60 s) instead of using the computed delay.
TLS policy
Groups SSL/TLS configuration into a named object rather than raw cURL option arrays.
use Thingston\Psr18\Client; use Thingston\Psr18\TlsPolicy; // Custom CA bundle and client certificate $client = new Client( tlsPolicy: new TlsPolicy( verifyPeer: true, verifyHost: true, caInfo: '/etc/ssl/certs/ca-bundle.crt', certificate: '/path/to/client.crt', privateKey: '/path/to/client.key', ), ); // Disable verification (testing only — never in production) $client = new Client( tlsPolicy: new TlsPolicy(verifyPeer: false, verifyHost: false), );
Timeout policy
Separates connection timeout from transfer timeout.
use Thingston\Psr18\Client; use Thingston\Psr18\TimeoutPolicy; $client = new Client( timeoutPolicy: new TimeoutPolicy( connectTimeoutMs: 3_000, // fail fast if the host is unreachable transferTimeoutMs: 30_000, // allow 30 s for the full response ), );
| Parameter | Type | Default | Effect |
|---|---|---|---|
$connectTimeoutMs |
int |
10_000 |
Sets CURLOPT_CONNECTTIMEOUT_MS |
$transferTimeoutMs |
int |
0 (unlimited) |
Sets CURLOPT_TIMEOUT_MS; 0 disables the transfer timeout |
TimeoutPolicy takes precedence over any equivalent options set in RequestOptions.
Auth policy
Handles Basic, Bearer, and Digest authentication without manually building Authorization headers.
use Thingston\Psr18\AuthPolicy; use Thingston\Psr18\Client; $client = new Client(authPolicy: AuthPolicy::basic('alice', 's3cr3t')); $client = new Client(authPolicy: AuthPolicy::bearer('eyJhbGciOi...')); $client = new Client(authPolicy: AuthPolicy::digest('alice', 's3cr3t'));
Basic and Bearer auth inject an Authorization header. Digest auth uses curl's native challenge-response mechanism via CURLOPT_HTTPAUTH.
Proxy options
use Thingston\Psr18\Client; use Thingston\Psr18\ProxyOptions; $client = new Client( proxyOptions: new ProxyOptions( proxy: 'http://proxy.corp:3128', proxyType: CURLPROXY_HTTP, // default; use CURLPROXY_SOCKS5 etc. as needed userPassword: 'proxyuser:proxypass', noProxy: 'localhost,127.0.0.1,.internal', ), );
Concurrent requests
MultiClient wraps curl_multi_* and sends multiple requests in parallel. It accepts the same policy parameters as Client except retryPolicy, circuitBreaker, logger, and middleware.
use Thingston\Psr17\RequestFactory; use Thingston\Psr18\MultiClient; $factory = new RequestFactory(); $client = new MultiClient(); $responses = $client->sendAll([ $factory->createRequest('GET', 'https://api.example.com/users'), $factory->createRequest('GET', 'https://api.example.com/orders'), $factory->createRequest('GET', 'https://api.example.com/products'), ]); // Responses are indexed in the same order as the requests echo $responses[0]->getStatusCode();
Cookie jar
Persists session cookies across requests using CURLOPT_COOKIEFILE and CURLOPT_COOKIEJAR. A temporary file is created automatically when no path is given and deleted on destruction.
use Thingston\Psr18\Client; use Thingston\Psr18\CookieJar; // Temporary file, automatically deleted when the CookieJar goes out of scope $jar = new CookieJar(); $client = new Client(cookieJar: $jar); // Persistent file $client = new Client(cookieJar: new CookieJar('/var/run/session-cookies.txt'));
CookieJar::getPath(): string returns the underlying file path. CookieJar::clear(): void truncates the file, discarding all stored cookies without changing the path.
Circuit breaker
Opens the circuit after a configurable number of consecutive failures and throws immediately — without attempting the request — until the reset timeout elapses.
use Thingston\Psr18\CircuitBreakerPolicy; use Thingston\Psr18\Client; $circuitBreaker = new CircuitBreakerPolicy( failureThreshold: 5, // open after 5 consecutive failures resetTimeoutSeconds: 30, // try again after 30 s ); $client = new Client(circuitBreaker: $circuitBreaker);
States: closed (normal) → open (blocking) → half-open (probe allowed) → closed.
Middleware
A MiddlewareInterface pipeline wraps sendRequest() with cross-cutting behaviour. Middleware runs in registration order (first registered = outermost wrapper).
use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Thingston\Psr18\Client; use Thingston\Psr18\MiddlewareInterface; final class RequestIdMiddleware implements MiddlewareInterface { public function process(RequestInterface $request, callable $next): ResponseInterface { return $next($request->withHeader('X-Request-Id', uniqid())); } } $client = new Client(middleware: [new RequestIdMiddleware()]);
PSR-3 logging
Inject any Psr\Log\LoggerInterface to receive structured log entries for requests, responses, retries, and errors. Zero-cost when no logger is injected.
use Thingston\Psr18\Client; $client = new Client(logger: $monologLogger);
Log levels: debug for normal request/response, warning for retries and failures.
Raw cURL options
For anything not covered by a policy, pass raw cURL options via RequestOptions or ResponseOptions. Reserved options (those the client manages internally) are rejected at construction time.
use Thingston\Psr18\Client; use Thingston\Psr18\Curl\RequestOptions; use Thingston\Psr18\Curl\ResponseOptions; $client = new Client( requestOptions: (new RequestOptions()) ->withOption(RequestOptions::USER_AGENT, 'my-app/1.0'), responseOptions: (new ResponseOptions()) ->withOption(ResponseOptions::ENCODING, ''), );
RequestOptions constants: USER_AGENT, CONNECT_TIMEOUT, TIMEOUT, FOLLOW_LOCATION, MAX_REDIRECTS.
RequestOptions ships with defaults applied to every request: USER_AGENT = 'thingston/http-client' and CONNECT_TIMEOUT = 10 (seconds). These can be overridden via RequestOptions or superseded by the corresponding policy object.
Notes
- The request URI must be absolute and include a host.
- HTTP/1.0, HTTP/1.1, HTTP/2, and HTTP/3 are supported when the installed cURL build supports it.
- Policy options (redirect, TLS, proxy, timeout, auth, cookie jar) always override any equivalent raw options set in
RequestOptions. RetryPolicyonly retries idempotent methods by default;POSTandPATCHare excluded unless explicitly added to themethodslist.