freshcells/guzzle-bundle
A Symfony bundle that integrates the Guzzle HTTP client into your application, offering service configuration and dependency injection so you can make and manage outgoing HTTP requests cleanly within Symfony projects.
Three steps and you have a configured, profiler-visible client instead of a new Client() scattered across your services.
composer require freshcells/guzzle-bundle
// config/bundles.php
return [
// ...
Csa\Bundle\GuzzleBundle\CsaGuzzleBundle::class => ['all' => true],
];
# config/packages/csa_guzzle.yaml
csa_guzzle:
clients:
github:
config:
base_url: 'https://api.github.com'
timeout: 5
connect_timeout: 2
headers:
Accept: 'application/vnd.github.v3+json'
User-Agent: 'my-app/1.0'
The bundle registers each client as a service named csa_guzzle.client.<name>, so you inject @csa_guzzle.client.github wherever you need it. If you configure a client called default, the bundle also wires a default client service you can alias for autowiring:
services:
GuzzleHttp\ClientInterface: '@csa_guzzle.client.github'
First use case: replace every hand-rolled new GuzzleHttp\Client([...]) in your app with a named, config-driven client. You immediately gain a web-debug-toolbar panel showing every outbound request (URL, headers, status, timing, body), centralised timeouts, and one place to add auth headers.
Where to look first: DependencyInjection/Configuration.php in the vendor directory. It is the authoritative list of what you may put under clients.<name>.config — don't guess at option names, read the tree.
One named client per upstream. github, stripe, internal_erp — each with its own base_url, auth, and timeouts. Resist the single "generic" client; per-upstream clients make middleware and mocking tractable.
Middleware is the main extension point. Write a __invoke(callable $handler): callable class and tag it:
services:
App\Guzzle\CorrelationIdMiddleware:
tags:
- { name: csa_guzzle.middleware, alias: correlation_id, priority: 10 }
This is where auth-token refresh, correlation IDs, structured logging, and Guzzle's own RetryMiddleware belong. Tagged middleware applies to the bundle's client stack; priority controls ordering, which matters — auth middleware must run before logging middleware or your logs will show unauthenticated requests.
Events for cross-cutting instrumentation. The bundle dispatches Symfony events around requests (before/after/error — check the bundle's events class for the exact constant names before wiring listeners). Prefer a listener over middleware when the concern is observability-only and shouldn't be able to mutate the request.
Testing with a mock handler. In config/packages/test/csa_guzzle.yaml, redefine the client service with a MockHandler + HistoryMiddleware handler stack, or build test clients through the bundle's client factory service. Assert on $history rather than hitting the network. Keep a small fixture of recorded responses per upstream so contract changes fail loudly.
Retries belong in a middleware, not in your service methods. A RetryMiddleware with exponential backoff on 429/5xx will save you more code and incidents than any wrapper class — but only around idempotent calls (see gotchas).
Laravel reality check: this is Symfony-only. In a Laravel app you'd get the same outcomes from the Http facade or a Guzzle client bound in a service provider; the bundle's config tree, profiler panel, and csa_guzzle.middleware tag have no equivalent there. The assessment below is only relevant to Symfony projects in your estate.
composer.json, check the last commit date and the Symfony version constraint before adopting, and keep a one-afternoon exit plan: plain Guzzle service definitions in services.yaml reproduce ~90% of the value.clients.<name>.config. Flattening them to clients.<name>.base_url is the single most common first mistake. The config tree will usually reject unknown keys, which is a decent error message — read it rather than guessing.Psr\Http\Client\ClientInterface only exists from Guzzle 7. If you type-hint it, verify the resolved Guzzle version, or you'll get a confusing "service not found" for a client that demonstrably exists.base_url semantics bite. Guzzle's base_uri resets the path on absolute-looking paths: /users discards any path prefix in the base. Use relative URIs (users) or the full URL, and test both.http_errors defaults to true. 4xx and 5xx throw GuzzleHttp\Exception\RequestException. Either catch GuzzleException at the boundary or set http_errors: false and branch on status codes — don't let a 404 from a third party bubble up as a 500 in your app.$request->getBody()->rewind() in the retry decision callback, and never retry POSTs that create resources.timeout and connect_timeout. Guzzle's default is 0 (infinite). An unresponsive upstream will otherwise hang a PHP-FPM worker until max_execution_time.alias will silently shadow one another. Namespace your aliases (app.auth, app.logging).Pool, cookies, curl options), get the client from the bundle's client factory or decorate csa_guzzle.client.<name> in services.yaml — the config tree only exposes the common surface.How can I help you explore Laravel packages today?