Skip to content

jardisAdapter/http

Repository files navigation

Jardis HTTP Client

Build Status License: PolyForm Shield PHP Version PHPStan Level PSR-12 PSR-18

Part of the Jardis Business Platform — Enterprise-grade PHP components for Domain-Driven Design

HTTP requests without overhead. A lean PSR-18 client built on cURL — designed for DDD applications that call external APIs, send webhooks, or integrate services. No framework, no middleware stack, no dependency bloat. Just what you need.


Why This Client?

  • Two classes to learnHttpClient + ClientConfig. Includes its own PSR-7/PSR-17 implementation — zero external dependencies
  • Handler pipeline — each concern is its own invokable, orchestrated internally by the client
  • Retry with backoff — automatic retry on 5xx and network errors
  • PSR-18 compatible — works with any PSR-18-capable code
  • 96% test coverage — integration tests against real HTTP requests, not mocks

Installation

composer require jardisadapter/http

Quick Start

GET Request

use JardisAdapter\Http\HttpClient;
use JardisAdapter\Http\Config\ClientConfig;

use JardisAdapter\Http\Message\Psr17Factory;

$psr17 = new Psr17Factory();
$client = new HttpClient($psr17, $psr17, $psr17, $psr17, new ClientConfig(
    baseUrl: 'https://api.example.com/v2',
));

$response = $client->get('/users');
$data = json_decode((string) $response->getBody(), true);

POST with JSON Body

$response = $client->post('/users', [
    'name' => 'John Doe',
    'email' => 'john@example.com',
]);

PUT, PATCH, DELETE

$client->put('/users/1', ['name' => 'Jane Doe']);
$client->patch('/users/1', ['status' => 'active']);
$client->delete('/users/1');

Custom Headers per Request

$response = $client->get('/reports', ['Accept' => 'text/csv']);
$response = $client->post('/import', $data, ['X-Request-Id' => 'abc-123']);

Fully Configured

$psr17 = new Psr17Factory();
$client = new HttpClient($psr17, $psr17, $psr17, $psr17, new ClientConfig(
    baseUrl: 'https://api.example.com/v2',
    timeout: 10,
    connectTimeout: 5,
    verifySsl: true,
    defaultHeaders: ['Accept' => 'application/json'],
    bearerToken: 'eyJhbGciOiJI...',
    maxRetries: 3,
    retryDelayMs: 200,
));

$response = $client->get('/users');
$response = $client->post('/orders', ['product' => 'Widget', 'quantity' => 3]);

Authentication

Bearer Token

$psr17 = new Psr17Factory();
$client = new HttpClient($psr17, $psr17, $psr17, $psr17, new ClientConfig(
    bearerToken: 'eyJhbGciOiJI...',
));
// Authorization: Bearer eyJhbGciOiJI... is set automatically

Basic Auth

$psr17 = new Psr17Factory();
$client = new HttpClient($psr17, $psr17, $psr17, $psr17, new ClientConfig(
    basicUser: 'api-user',
    basicPassword: 'secret',
));

Retry

$psr17 = new Psr17Factory();
$client = new HttpClient($psr17, $psr17, $psr17, $psr17, new ClientConfig(
    maxRetries: 3,          // Up to 3 retries on 5xx
    retryDelayMs: 200,      // Exponential backoff: 200ms, 400ms, 800ms
));

Automatically retries on HTTP 5xx and transport errors (HttpClientException, which covers both NetworkException and RequestException). No retry on 4xx — those are caller errors.


Error Handling

The client does not throw exceptions on HTTP 4xx/5xx — those are valid responses. Exceptions are only thrown for actual errors:

Exception When
NetworkException DNS failure, connection refused, timeout
RequestException Invalid request (malformed URI)
use JardisAdapter\Http\Exception\NetworkException;

try {
    $response = $client->get('/users');
} catch (NetworkException $e) {
    // Network problem — retry was already active (if configured)
}

if ($response->getStatusCode() >= 400) {
    // Handle HTTP errors yourself
}

PSR-18 Compatible

The client implements Psr\Http\Client\ClientInterface. For full control over the request, use sendRequest():

use JardisAdapter\Http\Message\Psr17Factory;

$factory = new Psr17Factory();
$request = $factory->createRequest('OPTIONS', 'https://api.example.com');
$response = $client->sendRequest($request);

Architecture

The user only sees HttpClient + ClientConfig. Internally, the client orchestrates a pipeline of invokable handlers — built from the config:

HttpClient (Orchestrator)
  │
  │  Convenience methods: get(), post(), put(), patch(), delete(), head()
  │  └── internally create PSR-7 requests
  │
  │  Transformers (Request → Request, built from config):
  │  ├── BaseUrl           resolve relative URLs
  │  ├── DefaultHeaders    set default headers
  │  ├── BearerAuth        add bearer token
  │  └── BasicAuth         add basic auth
  │
  │  Transport (Request → Response, built from config):
  │  ├── CurlTransport     cURL-based transport
  │  └── Retry             wraps transport with exponential backoff
  │
  ▼
  sendRequest():
    foreach transformer → $request = $transform($request)
    return $transport($request, $config)

Each handler is an invokable object (__invoke) — independently testable, replaceable, composable. Only what is configured gets instantiated.

Custom Transport

The transport is a closure — replaceable without changing the client:

$psr17 = new Psr17Factory();
$client = new HttpClient(
    requestFactory: $psr17,
    streamFactory: $psr17,
    responseFactory: $psr17,
    uriFactory: $psr17,
    config: new ClientConfig(),
    transport: function (RequestInterface $request, ClientConfig $config) use ($psr17) {
        return $psr17->createResponse(200)
            ->withBody($psr17->createStream('{"mocked": true}'));
    },
);

Jardis Foundation Integration

In a Jardis DDD project, the client is automatically configured via ENV:

HTTP_BASE_URL=https://api.example.com
HTTP_TIMEOUT=30
HTTP_CONNECT_TIMEOUT=10
HTTP_VERIFY_SSL=true
HTTP_BEARER_TOKEN=eyJhbGciOiJI...
HTTP_MAX_RETRIES=3
HTTP_RETRY_DELAY_MS=200

The HttpClientHandler in JardisApp builds the client and registers it in the ServiceRegistry. Your domain code receives ClientInterface via injection — without ever importing HttpClient directly.


Development

cp .env.example .env    # One-time setup
make install             # Install dependencies
make phpunit             # Run tests
make phpstan             # Static analysis (Level 8)
make phpcs               # Coding standards (PSR-12)

License

PolyForm Shield License 1.0.0 — free for all use including commercial. Only restriction: don't build a competing framework.

About

PSR-18 HTTP client with cURL transport, handler pipeline, retry — part of the Jardis Business Platform

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors