Skip to content

refactor: drop guzzle dependency for PSR-18 alignment#66

Open
kyeh-amp wants to merge 12 commits into
mainfrom
drop-guzzle-hard-dependency
Open

refactor: drop guzzle dependency for PSR-18 alignment#66
kyeh-amp wants to merge 12 commits into
mainfrom
drop-guzzle-hard-dependency

Conversation

@kyeh-amp
Copy link
Copy Markdown
Contributor

@kyeh-amp kyeh-amp commented Apr 27, 2026

Summary

  • Replace the hard guzzlehttp/guzzle requirement with PSR-18 / PSR-17 + php-http/discovery. Consumers bring their own PSR-18 client (Guzzle, Symfony HttpClient, or any compliant implementation).
  • Remove the SDK-owned HTTP transport surface (HttpClientInterface, GuzzleHttpClient, guzzleClientConfig); builders now accept Psr\Http\Client\ClientInterface and Psr\Http\Message\RequestFactoryInterface directly.
  • New RetryingClient decorator with RetryConfig. Auto-wraps the discovered client only — user-supplied clients are used verbatim to avoid layered-retry amplification. Default retry scope is GET only; opt POST in via RetryConfig::retryMethods to preserve the v1 retry knob without amplifying duplicate-event risk by default.

Behavior changes (v2)

  • A PSR-18 implementation must be installed; otherwise composer install fails on the unsatisfied virtual psr/http-client-implementation constraint at install time, not at runtime.
  • guzzleClientConfig is removed. Retry is configured via RetryConfig only.
  • HttpClientInterface and GuzzleHttpClient are removed. Use Psr\Http\Client\ClientInterface directly.

Test plan

  • Full test suite green on Guzzle (default require-dev)
  • Full test suite green on Symfony HttpClient + nyholm/psr7 (verified locally; new tests-symfony CI job exercises this)
  • PHPStan clean
  • composer validate --strict clean
  • CI green on push

Follow-up

  • UPGRADING.md with worked migration examples (Guzzle / Symfony / raw PSR-18) — held until review feedback identifies which migrations need the most explanation.

Closes #30.


Note

Medium Risk
Moderate risk because it rewires all outbound HTTP (events, remote fetch, flag config fetch) to PSR-18 discovery and a new retry layer, and changes public configuration surface away from guzzleClientConfig/custom interface.

Overview
Drops the SDK-owned Guzzle transport (HttpClientInterface/GuzzleHttpClient/guzzleClientConfig) and switches core clients (Amplitude, RemoteEvaluationClient, LocalEvaluationClient/FlagConfigFetcher) to PSR-18 ClientInterface plus PSR-17 factories resolved via new HttpClientFactory (php-http/discovery), throwing MissingHttpImplementationException when discovery fails.

Introduces RetryingClient + RetryConfig to preserve transport-error retries for auto-discovered clients while avoiding double-retry for user-supplied clients; default retry is GET-only with explicit opt-in for POST.

Updates composer.json to require PSR virtual packages and php-http/discovery (Guzzle moved to require-dev), adds a GitHub Actions smoke job that runs tests with Symfony HttpClient, and rewrites tests/utilities to use PSR-18 mocks and the new retry behavior.

Reviewed by Cursor Bugbot for commit 9ad2cec. Bugbot is set up for automated code reviews on this repo. Configure here.

Backoff::doWithBackoff was never invoked in src/ or tests/. The class
was a vestigial promise-chained retry helper with no call sites.
Removing it also drops guzzlehttp/promises from the transitive
dependency tree.

Part of #30.
Switch from a hard guzzlehttp/guzzle requirement to PSR-18 / PSR-17
interfaces plus php-http/discovery for runtime client lookup. Guzzle
moves to require-dev (still used by the test suite) and is listed
under suggest alongside symfony/http-client.

The new virtual psr/http-client-implementation constraint causes a
composer install without any PSR-18 implementation to fail at install
time rather than at runtime. SDK code still references the Guzzle
HTTP client classes and will be migrated to PSR-18 + discovery in
subsequent commits.

Part of #30.
Introduce a synchronous PSR-18 retry decorator with method-gated
retry behavior. RetryConfig defaults to 9 total attempts with
exponential backoff (500ms -> 10s, scalar 1.5) applied to GET only,
preserving the v1 retry budget while avoiding amplification on
non-idempotent POSTs. Callers needing POST retries opt in via
RetryConfig::retryMethods.

