Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Guzzle Bundle Laravel Package

eightpoints/guzzle-bundle

EightPoints GuzzleBundle integrates Guzzle 6.x/7.x with Symfony, letting you configure HTTP clients in YAML, register services, and add plugins such as retry, logging, and caching. It supports Symfony 5–8 and PHP 7.2+, with a Flex recipe and event hooks.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and let Flex do the wiring

composer require eightpoints/guzzle-bundle

With Symfony Flex the recipe registers EightPointsGuzzleBundle in config/bundles.php and scaffolds config/packages/eight_points_guzzle.yaml. Without Flex, add the bundle class manually and create that config file yourself (all env β†’ true).

2. Declare one client per upstream API β€” this is the file you will open 90% of the time:

# config/packages/eight_points_guzzle.yaml
eight_points_guzzle:
    logging: '%kernel.debug%'
    slow_response_time: 1000

    clients:
        payment:
            base_url: '%env(PAYMENT_API_URL)%'
            lazy: true
            options:
                headers:
                    Accept: 'application/json'
                auth: ['%env(PAYMENT_API_USER)%', '%env(PAYMENT_API_PASSWORD)%']
                timeout: 30
                connect_timeout: 5

Anything under options is passed straight through to Guzzle's client constructor, so the Guzzle request-options docs are your real reference β€” the bundle config is a thin wrapper.

3. Look up the generated service id

The container key is deterministic: eight_points_guzzle.client.<name>. Verify with:

php bin/console debug:container eight_points_guzzle.client.payment

4. First use case: inject it into a controller or service

namespace App\Controller;

use GuzzleHttp\ClientInterface;
use Symfony\Component\DependencyInjection\Attribute\Target;

class PaymentController
{
    public function __construct(
        #[Target('eight_points_guzzle.client.payment')]
        private ClientInterface $paymentClient,
    ) {}
}

Because the service id is not a FQCN, plain ClientInterface $client will not autowire β€” you need #[Target] (Symfony 6.2+), a named alias, or explicit arguments in services.yaml. See src/Resources/doc/autowiring-clients.md for the alias/bind recipes. Manual wiring equivalent:

services:
    app.payment_controller:
        class: App\Controller\PaymentController
        arguments: ['@eight_points_guzzle.client.payment']

5. First call

$response = $this->paymentClient->request('POST', '/v1/charges', [
    'json' => ['amount' => 1000, 'currency' => 'EUR'],
]);
$data = json_decode((string) $response->getBody(), true);

Then open the profiler panel in dev β€” every request shows up with method, URI, status and timing. That panel is your fastest feedback loop for the whole bundle.

⚠️ Framing note: this is a Symfony bundle. In a Laravel app you cannot meaningfully install it (it depends on symfony/http-kernel and symfony/dependency-injection); there you'd use guzzlehttp/guzzle directly or Laravel's Http facade. The guidance below assumes you work in a Symfony codebase β€” mentally map "service container" β†’ "service provider bindings" and "event listener tag" β†’ "event subscriber".

Implementation Patterns

One client per upstream, aliased to a domain name. The bundle's service ids are implementation details. Wrap them so application code depends on your name, not the bundle's:

services:
    app.crm_client: '@eight_points_guzzle.client.crm'

In autowired projects use #[Target] on the constructor argument instead; the alias trick is for manual wiring and for making a future swap to a hand-rolled client a one-line change.

Treat lazy: true as the default for anything not hit on every request. Lazy clients are only instantiated when first used, so a dashboard page doesn't pay for building five Guzzle clients and their handler stacks. Set it on payment/shipping/ERP clients; skip it on the one client your hot path always calls (the lazy proxy adds a tiny indirection).

Centralise auth and tracing in event listeners, not in every call site. The bundle dispatches eight_points_guzzle.pre_transaction / post_transaction, plus per-client variants suffixed with the client name:

services:
    app.guzzle_trace_listener:
        tags:
            - { name: 'kernel.event_listener', event: 'eight_points_guzzle.pre_transaction', method: 'onPreTransaction' }
            - { name: 'kernel.event_listener', event: 'eight_points_guzzle.post_transaction.payment', method: 'onPaymentResponse' }
public function onPreTransaction(PreTransactionEvent $event): void
{
    $request = $event->getTransaction()->getRequest();
    $request = $request->withHeader('X-Request-Id', $this->requestId->get());
    $event->getTransaction()->setRequest($request);
}

This is the idiomatic place for correlation IDs, tenant headers, refreshing OAuth tokens, and recording upstream latency metrics. Client-specific listeners run after generic ones and win on collisions β€” useful when one upstream needs to override a global header.

Use the event hooks for metrics and alerting, not for shaping bodies. pre_transaction is ideal for headers; parse/validate responses in your own service layer after the call so exceptions and retries stay predictable.

Pick a plugin instead of writing middleware. Retry, cache, OAuth2, WSSE and header-forward plugins exist (EugenGanshorn/GuzzleBundleRetryPlugin, gregurco/GuzzleBundleCachePlugin, etc.). Registration goes through the bundle constructor in src/Kernel.php:

