Enterprise outbound HTTP client for MonkeysLegion: fluent API, cURL transport with connection pooling, middleware pipeline, retry/backoff, PSR-18 bridge, async pools. Zero external runtime dependencies — just ext-curl and PSR interfaces.
| Feature | Description |
|---|---|
| Fluent API | $client->withToken($t)->post('/api', $data) |
| cURL Transport | Connection pooling, keep-alive, TLS session reuse, LRU eviction |
| Middleware Pipeline | Retry, Auth, Logging, RateLimit, Timeout — composable chain |
| Retry with Backoff | Constant, linear, exponential, exponential+jitter strategies |
| PSR-18 Bridge | Bidirectional: use as PSR-18 client or wrap any PSR-18 as transport |
| Async Pool | Concurrent curl_multi_* requests with configurable concurrency |
| Mock Transport | Queued responses + request history for testing |
| Event System | RequestSending, ResponseReceived, RequestFailed, RetryAttempted |
| PHPStan Level 9 | Full static analysis compliance, 0 errors |
- PHP 8.4+
- ext-curl
psr/http-client^1.0psr/http-message^2.0psr/http-factory^1.1
composer require monkeyscloud/monkeyslegion-http-clientuse MonkeysLegion\HttpClient\HttpClient;
use MonkeysLegion\HttpClient\DTO\ClientConfig;
$client = new HttpClient(new ClientConfig(
baseUrl: 'https://api.example.com',
timeout: 15,
));
// GET with query params
$response = $client->get('/users', query: ['page' => '1', 'limit' => '25']);
$users = $response->json();
// POST with JSON body
$response = $client->post('/users', json: [
'name' => 'Jorge',
'email' => 'jorge@monkeys.cloud',
]);
// Fluent builder
$response = $client
->withToken($apiToken)
->withHeader('X-Request-Id', uniqid())
->timeout(5)
->post('/orders', ['product_id' => 42, 'qty' => 1]);
// Response helpers
$response->statusCode; // 201
$response->isOk; // true
$response->json(); // ['id' => 1, 'name' => 'Jorge']
$response->get('id'); // 1
$response->header('X-RateLimit-Remaining'); // "42"
$response->duration; // 0.234 (seconds)┌──────────────────────────────────────────────────────┐
│ HttpClient (fluent) │
└──────────────────────┬───────────────────────────────┘
▼
┌──────────────────────────────────────────────────────┐
│ Middleware Pipeline │
│ Retry → RateLimit → Auth → Logging → Timeout → … │
└──────────────────────┬───────────────────────────────┘
▼
┌──────────────────────────────────────────────────────┐
│ Transport Layer │
│ ┌───────────┐ ┌──────────┐ ┌────────────────┐ │
│ │ CurlTransp│ │MockTransp│ │PsrTranspAdapter│ │
│ │ (pooled) │ │(testing) │ │(Guzzle bridge) │ │
│ └───────────┘ └──────────┘ └────────────────┘ │
└──────────────────────────────────────────────────────┘
use MonkeysLegion\HttpClient\Middleware\RetryMiddleware;
use MonkeysLegion\HttpClient\Retry\RetryPolicy;
use MonkeysLegion\HttpClient\Enum\BackoffStrategy;
use MonkeysLegion\HttpClient\Pipeline\MiddlewarePipeline;
use MonkeysLegion\HttpClient\Transport\CurlTransport;
$transport = new MiddlewarePipeline(new CurlTransport());
$transport->pipe(new RetryMiddleware(new RetryPolicy(
maxAttempts: 3,
backoff: BackoffStrategy::ExponentialJitter,
baseDelayMs: 100,
maxDelayMs: 5000,
retryableStatusCodes: [429, 500, 502, 503, 504],
)));
$client = new HttpClient(transport: $transport);use MonkeysLegion\HttpClient\Middleware\AuthMiddleware;
// Bearer token
$transport->pipe(AuthMiddleware::bearer($token));
// Basic auth
$transport->pipe(AuthMiddleware::basic('user', 'pass'));
// API key header
$transport->pipe(AuthMiddleware::apiKey('sk_live_xxx', 'X-API-Key'));
// Custom callback
$transport->pipe(AuthMiddleware::custom(fn($req) =>
$req->withHeaders(['X-Signature' => hmac($req->body)])
));use MonkeysLegion\HttpClient\Middleware\LoggingMiddleware;
$transport->pipe(new LoggingMiddleware(
logger: $psrLogger,
level: 'debug',
bodyLimit: 1024, // truncate bodies > 1KB
));
// Automatically redacts Authorization, Cookie, X-API-Key headersuse MonkeysLegion\HttpClient\Middleware\RateLimitMiddleware;
$transport->pipe(new RateLimitMiddleware(maxPerSecond: 10));
// Per-domain token bucketuse MonkeysLegion\HttpClient\Middleware\TimeoutMiddleware;
$transport->pipe(new TimeoutMiddleware(timeout: 30, connectTimeout: 5));use MonkeysLegion\HttpClient\Bridge\PsrClientAdapter;
$psrClient = new PsrClientAdapter($httpClient);
$psrResponse = $psrClient->sendRequest($psrRequest);
// Works as drop-in for any PSR-18 consumer (Stripe, AWS SDK, etc.)use MonkeysLegion\HttpClient\Bridge\PsrTransportAdapter;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Psr7\HttpFactory;
$guzzle = new GuzzleClient();
$factory = new HttpFactory();
$transport = new PsrTransportAdapter($guzzle, $factory, $factory);
$client = new HttpClient(transport: $transport);use MonkeysLegion\HttpClient\Async\Pool;
use MonkeysLegion\HttpClient\DTO\RequestContext;
use MonkeysLegion\HttpClient\Enum\HttpMethod;
$requests = [
new RequestContext(HttpMethod::GET, 'https://api.example.com/users/1'),
new RequestContext(HttpMethod::GET, 'https://api.example.com/users/2'),
new RequestContext(HttpMethod::GET, 'https://api.example.com/users/3'),
];
$responses = Pool::send($transport, $requests, concurrency: 5);
// Returns list<HttpResponse> in same order as requestsuse MonkeysLegion\HttpClient\Transport\MockTransport;
use MonkeysLegion\HttpClient\HttpClient;
$mock = new MockTransport();
$mock->enqueueJson(['id' => 1, 'name' => 'Test'], statusCode: 201);
$client = new HttpClient(transport: $mock);
$response = $client->post('/users', json: ['name' => 'Test']);
assert($response->statusCode === 201);
assert($response->get('name') === 'Test');
assert($mock->requestCount() === 1);
assert($mock->assertSent('/users'));php ml http-client:installhttp_client {
timeout = ${HTTP_CLIENT_TIMEOUT:-30}
connect_timeout = ${HTTP_CLIENT_CONNECT_TIMEOUT:-5}
verify_ssl = ${HTTP_CLIENT_VERIFY_SSL:-true}
max_redirects = ${HTTP_CLIENT_MAX_REDIRECTS:-5}
user_agent = ${HTTP_CLIENT_USER_AGENT:-MonkeysLegion/2.0}
pool_size = ${HTTP_CLIENT_POOL_SIZE:-10}
throw_on_error = ${HTTP_CLIENT_THROW_ON_ERROR:-false}
retry {
max_attempts = ${HTTP_CLIENT_RETRY_MAX:-3}
backoff = ${HTTP_CLIENT_RETRY_BACKOFF:-exponential_jitter}
base_delay = ${HTTP_CLIENT_RETRY_DELAY:-100}
max_delay = ${HTTP_CLIENT_RETRY_MAX_DELAY:-5000}
}
}
| Event | When |
|---|---|
RequestSending |
Before request is sent |
ResponseReceived |
After response received |
RequestFailed |
On connection/timeout failure |
RetryAttempted |
When retry is triggered |
$response->statusCode; // int
$response->body; // string (raw)
$response->headers; // array<string, string>
$response->duration; // float (seconds)
$response->isOk; // 2xx
$response->isRedirect; // 3xx
$response->isClientError; // 4xx
$response->isServerError; // 5xx
$response->isFailed; // 4xx or 5xx
$response->json(); // array<string, mixed>
$response->get('key'); // dot-notation access
$response->header('name'); // case-insensitive header lookup
$response->toArray(); // full response as arrayMIT © MonkeysCloud