jord-jd/psr-18-guzzle-adapter
A simple PSR-18 adapter for Guzzle, supporting PHP 7.1+ with Guzzle 6 or 7. It keeps Guzzle 6 projects compatible with PSR-18 clients and normalizes Guzzle exceptions to the PSR-18 exception interfaces.
What the artifact actually is. jord-jd/psr-18-guzzle-adapter is a single-purpose shim: it wraps GuzzleHttp\Client so callers can depend on Psr\Http\Client\ClientInterface instead of Guzzle concretions. The README is two sentences; the release notes are empty (v3.0.0 contains only a changelog link, v2.0.0 is a rename, v1.2.0 adds PHP 8, v1.1.0 adds Guzzle 7). A TPM should evaluate this as procurement of ~100 lines of glue code, not as a platform dependency. The entire decision is therefore about risk containment and exit cost, not about capability.
Architecture fit. PSR-18 is the right seam for a Laravel codebase for three concrete reasons: (1) third-party and future packages increasingly type-hint ClientInterface, so you need a container binding that satisfies them; (2) it lets domain/services code stop importing GuzzleHttp\Client, which is the real source of test friction; (3) it preserves Guzzle's middleware investment where you have it (retries, logging, auth handlers), which is not true of a hard migration to Symfony HttpClient or Buzz.
The fit is weak if the team already standardizes on Laravel's Illuminate\Support\Facades\Http. Laravel's HTTP client is Guzzle-backed but is not itself PSR-18; layering this adapter alongside it creates two HTTP surfaces with divergent retry, timeout, logging, and metrics behaviour. That is an operational regression, not a standardisation win. Adopt this only where a PSR-18 seam is actually required by a consumer, and pick one primary HTTP surface.
Integration feasibility. Mechanically trivial: composer require, then bind the adapter to ClientInterface in a service provider, then inject the interface. The constraint is not install effort but the surrounding wiring: PSR-18 gives you only sendRequest(), so you separately need a PSR-17 factory (guzzlehttp/psr7 or nyholm/psr7) to construct requests, and you need to decide where middleware and configuration live. Note also that the supplied engineering notes reference a Psr18GuzzleAdapterServiceProvider and a getAdapter() method; neither is evidenced in the README. Doc surfaces that disagree are themselves a risk signal — assume manual wiring, no auto-discovery, and verify every class name against the source before committing to a plan.
Technical risk. Ranked:
| Risk | Assessment |
|---|---|
| PSR-18 conformance gaps | High impact. PSR-18 requires that non-2xx responses be returned, not thrown, and that transport errors be raised as ClientExceptionInterface / NetworkExceptionInterface / RequestExceptionInterface. Guzzle defaults to http_errors: true and throws its own hierarchy that does not satisfy PSR-18. If the adapter does not force http_errors => false and map exceptions, consumers receive errors they are not contracted to catch. Must be verified by test, not by reading the README. |
| Supply chain / abandonment | High likelihood, low blast radius. 9 stars, 0 dependents, single maintainer, no SECURITY.md, no CI or PHP-matrix badges, and one major version bump with zero migration notes — meaning an upgrade can break you with no written signal. |
| License | Requires legal sign-off. LGPL-3.0 is highly atypical in the PHP ecosystem (which is near-universally MIT/BSD). It is almost certainly fine for unmodified Composer use, but it complicates forking and redistribution. |
| Redundancy / build-vs-buy | Real. php-http/guzzle7-adapter (MIT, php-http org, established) solves the same problem, and an in-house adapter is roughly a day of work. There is no evident differentiator justifying a one-maintainer dependency. |
| Date/data quality | The last-release date (2026-07-18) is forward-dated relative to a v1.0.0→v3.0.0 history. Treat metadata as unverified. |
Key questions for the spike owner.
NetworkExceptionInterface; malformed/refused requests as RequestExceptionInterface; request body is rewound on retry/redirect?TransferStats, async/promises, or per-request Guzzle options accessible at all — and if not, what is the sanctioned escape hatch?Stack fit. Assume Laravel 8+ with Guzzle 7 (and Guzzle 8 unverified). The adapter sits below application code and above Guzzle; it must never be referenced directly by domain code. The correct shape is an internal boundary — bind Psr\Http\Client\ClientInterface once, and treat the concrete adapter class as an implementation detail swappable in one file:
// AppServiceProvider::register()
$this->app->singleton(\Psr\Http\Client\ClientInterface::class, fn () => new GuzzleAdapter(
new \GuzzleHttp\Client([ /* base_uri, timeout, http_errors => false, handlers */ ])
));
$this->app->singleton(\Psr\Http\Message\RequestFactoryInterface::class, fn () => new \GuzzleHttp\Psr7\HttpFactory());
If the internal notes about a package service provider turn out to be accurate, prefer explicit wiring anyway — auto-discovery that isn't documented is a liability, not a convenience.
Migration path. Do not migrate call sites first. Sequence:
php-http/guzzle7-adapter and an in-house ~100-line adapter as comparators, scored on licence, maintenance, and conformance. Default recommendation: in-house or php-http, with this package as the reference implementation for behaviour.AppServiceProvider plus an architecture test (e.g. PHPStan/Deptrac rule) banning GuzzleHttp\Client imports outside the provider.Compatibility notes to verify, not assume: PHP version floor and ceiling; Guzzle 7 vs 8; PSR-7 implementation used for request construction (must match what the adapter returns, or response body typing surprises appear); Laravel's container resolution of ClientInterface in queued jobs and console commands; and test tooling — mock ClientInterface, never the adapter.
Sequencing gates. Gate 1: licence cleared. Gate 2: conformance checklist green. Gate 3: single HTTP surface agreed in writing. Gate 4: observability parity demonstrated (same log/metric output as current Guzzle path). Only then roll past a single non-critical service.
Maintenance. Realistic cost is low but non-zero: one Composer constraint to own, one adapter to monitor, one upstream to watch. Because dependents = 0, there is no community signal that a breakage will be caught before we hit it. Budget roughly 1–2 engineer-days to vendor or reimplement this internally, which buys permanent control over licence, exception semantics, and release timing. Given the empty release notes and an undocumented v3 breaking change, pinning to an exact version is mandatory and "upgrade to latest" must never be an unreviewed action.
Support. Two artifacts are needed before rollout: (a) a runbook documenting the exception taxonomy and what each maps to at the application layer, since PSR-18 deliberately hides transport detail and stack traces will still show Guzzle internals; (b) a one-page cheat sheet mapping Guzzle idioms (request('GET', ...), client-level defaults, TransferStats) to their PSR-18 equivalents and stating explicitly what is no longer available. Without these, every debugging session becomes a rediscovery of the abstraction boundary.
Scaling. Per-request overhead is negligible — a thin pass-through call. The scaling concerns are architectural, not computational: bind the client as a singleton so connection pooling is preserved across requests in a worker; confirm that queued jobs and Horizon workers get the same configured instance rather than a fresh default client; and ensure middleware (retry, backoff, rate limiting) is applied centrally, because a PSR-18 seam makes it easy for teams to hand-roll retries at call sites and multiply load during an upstream incident. No statefulness, so horizontal scaling and multi-region deployment are unaffected.
Failure modes.
| Failure | How it shows up | Mitigation |
|---|---|---|
| Non-2xx thrown instead of returned | Uncaught Guzzle exception in a consumer expecting a response; error-handling branches silently dead | Explicit conformance test; force http_errors => false in the client config |
| Exception type leakage | catch (ClientExceptionInterface) never matches; outages look like unhandled 500s |
Adapter-level mapping + test per PSR-18 exception interface |
| Middleware drift | Retries/logging present on the Guzzle path but absent on the PSR-18 path — or vice versa | Centralise middleware; integration test asserting a retry actually happens |
| Duplicate retries | Both Guzzle middleware and application retry logic fire; upstream sees 4–9× traffic | Single retry owner, documented |
| Abandonment | No updates; a Guzzle/PHP major forces an unplanned fork | Pin version; keep the seam to one file; pre-approve fork/in-house fallback |
| Licence surprise at audit | Distribution or fork restricted late | Resolve before adoption, not after |
Ramp-up. An experienced Laravel engineer is productive in half a day with the cheat sheet and a working example; the genuine ramp is conceptual, not syntactic — understanding that PSR-18 is a subset and that anything Guzzle-specific now requires a deliberate, reviewed exception to the boundary. Reinforce with an architecture test rather than documentation alone.
TPM recommendation. Do not adopt this package as a direct production dependency on the strength of its own merits. Use it as a reference implementation and one-day spike, then ship an internal MIT-licensed adapter (or php-http/guzzle7-adapter) behind a single ClientInterface binding. The value on offer — a PSR-18 seam over existing Guzzle investment — is real and worth capturing; the specific artifact is small, single-maintainer, atypically licensed, undocumented at the release level, and has zero production dependents. Exit criteria for revisiting: the package gains CI badges with a published PHP/Guzzle matrix, a SECURITY.md, and meaningful release notes — or the upstream maintainer is prepared to transfer/fork under a permissive licence.
How can I help you explore Laravel packages today?