if ($class === \EightPoints\Bundle\GuzzleBundle\EightPointsGuzzleBundle::class) {
    yield new $class([new \EugenGanshorn\GuzzleBundleRetryPlugin\GuzzleBundleRetryPlugin()]);
} else {
    yield new $class();
}

Prefer a plugin for cross-cutting HTTP concerns (retry, cache, auth handshakes) and the bundle's options: block for per-client tuning.

Environment-driven configuration everywhere. Put base_url, credentials and timeouts behind %env(...)% (see environment-variables-integration.md). This lets staging and prod share one YAML file and keeps secrets out of the repo.

Testing pattern. The cleanest seam is to swap the client service in the test container for one built with Guzzle's MockHandler, or to assert on the recorded history:

$mock = new MockHandler([new Response(200, [], '{"ok":true}')]);
$history = [];
$client = new Client(['handler' => HandlerStack::create($mock)]);

Then bind eight_points_guzzle.client.payment to that instance. Also assert your pre_transaction listener fires β€” that logic is easy to break silently.

Async where fan-out matters. The injected client is a real Guzzle client, so requestAsync() and Utils::settle() work as documented; just remember lazy clients resolve on first requestAsync call, and unhandled promise rejections surface as exceptions only when you wait on them.

Gotchas and Tips

Autowiring a bare GuzzleHttp\Client fails. There's no class-based alias, so Symfony either errors with "cannot autowire" or, worse, injects the wrong client when several exist. Always disambiguate with #[Target('eight_points_guzzle.client.<name>')] or a named alias. If you get "too many candidates", this is why.

base_url + a leading slash in the path. Guzzle resolves URIs per RFC 3986: base_url: 'http://api.example/v1' plus request('GET', '/users') yields http://api.example/users, silently dropping /v1. Use 'users' (no leading slash) or include the full prefix in base_url.

The curl option block is transformed, not passed raw. Keys are CURLOPT_* constants with the prefix stripped and lowercased (CURLOPT_SSLVERSION β†’ sslversion), and the values are still raw integers. That makes them brittle across PHP/cURL builds and opaque in review. Prefer verify, cert, timeout, connect_timeout and proxy in options:; reach for curl: only when nothing else works. Recent releases fixed these docs β€” check the version you're on.

timeout: 0 means infinite, not "default". And slow_response_time only flags slow calls in the profiler/logs; it never aborts the request. Actual enforcement requires timeout and connect_timeout. Set both explicitly on every client, or one hung upstream will pin PHP-FPM workers.

HTTP errors throw. A 4xx/5xx raises GuzzleHttp\Exception\RequestException/ClientException/ServerException unless you catch it or follow disable-exception-on-http-error.md to make the client return responses for error statuses. Decide per client: throwing is right for "this must have worked" APIs; swallowing is right for upstreams that use 404 semantically. Don't set it globally β€” you'll lose failures silently.

Logs go to Symfony's @logger, not a named channel. Format is eight_points_guzzle.{level}: {method} {uri} {code}, and it's controlled by logging: true (defaults to %kernel.debug%, so nothing is logged in prod unless you set it explicitly). Tune the line shape by overriding the eight_points_guzzle.symfony_log_formatter.pattern parameter using Guzzle MessageFormatter placeholders. Note the profiler panel only exists in debug, so "it worked in dev, no logs in prod" is a config issue, not a bug.

Lazy clients and the profiler interact oddly. In dev, data collectors and some compiler passes can force instantiation, so lazy services may not look lazy in the toolbar. Don't use the toolbar to judge whether laziness is working β€” check with a debug:container --show-private diff or an actual cold request in prod-like cache.

Changing YAML does nothing until you clear cache. config/packages/eight_points_guzzle.yaml is compiled into the container. After edits run bin/console cache:clear (dev usually rebuilds, but stale container files bite in CI and in --env=prod).

Event listeners are the wrong place for response content mutation. post_transaction fires after the round trip; if a response is an error status and exceptions are enabled, the event has already been dispatched but the exception still propagates afterwards β€” your listener can't "fix" the call. Use middleware/plugins for anything that must influence the result, and keep listeners to observability.

Retries need idempotency discipline. Adding the retry plugin globally will happily retry a POST /charges. Scope it per client, restrict it to idempotent methods or to 429/503-with-Retry-After, and remember the request body must be seekable β€” non-rewindable streams (large uploads) break on retry.

Plugin registration in src/Kernel.php is Symfony 6.4+/7 MicroKernelTrait style. On older skeletons the kernel lives elsewhere; the snippet simply won't apply. Also register plugins before relying on their per-client plugin: config keys β€” an unknown plugin name silently configures nothing useful.

Debugging checklist when a call misbehaves: debug:container eight_points_guzzle.client.<name> β†’ confirm the merged base_url/options; check the profiler panel for the actual URI and status; logging: true + a tailed log for the same; then reproduce with a standalone Guzzle client using identical options to prove whether it's bundle config or upstream behaviour.

Extension point to remember: redefine-client-class.md lets you substitute your own client class if you need global helper methods β€” that's a better lever than copy-pasting request-building code across services, and it keeps the bundle's plugin/event machinery intact.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc