Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,6 @@ This repository builds `jooservices/client`, a PHP 8.5+ HTTP client package unde

- Use the canonical product name **JOOservices Client**; use `jooservices/client` only as the Composer identifier.
- When public behavior changes, update README, `docs/`, AI skills, contributor guidance, `CHANGELOG.md`, and the relevant upgrade guide in the same change.
- Document only wired runtime behavior. Mark known limitations explicitly, especially the async middleware limitation and opt-in body/WAN-IP logging.
- Document only wired runtime behavior. Mark known limitations explicitly, especially opt-in body/WAN-IP logging, the blocking retry delay on the async path, and the blocking fallback for third-party sync-only middleware.
- For 3.0+, document Guzzle `^8.0` (and 7.10+ compatibility where shipped), PSR-3-only logging via `withLogger()`, and the absence of Laravel application integration and package Mongo/MySQL/Monolog sinks.
- Keep release workflows aligned with Composer requirements and required integration services.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Connection reuse metrics: `MetricsRecorderInterface::recordConnectionReused()` populated from Guzzle handler stats or libcurl connection ids, recorded by `MetricsMiddleware`.
- PHP 8.5 property hooks in `ClientConfig` (validated `timeout` / `connectTimeout`) and computed `partitionKeyResolver` properties on `CircuitBreakerConfig`, `RateLimitConfig`, and `BulkheadConfig`.
- Shared `Support\RetryAfterHeader` parser used by retry and rate-limit middleware.
- Async-safe middleware pipeline: first-party middleware implements `AsyncMiddlewareInterface` and chains Guzzle promises without `wait()`. Third-party `MiddlewareInterface` implementations still run through a blocking fallback. Retry backoff still uses `SleeperInterface` (blocking delay).
- `ClientBuilder::withHttpErrorMapping(?array $statuses = null, int $minStatus = 400)` / `HttpErrorMappingMiddleware`: map HTTP error statuses to `HttpResponseException` after inner middleware, so retry still sees raw status codes. Distinct from transport-level `withHttpErrors()`.
- JSON Schema / OpenAPI response validation: `JsonSchemaBodyValidator`, `ResponseValidationConfig::jsonSchema()`, and `ClientBuilder::withJsonSchemaValidation()`. Requires optional `justinrainbow/json-schema` (suggested; fail-closed when missing).
- Optional `jooservices/useragent` integration: `withUserAgent(string|callable(): string)`, `withGeneratedUserAgent()`, and `withRotatingUserAgent()`. Generated helpers fail closed with `InvalidConfigurationException` when the package is missing and no callable is passed. Realistic browser UAs are never the default.
- Transport failover: `FailoverTransportAdapter` + `ClientBuilder::withFailoverTransport('guzzle'|'curl'|TransportAdapterInterface)`. Fails over on `NetworkConnectionException`, `TimeoutException`, `TransportBusyException`, and `AsyncTransportNotSupportedException` only — never on HTTP 4xx/5xx. Wrapped inside the adapter so middleware is not duplicated.

### Changed
- `RateLimitStoreInterface` gained `pauseUntil(string $partitionKey, int $unixTimestampSeconds)`; `InMemoryRateLimitStore` and `Psr16RateLimitStore` honour pauses (most recent hint wins).
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ Latest stable release: `v3.0.0` (see [CHANGELOG](./CHANGELOG.md))

- **Strictly Typed**: Configuration object (`ClientConfig`) ensures type safety before requests start (validated via PHP 8.5 property hooks).
- **Layered Architecture**: Guzzle (default), native cURL, and cURL multi transports are isolated from core logic.
- **Resilience**: Built-in Retry (Backoff/Jitter + `onRetry` hook), Circuit Breaker (per-host/`partition_key` scoping + open/close hooks), Rate Limit (server `RateLimit-Reset`/`Retry-After` hints), Bulkhead, Fallback, and Deadline middleware.
- **Resilience**: Built-in Retry (Backoff/Jitter + `onRetry` hook), Circuit Breaker (per-host/`partition_key` scoping + open/close hooks), Rate Limit (server `RateLimit-Reset`/`Retry-After` hints), Bulkhead, Fallback, Deadline, HTTP error mapping, and curl↔Guzzle transport failover.
- **Async middleware**: First-party middleware chains Guzzle promises without `wait()` (`AsyncMiddlewareInterface`).
- **Observability**: Logging, W3C trace context, metrics (including connection reuse), correlation IDs, and `onError` recovery interceptors.
- **Auth**: Bearer, API key, Basic auth, and OAuth token refresh middleware.
- **Performance**: < 10μs overhead per request; true concurrency via cURL multi `batch()`.
Expand Down Expand Up @@ -74,7 +75,14 @@ items may be `RequestInterface`, `callable(): RequestInterface`, or
| `allow_redirects` | `max`, `protocols`, `track_redirects`, `strict`, `referer`, `on_redirect` |
| `proxy`, `progress`, `cookies`, `version`, `force_ip_resolve`, `on_stats` | `cookies` = jar or `name => value` map; `version` includes HTTP/3 when libcurl supports it |

Non-portable keys (`handler`, `curl`, `delay`, `on_headers`, `read_timeout`) throw. Enable strict mode with `withCurlAdapter(strictPortableOptions: true)` to also reject unknown keys. Async / `batch()` remain Guzzle-only.
Non-portable keys (`handler`, `curl`, `delay`, `on_headers`, `read_timeout`) throw. Enable strict mode with `withCurlAdapter(strictPortableOptions: true)` to also reject unknown keys. Async / `batch()` remain Guzzle-only unless you fail over with `withFailoverTransport('guzzle')`.

