Search by

jooservices / client

jooservices

PSR-18 HTTP client with JOOservices developer experience.

Package info

github.com/jooservices/client

pkg:composer/jooservices/client

Statistics

Installs: 4 413

Dependents: 4

Suggesters: 0

Stars: 0

Open Issues: 0

v4.5.0 2026-10-10 23:39 UTC

README

CI Coverage (develop) Quality Gate (master) OpenSSF Scorecard PHP Version GitHub Release Packagist Version Total Downloads License: MIT

A PHP 8.5+ PSR-18 HTTP client with a strict standards core and batteries included: fluent request building, a ranked middleware pipeline, resilience (retry, circuit breaker, rate limit, bulkhead, fallback, deadline), hardened security defaults, response-to-DTO mapping via jooservices/dto, and deterministic test fakes.

Warning

v4.0.0 is a complete ground-up rebuild around PSR-7 / PSR-17 / PSR-18 and is NOT backward compatible with any previous version. Client verb methods and Guzzle option bags are gone; there are no legacy shims, no deprecation bridges, and no compatibility code. Upgrading means rewriting call sites against the new API — see UPGRADE-4.0.md and the changelog.

Upgrade highlights

  • v4 is a strict PSR-18 HttpClient — sendRequest() plus send($request, $options); verb methods are removed.
  • Guzzle option bags are replaced by a portable, validated RequestOptions DTO.
  • Fluent immutable RequestBuilder produces a PreparedRequest (PSR-7 form plus options).
  • Redirect-chain cookies carrying a Domain attribute stay scoped to the response host; sibling subdomains do not share these temporary cookies.
  • Response wrapper adds status helpers, cached JSON, download-size ceiling, opt-in throw(), and DTO mapping.
  • Deterministic test fakes replace live-network dependencies.

Features

  • PSR-18 HttpClient built through the immutable ClientBuilder; base URI treated as a directory prefix, protocol-relative URIs rejected.
  • Fluent RequestBuilder: verb methods, headers, query, raw body, withJson() / withMultipart() (auto Content-Type).
  • Response::from(): status helpers, header access, BOM-stripping cached JSON, 100 MB body ceiling, throw(), toPsrResponse() escape hatch.
  • Ranked middleware pipeline with canonical ordering and presets: observability, resilience, auth/security, and DX.
  • Resilience: retry, circuit breaker, rate limit, bulkhead, fallback, deadline — in-memory stores by default, PSR-16 adapters supported. A withDeadline() budget includes request attempts and retry backoff; each attempt timeout is clamped to the remaining budget.
  • Security defaults: TLS verification on, CR/LF injection rejected, credential stripping and private-IP policy on redirects, download-size guard, log sanitizer.
  • Transports: CurlTransport (default), PsrTransport, GuzzleTransport (optional), FailoverTransport.
  • Deterministic testing: fake registry, scripted TestResponseSequences, recorded-request assertions, InteractsWithHttpClient trait.

Requirements

  • PHP >= 8.5
  • Extension: curl
  • Core dependencies: psr/http-client, psr/http-message, psr/http-factory, nyholm/psr7, jooservices/dto ^3.0
  • Optional: guzzlehttp/guzzle (GuzzleTransport), justinrainbow/json-schema (schema validation), psr/log (logging), psr/simple-cache (cache middleware + persistent stores)
  • Docker (recommended — all local tooling runs in php:8.5-cli-bookworm)

Installation

composer require jooservices/client:^4.5

Quick start

use JOOservices\Client\Client\ClientBuilder;
use JOOservices\Client\Response\Response;
use JOOservices\Client\Resilience\RetryConfig;
use JOOservices\Client\Testing\RecordedRequest;
use JOOservices\Client\Testing\TestResponse;
use JOOservices\Client\Testing\TestResponseSequence;

// Build once — immutable configuration, canonical middleware ranking
$client = ClientBuilder::create()
    ->withBaseUri('https://api.example.test/v1')
    ->withBearerToken($token)
    ->withRetry(new RetryConfig(maxAttempts: 3))
    ->build();

// Fluent request construction → PreparedRequest (PSR-7 form + portable options)
$request = $client->requestBuilder()->post('users')->withJson($user)->build();

