mapudo/guzzle-bundle
Symfony bundle that integrates the Guzzle HTTP client, providing a configurable service for making HTTP requests, with profiling and easy dependency injection into your applications.
Install and register. The bundle targets Guzzle 6 / Symfony 3–4 era projects, so pin your PHP and Symfony versions before pulling it in.
composer require mapudo/guzzle-bundle:^2.3
Register Mapudo\Bundle\GuzzleBundle\MapudoGuzzleBundle in app/AppKernel.php (or config/bundles.php on Symfony 4).
Define a client in config. Everything flows from the mapudo_guzzle tree — this is where you declare one named client per remote API:
# app/config/config.yml
mapudo_guzzle:
clients:
acme_api:
base_uri: "https://api.acme.test/v1/"
timeout: 10
headers:
Accept: "application/json"
Look here first. Open Mapudo\Bundle\GuzzleBundle\Resources\config/services.xml. That file is the authoritative list of service ids — most confusion around this bundle comes from guessing ids instead of reading it. The central one is the client builder (mapudo.guzzle.client_builder), which turns a client name from your config into a configured GuzzleHttp\Client.
First use case. Inject the builder into a gateway service and build the client lazily, rather than constructing new Client([...]) inline:
use Mapudo\Bundle\GuzzleBundle\Client\ClientBuilder;
class AcmeGateway
{
private $clientBuilder;
public function __construct(ClientBuilder $clientBuilder)
{
$this->clientBuilder = $clientBuilder;
}
public function fetchUser(int $id): array
{
$client = $this->clientBuilder->build('acme_api');
$response = $client->get('users/' . $id);
return json_decode($response->getBody()->getContents(), true);
}
}
That is the whole loop: config → builder → client → PSR-7 response.
One named client per upstream. Don't try to make a single client serve many hosts. Separate entries in clients: let you set distinct base_uri, auth, timeouts, and handler stacks, and they make it obvious in profiling which backend is slow.
Wrap the builder, not the client. Because build() is cheap and returns a fresh client each call, the idiomatic pattern is a thin gateway class per API (as above) that knows the client name and the endpoint shapes. Your application code then depends on AcmeGateway, not on Guzzle at all — which is what makes it testable.
Symfony events instead of Guzzle middleware. The bundle bridges Guzzle's middleware system to the Symfony event dispatcher, dispatching events around transactions and requests (see Mapudo\Bundle\GuzzleBundle\Events\GuzzleEvents for the constants). This is the cleanest extension point for cross-cutting concerns — request/response logging, timing, correlation IDs, metrics:
use Mapudo\Bundle\GuzzleBundle\Events\GuzzleEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ApiTimingSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
GuzzleEvents::POST_TRANSACTION => 'onTransaction',
];
}
public function onTransaction($event): void
{
// inspect the request/response, push to statsd, etc.
}
}
Register the subscriber normally; the bundle's dispatcher is the app dispatcher, so no extra wiring.
Channel-scoped logging (the headline of 2.3.0). LogMiddleware can now be registered with an explicit channel, so each client (or group of clients) can log to its own Monolog channel instead of everything landing in the default one. Register the middleware per client and pass the channel you want injected; the middleware then resolves a logger for that channel rather than reusing the generic one. This is the feature to reach for when you want, say, acme_api traffic in logs/api_acme.log while billing_api goes somewhere else with a different level or formatter:
# monolog.yaml — one channel per upstream
monolog:
channels: ["api_acme", "api_billing"]
# config.yml — the channel is what gets injected into LogMiddleware
mapudo_guzzle:
clients:
acme_api:
middleware:
log:
channel: api_acme
Confirm the exact key names against DependencyInjection/Configuration.php for your version before copying — the channel option is new in 2.3.0, and older configs that omit it keep the previous default-logger behaviour. If you already run a hand-rolled logging middleware purely to get per-API log routing, this release lets you delete it.
Async where latency matters. Guzzle 6 promises work unchanged — $client->getAsync(...) returns a promise you can settle with Utils::settle() or Promise\all(). The bundle adds nothing here, but it doesn't get in the way either. Use this in console commands or batch importers where N sequential calls would otherwise dominate runtime.
Profiler integration. The bundle ships a data collector that surfaces requests in the Symfony web profiler. Day-to-day this is the fastest way to answer "what did this page actually call, and how long did it take?" — enable it in dev/test only.
Testing. Don't reach for the container in unit tests. Construct the Guzzle client directly with a MockHandler and a HandlerStack, or stub your gateway class. If you must exercise the bundle's wiring — including that the right logger channel is wired into LogMiddleware — swap the builder service in the test container and assert against a test logger rather than a real handler.
It is unmaintained — and 2.3.0 is the last release. v2.3.0 (12 April 2019) is both the feature release that added channel-aware logging and, as far as the tag history shows, the final release. That means the package is still Guzzle 6 only, with no Guzzle 7 support and no fixes for newer Symfony DI or PHP 8.x strictness. Treat 2.3.0 as the ceiling: budget for a fork or a migration path before adopting this in anything long-lived. For new Symfony projects, symfony/http-client or Guzzle used standalone with a small factory is usually less friction than this bundle.
Clients are built on demand, not shared as services. Repeated build('acme_api') calls produce separate client instances and rebuild the handler stack. That's fine for readability but wasteful in hot paths — hold the built client in a property on your gateway if you're issuing many calls per request.
The config tree is shallow. Only a documented subset of Guzzle options is exposed. If you need something exotic (custom handler stack, on_stats, cookies, retry middleware), you'll have to configure it in code or decorate the builder — check DependencyInjection/Configuration.php and MapudoGuzzleExtension for exactly what passes through before assuming a key is supported. The logging channel option landed in the same tree in 2.3.0, so re-read Configuration.php after upgrading rather than trusting older examples.
A new channel needs a Monolog channel declared. Passing a channel name to LogMiddleware does not create the channel — Monolog only knows the channels you list under monolog.channels. Miss that step and you either get a logger that silently falls back to the default channel or a "channel not found" surprise depending on how the logger factory is wired. Declare the channel and verify in dev that a log line actually lands where you expect.
Check the version constraint before relying on channels. If your composer.json allows ^2.0, a composer update may or may not pull 2.3.0 depending on your lockfile, and channel config on an older 2.x will be ignored or rejected at container compile time. Pin ^2.3 explicitly if you intend to use per-client log routing, and run a container compile (bin/console cache:clear / debug:container) as your first check after upgrading.
base_uri trailing slash matters. Guzzle resolves relative URIs against it, so https://api.test/v1 and https://api.test/v1/ produce different final URLs when you call $client->get('users'). Pick one convention and stick to it in config.
Exceptions are Guzzle's, not the bundle's. 4xx/5xx throw GuzzleHttp\Exception\ClientException / ServerException unless you set http_errors: false. Catch GuzzleHttp\Exception\GuzzleException at your gateway boundary and translate to domain exceptions — otherwise Guzzle leaks into controllers.
Guard against listener recursion. An event subscriber that itself performs an HTTP call through the same bundle will re-trigger the dispatcher. The same trap applies to logging: if a handler on the logging channel ships entries to a remote collector over Guzzle, you get a loop. Keep the logging path strictly local.
Service ids are internal. mapudo.guzzle.client_builder and friends are private-ish container details that have changed semantics across Symfony versions. Prefer constructor injection with the class name and let autowiring (or an explicit alias in services.yml) resolve it, so an upgrade doesn't break string-based get() calls.
Disable profiler collection — and be deliberate about log channels — in production. The data collector retains request/response data per request, so leaving it on in prod is a memory and PII liability. The new per-channel logging is useful in prod precisely because it lets you tune verbosity and retention per upstream, but remember that request bodies and auth headers are still what LogMiddleware sees; set levels and redaction accordingly.
How can I help you explore Laravel packages today?