```php
$client = ClientBuilder::create()
->withTransport('curl')
->withFailoverTransport('guzzle')
->build();
```

## Quick Start

Expand Down
10 changes: 10 additions & 0 deletions UPGRADE-3.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ Optional WAN/public IP enrichment remains opt-in via `withWanIpProvider()`.
- Runtime `monolog/monolog` is **no longer** required by this package. Add it in your application if you use Monolog.
- `mongodb/mongodb`, `ext-mongodb`, `ext-pdo`, and `ext-pdo_mysql` are no longer suggested for Client logging. Require them only if your app still uses those sinks.

## 3.0 additions (same major)

These ship in 3.0.0 alongside the logging break:

- **Async middleware.** First-party middleware implements `AsyncMiddlewareInterface`. `getAsync()` / `requestAsync()` return pending promises; the stack no longer `wait()`s between layers. Third-party `MiddlewareInterface` classes still block via a Guzzle task fallback. Retry delays still sleep through `SleeperInterface`.
- **Status → exception mapping.** `withHttpErrorMapping()` throws `HttpResponseException` *after* retry has inspected the raw status. `withHttpErrors(true)` still throws at the transport boundary (retry then sees an exception, not a status). Prefer mapping when you want retries on 503/429.
- **JSON Schema validation.** `withJsonSchemaValidation($schema)` requires `composer require justinrainbow/json-schema`. Missing the package fails closed at builder/validator construction.
- **User-Agent helpers.** `withUserAgent()` accepts `string|callable(): string`. `withGeneratedUserAgent()` / `withRotatingUserAgent()` require `jooservices/useragent` unless you pass a callable. Default identity remains `jooservices-client/<version>` — do not use rotating browser-like UAs as the service default.
- **Failover transport.** `withTransport('curl')->withFailoverTransport('guzzle')` (or the reverse, or custom adapters). Only transport-level failures failover; HTTP error statuses do not.

## Migration checklist

1. Replace every `withMongo*` / `withMySql*` / `withDefaultLogging()` call with `withLogger($yourLogger)`.
Expand Down
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,15 @@
},
"suggest": {
"jooservices/dto": "Optional DTO helpers for ResponseWrapper::toDto()",
"jooservices/useragent": "Optional User-Agent generation for ClientBuilder::withGeneratedUserAgent() / withRotatingUserAgent()",
"justinrainbow/json-schema": "Optional JSON Schema / OpenAPI response validation via ClientBuilder::withJsonSchemaValidation()",
"ext-curl": "Required when using ClientBuilder::withCurlAdapter() or withCurlMultiAdapter()/buildCurlMulti()"
},
"require-dev": {
"captainhook/captainhook": "^5.25",
"captainhook/plugin-composer": "^5.3",
"friendsofphp/php-cs-fixer": "^3.66",
"justinrainbow/json-schema": "^6.4",
"laravel/pint": "^1.18",
"mockery/mockery": "^1.6",
"phpbench/phpbench": "^1.4",
Expand Down
150 changes: 149 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions docs/00-architecture/04-modules-and-domains.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ $response = $promise->wait();

**Status**: ✅ Confirmed

**Tests**: `tests/Feature/AsyncTest.php`, `tests/Unit/Adapters/GuzzleHttpClientAdapterAsyncTest.php`
**Notes**: First-party middleware implements `AsyncMiddlewareInterface` so `getAsync()` returns a pending promise. Third-party `MiddlewareInterface` implementations still block via a Guzzle task fallback.

**Tests**: `tests/Feature/AsyncTest.php`, `tests/Unit/Middleware/AsyncMiddlewarePipelineTest.php`, `tests/Unit/Adapters/GuzzleHttpClientAdapterAsyncTest.php`

**Confidence**: Confirmed

Expand Down Expand Up @@ -392,7 +394,12 @@ $client = ClientBuilder::create()
**Usage**:
```php
$client = ClientBuilder::create()
->withUserAgent('MyApp/1.0')
->withUserAgent('MyApp/1.0') // service identity (default intent)
->build();

// Per-request provider (QA / scraping). Do not make this the service default.
$client = ClientBuilder::create()
->withRotatingUserAgent(static fn (): string => 'Custom/' . bin2hex(random_bytes(2)))
->build();
```

Expand Down
2 changes: 1 addition & 1 deletion docs/00-architecture/business-context-and-goals.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The Composer package is `jooservices/client`; the public namespace is `JOOservic

## Deliberate limits

- Async methods return Guzzle promises; the middleware stack still has the documented synchronous `wait()` limitation.
- Async methods return Guzzle promises. First-party middleware chains those promises without `wait()`; third-party sync-only middleware still uses a blocking fallback. Retry backoff still sleeps via `SleeperInterface`.
- The native cURL transport supports portable synchronous configuration and package middleware (including when combined with `withAdapter()`), but does not implement async requests, `batch()`, or non-portable Guzzle options (`handler`, `curl`, `delay`, `on_headers`, `read_timeout`, and related keys — these fail closed).
- The package does not provide Laravel service providers, facades, or automatic application configuration.
- Retry/circuit callbacks remain post-2.0 work, not implied release behavior.
Expand Down
Loading