$upload = $client->requestBuilder()->post('media')->withMultipart([
    ['name' => 'title', 'contents' => 'Photo'],
    ['name' => 'file', 'contents' => fopen($photoPath, 'rb'), 'filename' => 'photo.jpg', 'contentType' => 'image/jpeg'],
])->build();

// PSR-18 send; per-request options override builder defaults
$psrResponse = $client->send($request->toPsr(), $request->options());

// Opt-in HTTP-status exception, then map the body to a DTO
$response = Response::from($psrResponse)->throw()->toDto(UserDto::class);

// Deterministic tests — no network
ClientBuilder::fake();
ClientBuilder::respond(
    'GET',
    'users/*',
    (new TestResponseSequence())->push(TestResponse::json(['id' => 'u_123'])),
);
ClientBuilder::assertSent(
    fn (RecordedRequest $record) => $record->request->getMethod() === 'GET',
);

Compression

Response compression is disabled by default. Enable it on the immutable builder to advertise supported encodings and receive decoded response bodies:

$client = ClientBuilder::create()
    ->withCompression()
    ->build();

Use the compression request option to override the builder default for one request. Caller-provided Accept-Encoding headers are preserved; decoded responses no longer expose Content-Encoding or encoded-body length headers.

Redirects

Redirects are followed by default (up to 5 hops). Credentials are checked on every hop, and cross-host targets are re-checked against the public/private target policy; same-host redirects skip DNS-based target-policy resolution. Pass an array to withRedirects() (or the allowRedirects request option) to tune the chain:

$client = ClientBuilder::create()
    ->withRedirects([
        'max' => 5,              // hop limit
        'allow_private' => false, // allow redirects to private/link-local hosts
        'total_timeout' => 10.0,  // one budget shared by every hop
        'track_redirects' => true, // expose X-Joo-Effective-Uri / X-Joo-Redirect-History
        'cookies' => true,        // replay cookies a server set on an earlier hop
    ])
    ->build();
  • Same-host redirects skip the DNS-based target policy, so a relative redirect never fails closed on a transient lookup or an /etc/hosts-only host.
  • GuzzleTransport fails closed before following a cross-host redirect whenever the policy verifies DNS addresses, because its generic send() contract cannot pin the connection to those addresses. Cross-host redirects that do not require DNS pinning, including literal IP targets and explicitly allowed private targets, retain their existing behavior.
  • Sensitive headers (including Cookie) are stripped only when the host changes or the scheme downgrades to http — not on a port change or an http → https upgrade.
  • The cookie jar is scoped to the redirect chain: a cookie set on one hop is replayed to a later hop only when its domain, path, and Secure flag match. It is never persisted across separate requests.
  • withDeadline($seconds) also bounds the whole redirect chain, not just each individual hop.

Design notes

  • sendRequest(RequestInterface) is strict PSR-18: HTTP 4xx/5xx responses are returned, not thrown. Use Response::from($response)->throw() when status exceptions are wanted.
  • Each layer owns one concern:
Layer Responsibility
ClientBuilder Immutable configuration; canonical middleware ranking; terminal build(): HttpClient
HttpClient::send($request, $options) Default headers, base URI resolution, portable per-request options
RequestBuilder Fluent request construction → PreparedRequest with PSR-7 form plus options
Response Optional convenience over PSR-7; always returns the raw response via toPsrResponse()
  • Builder options are defaults, never overrides: explicit send() / RequestBuilder options win per request.

Documentation

Development

All PHP tooling runs inside Docker (php:8.5-cli-bookworm via Docker Compose); Composer downloads dependencies from Packagist.

make build     # build the tooling image
make install   # composer install in the container
make shell     # interactive container shell
Command Purpose
make validate composer validate --strict
make lint Pint, PHPCS, PHPStan, PHPMD, PHP-CS-Fixer
make test PHPUnit (Arch, Unit, Integration, E2E; no coverage)
make test-coverage PHPUnit with PCOV Clover coverage
make audit Composer audit
make bench phpbench
make ci validate, audit, lint, coverage run, 85% Unit floor

Coverage is enforced at an 85% floor by tools/coverage-enforce.php. Git hooks are opt-in: run composer hooks:install from a clone when you want commit-message, lint, and test hooks (Captainhook).

Community

License

MIT — see LICENSE.