m6web/guzzle-http-bundle
Symfony bundle that integrates the Guzzle HTTP client into your application. Configure Guzzle clients in bundle configuration, inject them as services, and make outbound HTTP calls cleanly. Built-in logging and Symfony profiler integration help you inspect and debug requests and responses.
1. Install and register
composer require m6web/guzzle-http-bundle
// config/bundles.php
return [
// ...
M6Web\Bundle\GuzzleHttpBundle\M6WebGuzzleHttpBundle::class => ['all' => true],
];
There is generally no Flex recipe, so config and registration are manual.
2. Declare your first client
# config/packages/m6_web_guzzle_http.yaml
m6_web_guzzle_http: # confirm root key with config:dump-reference
clients:
github:
base_uri: 'https://api.github.com'
timeout: 3.0
connect_timeout: 1.0
headers:
Accept: 'application/vnd.github+json'
3. Consume it
The bundle exposes one service per configured client, named <alias>.client.<name>:
# config/services.yaml
services:
App\Api\GithubClient:
arguments:
$http: '@m6_web_guzzle_http.client.github'
final class GithubClient
{
public function __construct(private \GuzzleHttp\ClientInterface $http) {}
public function repository(string $owner, string $repo): array
{
return $this->http->get("/repos/$owner/$repo")->getBody()->getContents()
|> json_decode(...); // or plain json_decode in PHP < 8.5
}
}
4. Where to look first
| Need | Look at |
|---|---|
| Option tree / defaults | bin/console config:dump-reference M6WebGuzzleHttpBundle |
| Exact service ids | bin/console debug:container --show-private m6_web_guzzle_http |
| Real capabilities | vendor/m6web/guzzle-http-bundle/README.md |
| Behaviour when docs are thin | DependencyInjection/Configuration.php + DependencyInjection/*Extension.php |
| Runtime introspection | Symfony profiler → the Guzzle panel (debug env only) |
First use case: replace ad-hoc new Client([...]) scattered across services with a single declarative client per upstream, then hit one endpoint and check the profiler panel to confirm the request was captured.
One client per upstream, named after the upstream. billing, github, internal-search. Resist a single generic api client with per-call base_uri overrides — the whole value of the bundle is that the transport contract lives in config, reviewable and diffable per environment.
Wrap Guzzle, don't leak it. The bundle hands you a GuzzleHttp\ClientInterface. Never inject it into a controller and never return ResponseInterface upward. Put a thin domain class in front of it that returns arrays/DTOs and owns error translation:
final class BillingClient
{
public function __construct(private ClientInterface $http) {}
public function invoice(string $id): Invoice
{
try {
$res = $this->http->get("/invoices/$id");
} catch (RequestException $e) {
throw new BillingUnavailable($e->getMessage(), previous: $e);
}
return Invoice::fromArray(json_decode((string) $res->getBody(), true));
}
}
Per-environment overrides. The bundle is pure config, so this is where it shines:
# config/packages/test/m6_web_guzzle_http.yaml
m6_web_guzzle_http:
clients:
github:
base_uri: 'http://127.0.0.1:8081'
timeout: 1.0
Point it at WireMock / a local stub server for integration tests, or override the service in services_test.yaml with a MockHandler-backed client for unit tests.
Cross-cutting concerns via middleware, not call sites. Add correlation IDs, structured request logging, and retry/backoff once. If your version of the bundle exposes handler-stack / middleware options in config, use them; otherwise decorate the service:
App\Api\CorrelatedGithubClient:
decorates: m6_web_guzzle_http.client.github
arguments: ['@.inner']
Fan-out with async. For N independent calls, requestAsync + Utils::settle (tolerates partial failures) or Pool (bounded concurrency) beats sequential calls, and per-client timeout config keeps one slow upstream from stalling the batch.
Typed aliases beat string ids. Define App\Api\GithubClient as the injection target and keep the bundle's service id in exactly one place (services.yaml). If you later drop the bundle, you change one alias.
Compiler pass for many clients. If tenants/environments generate dozens of clients, build the config in a PrependExtensionInterface extension or tag-and-collect rather than hand-writing YAML.
It is a small, low-traffic package (17 stars, zero known dependents). Expect no StackOverflow answers and sparse docs. Budget time to read the source — it is only a handful of classes — and treat it as a thin convenience layer, not infrastructure.
Confirm the config root key per version. Older releases use m6_web_guzzle_http, newer ones may use guzzle_http. Always verify with config:dump-reference rather than copying a snippet from a blog post; a wrong root key silently does nothing in some Symfony versions.
http_errors defaults to true. Guzzle throws RequestException on 4xx/5xx and ConnectException on transport failures. Decide centrally: either set http_errors: false in the client options and handle status codes explicitly, or catch inside your wrapper. Mixed approaches produce "sometimes it throws, sometimes it returns 500" bugs.
Timeouts are not optional. Guzzle's default timeout is effectively infinite. Always set both timeout and connect_timeout. A hung upstream in a queue worker is the classic incident this prevents.
Service ids are shared, stateful singletons. Mocking the client once and reusing it across tests leaks recorded history between cases. Build a fresh MockHandler per test (HandlerStack::create($mock) → new Client(['handler' => $stack])) and override the alias in the container.
The profiler panel is debug-only. Useful for local debugging, useless for production observability. Add your own middleware emitting metrics (duration, status class, upstream name) if you need SLOs.
Guzzle version compatibility. The bundle sits on top of Guzzle 6/7 semantics. Two distinct interfaces are in play — GuzzleHttp\ClientInterface and PSR-18's Psr\Http\Client\ClientInterface. Type-hint the one the bundle actually registers, and check composer.json constraints before a Symfony major upgrade; abandoned bundles are the usual casualty of those.
Handler stack ordering matters. Retry must wrap logging (or logging must wrap retry) depending on whether you want to log attempts or final outcomes. Getting this backwards is a silent source of misleading dashboards.
Don't inject HTTP clients into Doctrine listeners or entities. It creates container cycles and couples persistence to network availability. Keep HTTP at the edges: controllers, message handlers, console commands.
Migration escape hatch. Everything the bundle does can be reproduced with plain services.yaml:
services:
app.github.guzzle:
class: GuzzleHttp\Client
arguments: [{ base_uri: '%env(GITHUB_BASE_URI)%', timeout: 3 }]
If you find yourself fighting the config schema, take that path — you keep the declarative client and lose only the profiler panel.
How can I help you explore Laravel packages today?