SDK Toolbox docs
HTTP semantics
PHP ^8.316 pages
Docs for v0.0.1, from GitHub.
Pages in these docs
HTTP caching
Give the connector a PSR-16 cache and it caches responses the way RFC 9111 says a private cache should:
parent::__construct(baseUri: 'https://api.acme.test', cache: $psr16Cache);- Fresh responses are returned without a request, with an
Ageheader. - Stale responses, and those marked
no-cache, are revalidated: the request goes out withIf-None-MatchandIf-Modified-Since, and a 304 refreshes the stored headers and returns the stored body. - What’s stored: 200 and 203 responses to GET that can be reused, either because they have
max-ageorExpires, or because they have a validator (ETag,Last-Modified). - Never stored:
no-store, from either side;Vary: *;- 429s (RFC 6585);
- bodies over 1MB.
Varyis honoured: a stored response is only used for a request that matches it on every header it names.- Writes invalidate. A successful POST, PUT, PATCH or DELETE removes the stored response for its URI.
- Users stay apart. Entries are keyed on every credential the request carries, so one user never gets another’s response, even through a shared Redis.
There’s no heuristic freshness: a response without max-age or Expires is always revalidated before it’s reused. s-maxage and public are for shared caches and are ignored. Entries are stored as plain arrays, never serialized objects.
Conditional requests
The lost-update problem: two people read a resource, both change it, and the second save silently overwrites the first. Read the ETag and send the change with If-Match:
$etag = $acme->response(new GetProject($id))->etag();
$acme->send(new RenameProject($id, 'Production', $etag));final readonly class RenameProject implements Conditional, HasBody, Request{ public function __construct(private string $id, private string $name, private ?EntityTag $etag) {}
public function precondition(): ?Precondition { return $this->etag === null ? null : Precondition::ifMatch($this->etag); }
// method(), path(), body(), result()...}If somebody changed it in between, the API answers 412 and send() throws PreconditionFailedException. A 428 means the API insists on a precondition, and throws PreconditionRequiredException.
| Precondition | Means |
|---|---|
Precondition::ifMatch($etag, ...) | only if it’s still this version |
Precondition::ifExists() | only if it exists (If-Match: *) |
Precondition::ifNoneMatchAny() | only if it doesn’t exist yet: create, never overwrite |
Precondition::ifUnmodifiedSince($date) | only if unchanged since |
If-Match uses strong comparison (RFC 9110 §13.1.1), so a weak tag (W/"...") can never match. It’s refused up front rather than failing every time.
Deprecation and Sunset
$response->deprecation() reads what an API says about the end of an endpoint:
Deprecation(RFC 9745’s@1688169599, or the undated?1older drafts used);Sunset(RFC 8594);- the
deprecationandsunsetlink relations.
To hear about it as it happens:
deprecations: new Deprecations(fn (DeprecationNotice $notice) => $logger->warning($notice->describe())),deprecations: new Deprecations, // only logged and dispatched (see Observability)deprecations: Deprecations::strict(), // throws: for an SDK's own test suiteThe handler runs once per endpoint per process, so a loop over a deprecated endpoint doesn’t flood a log. Both headers are hints: nothing about the request changes.
Deprecations::strict() is the useful one for SDK authors. Run your tests against recorded fixtures with it, and an endpoint the API has deprecated fails your build, not a user’s.
Preferences and 202 polling
A request that implements HasPreferences sends Prefer (RFC 7240):
public function preferences(): Preferences{ return Preferences::none()->returnMinimal()->respondAsync()->wait(10);}$response->preferenceApplied() says which preferences the server honoured.
For operations that take a while, await() handles the pattern Microsoft’s and Azure’s guidelines use, and api-toolbox emits:
$export = $acme->await(new StartExport($filters), timeout: 300.0, interval: 2.0);- A
202 Acceptedwith aLocationis an operation in progress, andLocationis its status monitor. - The monitor is polled with GET, waiting for
Retry-After(orinterval) between polls, until it stops answering 202. - A
303 See Otherfrom the monitor is followed to the result; a 200 is the result itself. - The original request’s
result()reads it.
A request the API answers straight away comes back as normal. Every monitor and result URI has to be under the base URI. At the timeout, OperationTimeoutException carries the monitor URI, so you can look again later: the operation may still finish.