jooservices / client
PSR-18 HTTP client with JOOservices developer experience.
Requires
- php: >=8.5
- ext-curl: *
- jooservices/dto: ^3.0
- nyholm/psr7: ^1.8
- psr/http-client: ^1.0
- psr/http-factory: ^1.1
- psr/http-message: ^2.0
Requires (Dev)
- captainhook/captainhook: ^5.23
- friendsofphp/php-cs-fixer: ^3.65
- laravel/pint: ^1.18
- phpbench/phpbench: ^1.6
- phpmd/phpmd: ^2.15
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^13.0
- squizlabs/php_codesniffer: ^3.12 || ^4.0
Suggests
- guzzlehttp/guzzle: Enables GuzzleTransport.
- justinrainbow/json-schema: Enables JSON schema response validation.
- psr/log: Enables LoggingMiddleware.
- psr/simple-cache: Enables CacheMiddleware and persistent resilience stores.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-01 13:29:37 UTC
README
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 About v4.0.0, UPGRADE-4.0.md, and the changelog.
About v4.0.0
| Status | v4.2.0 — current release |
| First public line | v4.0.0 — earlier releases belong to the retired implementation (changelog starts here) |
| Compatibility | None with older versions. Client verbs ($client->get(), post(), …) and Guzzle-style option bags are removed |
| Core contract | PSR-18 sendRequest(RequestInterface) — HTTP 4xx/5xx responses are returned, never thrown |
| Per-request options | Portable RequestOptions DTO accepted by send($request, $options) — timeout, connect timeout, proxy, TLS verification, redirects |
| Exceptions | HTTP-status exceptions are opt-in via Response::from($response)->throw() |
| Response mapping | jooservices/dto ^3.0 integration through Response::toDto() / collect() |
Highlights vs the previous line
| Area | Previous | This rebuild (v4.0.0) |
|---|---|---|
| Core interface | Client verb methods | Strict PSR-18 HttpClient: sendRequest() + send($request, $options) |
| Request options | Guzzle option bags | Portable RequestOptions DTO validated at the boundary |
| Requests | Ad hoc construction | Fluent immutable RequestBuilder → PreparedRequest (toPsr(), options()) |
| Responses | Direct PSR-7 handling | Response wrapper: status helpers, cached JSON, download-size ceiling, opt-in throw(), DTO mapping |
| Middleware | — | 22 ranked middleware with canonical ordering, presets, and insertMiddlewareBefore() / insertMiddlewareAfter() |
| Resilience | — | Retry, circuit breaker, rate limit, bulkhead, fallback, deadline — pluggable in-memory / PSR-16 stores |
| Security | — | TLS-on-by-default, CR/LF rejection, credential stripping on redirects, private-IP redirect policy, log sanitizer |
| Testing | — | Deterministic fakes: ClientBuilder::fake(), HttpFakeRegistry, TestResponseSequence, assertSent() |
Features
Core
- PSR-18
HttpClientbuilt through the immutableClientBuilder; base URI is treated as a directory prefix (trailing slash added) and protocol-relative URIs are rejected - Fluent
RequestBuilder: verb methods, headers, query, raw body,withJson()/withMultipart()(autoContent-Type) Response::from(): status helpers, header access, BOM-stripping cached JSON, 100 MB body ceiling,throw(),toPsrResponse()escape hatch- Per-request portable options layered over builder defaults
Middleware pipeline
- Observability: logging (sanitized), metrics, correlation ID, trace context, progress
- Resilience: retry, circuit breaker, rate limit, bulkhead, fallback, deadline, request coalescing
- Auth/security: authentication, OAuth token refresh, HMAC request signing, idempotency keys, WAN-IP awareness
- DX: user agent (fixed / generated / rotating), API version, cache, response validation, interceptors (
onRequest()/onResponse()/onError()) - Canonical outermost-to-innermost ranking is applied on
build()by default.withStandardMiddlewareOrder()andwithProductionMiddlewareOrder()are explicit aliases of that same ranked list; custom middleware must be placed withinsertMiddlewareBefore()/insertMiddlewareAfter()orbuild()rejects it as unranked
Resilience state
- Validated config DTOs:
RetryConfig,CircuitBreakerConfig,RateLimitConfig,BulkheadConfig,FallbackConfig - In-memory stores by default; swap in PSR-16 adapters for persistent, multi-process resilience state
- Dedicated exceptions:
CircuitOpenException,RateLimitExceededException,BulkheadRejectedException
Transports
CurlTransport(default) — native cURL with streaming response bodiesPsrTransport— wraps any PSR-18 client;GuzzleTransportvia optionalguzzlehttp/guzzleFailoverTransport— ordered transport list with capability reporting
Security defaults
- TLS certificate verification enabled out of the box; CR/LF header injection rejected
- Cross-origin redirects strip credential headers (including common API-key/token names)
- Public-origin redirects to private/link-local IP targets rejected (explicit opt-in available)
- Response download-size guard; sensitive data redacted from logs
Validation, auth, testing
- Opt-in JSON Schema response validation (
justinrainbow/json-schema) - Bearer token, API key, basic auth;
HmacSha256Signerfor request signing - Deterministic testing: fake registry, scripted
TestResponseSequences, recorded-request assertions,InteractsWithHttpClienttrait
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.0
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', );
Design contract
sendRequest(RequestInterface)is strict PSR-18: HTTP 4xx/5xx responses are returned, not thrown. UseResponse::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()/RequestBuilderoptions win per request.
Documentation
- Changelog — starts at
v4.0.0 UPGRADE-4.0.md— migrating from pre-v4 APIsWORKFLOWS.md— CI and release workflow notes
Development
All 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
| Command | Purpose |
|---|---|
make validate |
composer validate --strict |
make lint |
Pint, PHPCS, PHPStan, PHPMD, PHP-CS-Fixer |
make test |
PHPUnit (no coverage) |
make test-coverage |
PHPUnit with PCOV Clover coverage |
make bench |
phpbench |
make ci |
lint + coverage run + coverage gate (local CI parity) |
Coverage is enforced at an 85% floor by the reusable tools/coverage-enforce.php.
Git hooks are opt-in so installing this library outside a Git checkout never fails: run composer hooks:install from a clone when you want commit-message, lint, and test hooks (Captainhook).
Branch model & CI
master— production;develop— integration- Feature/fix branches from
develop, PR back intodevelop; releases via tags frommaster; hotfixes frommaster - PRs required, all CI checks green before merge
Required CI flow (Docker-based quality gate):
Pull-request gate (ci.yml):
docker build → composer install → validate --strict
→ lint ×5 (Pint, PHPCS, PHPStan, PHPMD, PHP-CS-Fixer)
→ PHPUnit coverage (PCOV) → 85% Clover floor
Post-merge sanity (ci-post-merge.yml):
same gate on push heads
Workflows:
| Workflow | Trigger | Purpose |
|---|---|---|
ci.yml |
pull_request → master / develop |
Full quality gate: validate, lint ×5, tests with coverage, 85% floor |
ci-post-merge.yml |
push → master / develop |
Same gate on merged heads |
codeql.yml |
push/PR; weekly | CodeQL analysis |
commitlint.yml |
pull_request | Conventional Commits on every PR commit |
semantic-pr.yml |
pull_request | Conventional Commits PR title |
pr-labeler.yml |
pull_request | Path labels |
release.yml |
tag v*.*.* |
Tag reachability from master, full quality gate, GitHub Release |
scorecard.yml |
push to master; weekly |
OpenSSF Scorecard |
workflow-audit.yml |
.github/** changes; weekly |
actionlint + zizmor on workflow files |
link-check.yml |
weekly | Markdown link check |
stale.yml |
daily | Stale issue/PR housekeeping |
Quality gates: Pint · PHPCS · PHPStan · PHPMD · PHP-CS-Fixer · 85% coverage floor.
CI secrets: none required — the entire gate runs inside Docker via Composer scripts; no third-party upload steps (Codecov / Sonar) are wired.
Community
- Contributing guide — setup, git workflow, commit convention, quality gates, PR rules
- Security policy — how to report vulnerabilities privately
- Code of Conduct
- Support
- Governance
License
MIT — see LICENSE.