pierrerolland/guzzle-config-operations-bundle
Symfony bundle that simplifies Guzzle HTTP client configuration by letting you define and manage client settings through Symfony configuration. It helps keep HTTP client setup consistent and reusable across services, reducing boilerplate for projects that rely on Guzzle.
Before you install, check survivability. This package's last release is 2019-04-17, with 3 stars and 0 dependents. That means it was written against Symfony 3.4/4.x and Guzzle 6, and almost certainly has no PHP 8 return-type annotations, no symfony/deprecation-contracts, and possibly hard version constraints that conflict with Guzzle 7. Start with:
composer require pierrerolland/guzzle-config-operations-bundle --dry-run
If the resolver fights you, you're looking at constraint surgery or a fork — decide that before you write code against it.
Minimal setup (Symfony 4/Flex era):
composer require pierrerolland/guzzle-config-operations-bundleconfig/bundles.php:
Pierrerolland\GuzzleConfigOperationsBundle\GuzzleConfigOperationsBundle::class => ['all' => true],
The exact FQCN is the one thing worth confirming from the repo's README/composer.json autoload block — get it wrong and you get a silent "no services" instead of an error.bin/console config:dump-reference guzzle_config_operations
The alias may differ; find it with bin/console debug:config --help or by grepping the bundle's DependencyInjection/Configuration.php.Where to look first, in this order:
| File | Why it matters |
|---|---|
DependencyInjection/Configuration.php |
The config tree. This is the documentation — keys, nesting, defaults, required fields. |
DependencyInjection/*Extension.php |
How your YAML becomes service definitions; tells you the service IDs and whether clients get public/private aliases. |
Resources/config/services.xml (or .yaml) |
Base service IDs and tags you can decorate or override. |
Tests/ |
With 3 stars there's likely no docs site; the test fixtures are the real usage examples. |
composer.json |
Actual Guzzle/Symfony constraint ranges — decides whether this is viable at all. |
First use case. Don't start with your whole API surface. Define one client with one operation, inject it, and make a call:
# config/packages/guzzle_config_operations.yaml (illustrative — confirm keys)
guzzle_config_operations:
clients:
billing:
base_uri: 'https://billing.internal'
timeout: 5.0
operations:
get_invoice:
method: GET
uri: 'invoices/{invoiceId}'
Then bin/console debug:container guzzle --show-private to see what service ID you actually got, and inject that. The whole point of the bundle is that the client is a container-managed service, so autowire should work once you know the ID.
Treat the config as an operation registry, not as a client factory. The bundle's value proposition is "reusable, composable config operations" — so the idiomatic use is one YAML block per upstream API, listing every operation you call against it, with stable names. Code then refers to operations by name instead of rebuilding URLs inline. This is what makes the package worth a look: the operation list becomes a reviewable contract for what your app does to a third party.
One file per upstream. config/packages/http/billing.yaml, config/packages/http/shipping.yaml. With many clients, a single monolith YAML becomes unreviewable, and per-file config also gives you clean per-environment overrides:
# config/packages/test/http/billing.yaml
guzzle_config_operations:
clients:
billing:
base_uri: 'http://localhost:8080' # WireMock / MockServer
That's the most underrated use of a config-driven client: the operation definitions stay byte-identical between dev, test, and prod, and only the transport target changes.
Wrap it, don't leak it. The bundle's services are Guzzle clients. Don't inject a raw Guzzle client into a domain service — inject it into a thin gateway class in your own namespace:
final class BillingGateway
{
public function __construct(private ClientInterface $billingClient) {}
public function invoice(string $id): array
{
return json_decode(
(string) $this->billingClient->request('GET', "invoices/{$id}")->getBody(),
true,
512,
JSON_THROW_ON_ERROR
);
}
}
Wire the bundle's client into it via a named alias in services.yaml. Two benefits: if you later migrate to symfony/http-client (which you probably will), you rewrite one class; and your domain tests can fake BillingGateway without touching PSR-18 plumbing.
Environment-scoped parameters via %env()%, deliberately. Base URIs and credentials belong in env, referenced as '%env(BILLING_BASE_URI)%'. Note that env resolution timing matters — see gotchas.
Middleware/handler stack is the extension seam. Auth headers, correlation IDs, retry, and logging are all HandlerStack middlewares. Build them as small tagged services and push them onto each client's stack in a compiler pass or a small factory service, rather than copy-pasting YAML per client. If the bundle exposes a handler/middleware config key, that's your hook; if not, decorate the client service.
Testing. Use GuzzleHttp\Handler\MockHandler in a test-environment service definition that swaps the handler, so the operation config under test is the production config. Queue responses and assert on $mock->getLastRequest(). This is cheap and catches the config errors (typo'd base_uri, wrong method) that a mocked gateway would hide.
Composability tip. If the bundle supports multiple config files or a fragment mechanism, lean on it for shared defaults (timeouts, user-agent, retry policy) and keep per-API files limited to what's genuinely different. Duplicated timeout: 5.0 across twelve files is how these setups rot.
Maintenance risk is the headline gotcha. Last release 2019, 3 stars, 0 dependents. Expect: no PHP 8.1+ #[ReturnTypeWillChange] fixes, Guzzle 6 type signatures (e.g. ClientInterface::request signatures changed subtly), Symfony 5/6 removal of ContainerBuilder APIs it may use. Pin it ("pierrerolland/guzzle-config-operations-bundle": "dev-master#<sha>") and treat it as vendored code you own. If you need more than a week's friction to make it run, the honest answer is that eightpoints/guzzle-bundle or plain PSR-18 with symfony/http-client will cost less.
The config tree is undocumented — introspect it, don't infer it. bin/console config:dump-reference <alias> and bin/console debug:config <alias> are the only reliable sources. Guessing YAML keys from the package name will burn an afternoon on "Unrecognized option" errors.
Config cache in prod. Symfony compiles extension config into the container cache. Editing YAML on a running production box does nothing until bin/console cache:clear (or a deploy that rebuilds). Symptom: "I changed the base URI and it's still hitting staging." Also ensure var/cache is writable by the deploy user — permission errors here surface as confusing missing-service exceptions.
%env()% resolution timing. Env vars referenced in bundle config are resolved at container compile time in most setups, not per-request. If you need a per-environment base URI that isn't known at build time, this is a landmine — the value gets baked into the compiled container. Confirm whether the bundle defers resolution; if not, build separate container caches per environment, or move the dynamic part into a middleware.
Secrets. Don't inline API tokens in YAML that lands in git. Use %env(BILLING_TOKEN)%, Symfony secrets vault, or a token-provider middleware. A token in a compiled container cache is also readable by anything that can read var/cache.
Debugging failed requests. In order: debug:container --show-private | grep -i guzzle to confirm the service exists and get its ID; check the handler is CurlHandler not StreamHandler if you see weird SSL/timeout behaviour; enable GuzzleHttp\Middleware::history and log $transferTime/$effectiveUri. Guzzle's exception messages truncate the body — catch RequestException, read getResponse()->getBody() before it's consumed.
Guzzle's default timeout is none. If the bundle doesn't inject timeout/connect_timeout defaults, a stalled upstream will hang your PHP workers until max_execution_time. Set both explicitly in every client block; this is the single most common production incident in Guzzle setups.
Overriding services: decorate, don't replace. The extension registers services under IDs you don't control. Use decorates: in services.yaml so you keep the bundle's wiring; replacing by ID breaks silently when the bundle changes its naming scheme.
Extension points worth knowing. Look for tagged services or a factory interface for custom client/operation construction, and a compiler pass for programmatically registering operations (useful when operations are generated from an OpenAPI spec). If neither exists, the config tree is your only seam — which argues for keeping your gateway layer thick.
When not to use it. For one or two HTTP clients, this bundle is strictly more moving parts than new Client([...]) in a service definition. Its value only amortizes across many clients with overlapping operation sets — and given its age, "many clients" is exactly the scenario where you'd rather not be on unmaintained code.
How can I help you explore Laravel packages today?