Retries trigger only on PSR-18 ClientExceptionInterface; HTTP error
responses (4xx/5xx) and non-PSR throwables propagate without retry,
matching v1 GuzzleHttpClient middleware semantics.

Not yet wired up; callers continue to use GuzzleHttpClient until the
default-client swap commit.

Part of #30.
Replace the SDK-owned HttpClientInterface abstraction with direct
PSR-18 ClientInterface + PSR-17 RequestFactoryInterface usage at the
three call sites: Amplitude::post, FlagConfigFetcher::fetch, and
RemoteEvaluationClient::fetchWithOptions. Configs now expose three
optional fields:

  httpClient: ?ClientInterface
  requestFactory: ?RequestFactoryInterface
  retryConfig: ?RetryConfig

A new HttpClientFactory helper centralizes the resolution rules:
when httpClient is null, a PSR-18 implementation is auto-discovered
via php-http/discovery and wrapped in RetryingClient using the
supplied RetryConfig (or sensible defaults). When httpClient is
supplied, the SDK uses it verbatim — no auto-wrap — so users whose
own client already implements retry are not double-wrapped.

guzzleClientConfig is removed entirely; retry behavior is now
configured via RetryConfig only. GuzzleHttpClient and
HttpClientInterface remain in the codebase but are no longer
referenced; they are removed in a follow-up commit.

Tests migrate from MockGuzzleHttpClient (Guzzle MockHandler-backed)
to a simple MockPsr18Client harness in tests/Util/. The Amplitude
backoff tests are rewritten to opt POST into RetryConfig::retryMethods
since the new default scopes retry to GET only; an additional test
asserts that POST is NOT retried under the default RetryConfig.

Part of #30.
The Guzzle-era SDK HTTP scaffolding has no remaining call sites after
the PSR-18 migration. Delete the three classes and the autoload entry
for GuzzleConstants. Also drop a stale GuzzleHttp\Psr7\Request import
from AmplitudeTest that no longer references the type.

guzzlehttp/guzzle stays in require-dev: tests use Guzzle's PSR-7
implementation (Request/Response) and ConnectException as a
ClientExceptionInterface fixture, and one integration test uses
GuzzleHttp\Client directly to fetch flag configs from the live API.

Part of #30.
Replace direct GuzzleHttp\Psr7\Request, Response, and ConnectException
usage with Psr7TestUtil helpers backed by php-http/discovery, plus a
portable test-internal ClientExceptionInterface implementation. This
lets the existing unit tests run against any PSR-7 implementation —
required for the upcoming CI matrix axis that exercises symfony/http-
client + nyholm/psr7 alongside the default Guzzle stack.

Also consolidate RetryingClientTest's inline mock onto the canonical
tests/Util/MockPsr18Client.

Part of #30.
Lets the test run on PSR-18 stacks other than Guzzle.

Part of #30.
Smoke-tests the SDK against a non-Guzzle PSR-18 stack to catch
regressions in the discovery path. Verified locally.

Part of #30.
@kyeh-amp kyeh-amp requested a review from a team April 27, 2026 22:19
Required by Composer 2.2+ allow-plugins enforcement. Without this,
lowest-deps installs blocked at php-http/discovery 1.15.0.
@kyeh-amp kyeh-amp changed the title refactor!: drop guzzle dependency for PSR-18 alignment refactor: drop guzzle dependency for PSR-18 alignment Apr 27, 2026
Pin guzzle ^7.4 + psr7 ^2.0 in require-dev so discovery resolves a
working PSR-17 factory under --prefer-lowest. Disable discovery's
allow-plugins entry to prevent auto-injection of abandoned adapter
packages during composer install.
Lets consumers catch missing-impl distinctly from transport faults.
@kyeh-amp kyeh-amp force-pushed the drop-guzzle-hard-dependency branch from 1a9e655 to e6130c5 Compare April 28, 2026 03:32
The third arg of HttpClientFactory::resolveAll was always null at every
call site and no config builder exposed a setter to override it. Inline
Psr17FactoryDiscovery::findStreamFactory() and drop the dead parameter.
@kyeh-amp kyeh-amp marked this pull request as ready for review April 28, 2026 22:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Get rid of guzzle/http dependency

1 participant