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-30 03:31:05 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 UPGRADE-4.0.md and the changelog.
Upgrade highlights
- v4 is a strict PSR-18
HttpClient—sendRequest()plussend($request, $options); verb methods are removed. - Guzzle option bags are replaced by a portable, validated
RequestOptionsDTO. - Fluent immutable
RequestBuilderproduces aPreparedRequest(PSR-7 form plus options). Responsewrapper adds status helpers, cached JSON, download-size ceiling, opt-inthrow(), and DTO mapping.- Deterministic test fakes replace live-network dependencies.
Features
- PSR-18
HttpClientbuilt through the immutableClientBuilder; base URI treated as a directory prefix, protocol-relative URIs 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.- 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.
- 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,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 notes
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 — version history and upgrade notes
UPGRADE-4.0.md— migrating from pre-v4 APIs- Development workflows — branches, CI, releases, and repository automation
- Security policy — private vulnerability reporting
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 (Unit + Integration, no coverage) |
make test-coverage |
PHPUnit with PCOV Clover coverage |
make audit |
Composer audit |
make bench |
phpbench |
make ci |
lint + coverage run + 85% coverage gate (local CI parity) |
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
- 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.