guzzlehttp/guzzle-services
Build SDK-style API clients with service descriptions: map named operations and parameters to HTTP requests and transform responses into result data, instead of hand-writing request serializers for every endpoint.
Architecture fit
guzzlehttp/guzzle (transport) → guzzlehttp/command (command/handler pipeline) → guzzlehttp/guzzle-services (service description → request serialization → response model hydration). That maps cleanly onto the "SDK-style client for one external API" problem, which is different from the "make one HTTP call" problem Laravel's Http facade solves.Http::withToken()->get()), or when the team expects spec-first generation. There is no OpenAPI/Swagger importer in this package; descriptions are hand-authored arrays or produced by external codegen. That is the single biggest functional gap versus alternatives like Saloon or generated SDKs, and it moves the "keep in sync with the vendor's API" burden onto us.Integration feasibility
guzzlehttp/guzzle ^8, psr7 ^3, promises ^3, command ^2, and PHP >=7.4,<8.7. Laravel's Illuminate\Http\Client is built on Guzzle 7, so any app using the HTTP client (i.e., almost every app) cannot take 2.0 without a framework-wide Guzzle 7 → 8 uplift. Plan on the ^1.7 line today; it supports PHP 7.2.5–8.6 and Guzzle ^7.15.baseUrl → baseUri, responseClass → responseModel, lowercase httpMethod, loose parameter schemas), so the 2.0 migration is pre-announced rather than a surprise — good governance, but it means the ^1.7 line is by definition transitional.Technical risk
| Risk | Severity | Notes |
|---|---|---|
| Guzzle major mismatch with Laravel | High | Pins us to ^1.7; blocks 2.0 adoption until Laravel moves to Guzzle 8 |
| Hand-maintained descriptions drift from vendor API | High | No OpenAPI import; drift surfaces as runtime validation/parse errors, not build failures |
Magic __call client surface |
Medium | $client->getUser([...]) is untyped; PHPStan/IDE blind without @method annotations on a wrapper class |
| Response models are permissive | Medium | additionalProperties passes unknown JSON through; no compile-time contract enforcement |
| Description-array authoring learning curve | Medium | location, sentAs, parameter models, filters — non-obvious, and errors are caught at runtime |
| Transitive dependency surface | Low | Three Guzzle-org packages on the critical path; all actively maintained |
PHP <8.7 ceiling on 2.0 |
Low | A future constraint, not today's problem |
Security posture is a positive: a published policy, Tidelift-mediated disclosure, a shipped fix for XML CDATA injection (GHSA-q8r6-5hfw-5jff), and 2.0 adds API-key redaction in PHP 8.2+ backtraces. For a package that will carry credentials into third-party APIs, that is the signal I want to see.
Key questions to resolve before commitment
^3, pools) for bulk fetch, or is sequential fine? This influences whether the 2.0 line is worth lobbying for.Stack fit
guzzlehttp/guzzle ^7.15. Composer: guzzlehttp/guzzle-services ^1.7. Do not allow ^2.0 in the constraint — a transitive resolution attempt would fail anyway, but pin explicitly and document why.GuzzleHttp\Client instance with its own HandlerStack. Keep those handlers separate so our retry/logging middleware does not leak onto outbound webhooks and mail-provider traffic.packages/acme/vendor-sdk or app/Services/{Vendor}) rather than loose code: one Description factory per external API, one ServiceProvider binding each client as a named singleton, config file for baseUri/credentials/timeouts.Migration path
Http:: calls: extract the operation list first (endpoint, method, params, response shape) as a plain table, then transcribe to description arrays. Do not attempt a big-bang rewrite — new operations land on the service client, existing calls migrate opportunistically.@method annotations and typed wrapper returns. This is the single highest-leverage piece of glue: it restores IDE autocompletion and PHPStan coverage over the magic __call surface, and gives us one place to hang deprecation shims when we move to 2.0.GuzzleHttp\Command\Exception\CommandException (validation), GuzzleHttp\Exception\RequestException/ConnectException (transport/HTTP), and JSON/XML parse failures into a small set of Laravel-native exceptions. Do not let Guzzle exceptions escape into controllers.Compatibility
^7.15) against whatever the platform pins. Laravel's own ^7.8-style constraint satisfies it, but an app with a hard pin at 7.4 or 7.10 will produce a resolution conflict and force a Guzzle bump — a small but real change-management item.psr7 ^2.13 is required by 1.7.3+; older Laravel apps still on psr7 ^1 need an uplift.string instead of a stream, scalar top-level JSON rejected, 512-element XML depth cap, strict enum matching). Any code written against 1.x assumptions should be flagged in the wrapper layer, not scattered across call sites.Sequencing
MockHandler in tests. Output: go/no-go on the description-array authoring cost.@method annotations, exception mapping, and a HandlerStack carrying auth, correlation-ID logging, retry-with-backoff, and timeout policy.php artisan vendor:publish, optional facade, health-check command.UPGRADING.md.Maintenance
baseUrl, responseClass, method casing, loose schemas). Any new code should be written to the 2.0-compatible form now — that halves the eventual migration.Support
Scaling
baseUri are per-tenant resolved through a factory rather than reconfigured on a shared instance.guzzlehttp/promises) rather than sequential calls.Description per service and avoid rebuilding it per request; consider caching the parsed structure if profiles show it.HandlerStack — set them once, centrally, or they will be inconsistent across operations.Failure modes
| Mode | Symptom | Mitigation |
|---|---|---|
| Description drift | Validation errors or silently missing fields after a vendor change | Weekly contract test against sandbox; schema-diff alerting |
| Parameter validation failure | CommandException before any HTTP call |
Typed wrapper + unit tests per operation; surface as 4xx-class app error, never a 500 |
| Transport failure | ConnectException / timeout |
Handler-stack retry with backoff on idempotent methods only; circuit breaker on repeated failures |
| Malformed/unexpected payload | JSON JsonException (2.0 uses native) or XML parse failure; 2.0 additionally rejects scalar top-level JSON and caps XML depth at 512 |
Fail closed, log the raw body with secrets redacted, alert on rate |
| Credential leakage | Keys in logs or stack traces | Rely on 2.0's #[\SensitiveParameter] redaction only where available; in 1.7 scrub at our logging middleware |
| Silent schema permissiveness | Unknown JSON fields pass through unnoticed | Contract tests asserting required fields; do not rely on additionalProperties for safety |
Ramp-up
location, sentAs, parameter models, filters) is the steep part and is learned by example, not by reading.__call client is the main discoverability tax. A hand-written subclass with @method annotations and typed returns is not optional polish — it is what makes the package tractable for a team that did not write the wiring, and it keeps PHPStan green in CI.How can I help you explore Laravel packages today?