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

leaseweb/guzzle-bundle

A Symfony bundle that integrates Guzzle into the service container, making it easy to configure HTTP clients, middleware, and plugins for clean, testable outgoing requests in PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: leaseweb/guzzle-bundle is a Symfony 2 bundle, not a Laravel package. It hooks into AppKernel::registerBundles(), symfony/dependency-injection compiler passes, and Symfony's DataCollector contract for the Web Debug Toolbar. None of that has surface area in a Laravel app, and the Composer graph (symfony/framework-bundle 2.x, Guzzle 5/6-era APIs) will not resolve alongside Laravel 10+/Symfony 6+ components. So "day one" for a Laravel developer is not composer require.

What you actually do on day one:

  1. Read the bundle for the pattern, not the code. Two files tell you everything: DependencyInjection/Configuration.php (the config schema — clients.<name>.base_url, headers, options, handler) and the DataCollector class (how requests/responses get captured for the toolbar). Those are the two ideas worth stealing: centrally configured, injectable HTTP clients and dev-time request visibility.
  2. Rebuild the config-client half in Laravel. One config entry per upstream API, one factory, one binding.
// config/services.php
'github' => [
    'base_uri' => 'https://api.github.com/',
    'headers'  => [
        'Accept'     => 'application/vnd.github.v3+json',
        'User-Agent' => 'acme-app',
    ],
    'timeout'         => 5,
    'connect_timeout' => 2,
],
// app/Providers/GuzzleServiceProvider.php
public function register(): void
{
    $this->app->singleton('http.github', function ($app) {
        $cfg = $app['config']['services.github'];

        return new \GuzzleHttp\Client([
            'base_uri'        => $cfg['base_uri'],
            'headers'         => $cfg['headers'],
            'timeout'         => $cfg['timeout'],
            'connect_timeout' => $cfg['connect_timeout'],
            'handler'         => $this->buildStack(),
        ]);
    });
}

private function buildStack(): \GuzzleHttp\HandlerStack
{
    $stack = \GuzzleHttp\HandlerStack::create();
    $stack->push(\GuzzleHttp\Middleware::log(
        $this->app['log']->channel('http'),
        new \GuzzleHttp\MessageFormatter('{method} {uri} -> {code} {error}')
    ));

    return $stack;
}
  1. First real use case: a flaky third-party API (payments, auth, shipping) where you cannot see the outbound call. Inject the client into the service that owns that integration, and confirm you can read method, URL, status, and timing in logs before you touch any business logic.
class GitHubClient
{
    public function __construct(private \GuzzleHttp\Client $client) {}

    public function repo(string $owner, string $name): array
    {
        return json_decode(
            $this->client->get("repos/{$owner}/{$name}")->getBody()->getContents(),
            true
        );
    }
}

Bind GitHubClient with 'http.github' in a service provider. In tests, swap the handler. That is the day-to-day loop.

Implementation Patterns

Named clients as container entries + typed wrappers. Mirror the bundle's "multiple named clients" idea by keeping config in config/services.php and wrapping each in a small, injectable class. Don't inject raw Client into ten services — you lose type safety and every consumer reaches for arbitrary URLs. One wrapper per upstream API, one method per endpoint.

Use Illuminate\Support\Facades\Http unless you need raw Guzzle. It is already a Guzzle wrapper, it is container-friendly, and it has built-in fakes/retries. Reach for GuzzleHttp\Client directly only when you need custom middleware, streaming, connection pooling, or a custom handler. The bundle's entire value proposition — "configured client, injected as a service" — is already covered by a config entry plus Http::withHeaders(...)->baseUrl(...).

Per-client defaults and headers. Keep auth headers out of a shared client. If two integrations use different bearer tokens, give them separate config entries and separate bindings; a single shared Client with headers mutated at runtime is the classic token-leak bug in multi-tenant apps.

Middleware as the extensibility seam. This is the real parallel to the bundle's "pluggable middleware and event dispatching." Build a HandlerStack factory and compose: logging, history (for tests and a debug panel), retry, correlation-ID injection, timing.

$history = \GuzzleHttp\Middleware::history($container);   // ArrayAccess collector
$stack->push($history);

$stack->push(\GuzzleHttp\Middleware::retry(
    fn ($retries, $req, $res, $e) => $retries < 2 && (! $res || $res->getStatusCode() >= 500),
    fn ($retries) => 100 * 2 ** $retries
));

Dev-time visibility = Telescope or a custom collector, not a Symfony toolbar. The bundle's selling point is request/response inspection in the profiler. In Laravel that's laravel/telescope (or itsgoingd/clockwork), plus Guzzle logging middleware. If you want the exact toolbar ergonomics, write a tiny middleware that appends {method, uri, status, duration, truncated body} to a singleton collector and render it in a dev-only Blade partial or with Telescope's tags.

Event integration. The bundle dispatches through Symfony's dispatcher. In Laravel, either bridge Guzzle middleware to event() or — more cheaply — listen to Illuminate\Http\Client\Events\* when using the Http facade (RequestSending, ResponseReceived, ConnectionFailed). Those are first-class and require no middleware.

Testing pattern (better than what the bundle offers).

Http::fake(['api.github.com/*' => Http::response(['id' => 1], 200)]);

// or, at the Guzzle layer:
$mock = new \GuzzleHttp\Handler\MockHandler([new Response(200, [], '{"id":1}')]);
$client = new Client(['handler' => HandlerStack::create($mock)]);

Assert on Http::assertSent() / on the history container. No network, no bundle.

Migration pattern if you actually have legacy Symfony 2 code. Strangler-fig: (1) inventory every lsw_guzzle.clients.* entry and every injected service id; (2) map each to a config entry + binding in the new stack; (3) port middleware one at a time behind a flag; (4) run both paths against a non-prod endpoint and diff payloads; (5) cut over per-endpoint, retire the bundle last.

Gotchas and Tips

The dependency wall is the first thing you hit, and it is not negotiable. Symfony 2 bundles require symfony/framework-bundle in the 2.x range — abandoned, PHP-capped, and in direct conflict with the Symfony 6/7 components Laravel itself depends on. A modern Laravel app will not resolve this. If a teammate says "just install it," the answer is no; port the idea.

Guzzle generation mismatch. Symfony 2-era code assumes Guzzle\Service\Client, event subscribers/plugins, and the Guzzle 3/5/6 API. Guzzle 7 uses GuzzleHttp\HandlerStack + middleware and has no Service\Client. Any snippets you find for this bundle will not typecheck against Guzzle 7. Budget for translation, not copy-paste.

timeout defaults to 0, which means no timeout. Guzzle's default is infinite. This is the single most common cause of hung workers and queue jobs that never die. Always set both timeout and connect_timeout per client, and set them lower than your queue's visibility timeout / job timeout.

base_uri URI resolution is subtle. Relative URIs resolve against base_uri per RFC 3986. If base_uri is https://api.example.com/v1/ and you request repos/x, you get /v1/repos/x; if you request /repos/x (leading slash) you drop /v1. Keep the trailing slash on base_uri and never lead with / on relative paths.

http_errors is true by default. 4xx/5xx throw RequestException; connection failures throw ConnectException, which has no response object. Guard accordingly — $e->getResponse() is null on connect failures, and getResponse()->getBody()->getContents() is consumed-once unless you rewind. Call ->getBody()->rewind() or cast to string before reading twice.

Middleware order is LIFO. $stack->push() puts middleware on top, so the last pushed runs first on the way out. If your history middleware shows a request before retries happen, or logging shows stale responses, your ordering is inverted. Push history first (innermost) if you want one record per attempt; push it last if you want the final result only.

HandlerStack::create() is not free. If you build your own stack, you keep the curl-multi default handler. If you override handler, you lose connection reuse, redirects, and proxy-from-env behavior. Only replace handler in tests.

Retry middleware does not retry POST by default in spirit — and you must decide. Middleware::retry will happily retry a non-idempotent POST and double-charge a customer. Gate retries on method/idempotency key, cap attempts (2–3), and add jitter to the exponential backoff.

Redact before you log or collect. Toolbar/profiler-style collectors retain full request and response bodies — Authorization, cookies, API keys, PII. This is a real leak vector, not a theoretical one. Redact in the MessageFormatter ({req_headers} includes auth headers), and never register the collector in production: gate on app()->environment('local') && config('app.debug'), not on a convention you hope is respected.

Memory growth in collectors. Retaining bodies across a long-running queue worker will bloat and eventually OOM. Cap capture size, store only headers + status for large payloads, or restrict collection to HTTP requests only.

Archived + zero dependents means you own every future fix. If you vendor the bundle into a legacy Symfony 2 app, pin an exact version, add composer patches for any CVE in the transitive tree, and budget a standing owner for PHP/Guzzle compatibility patches. There is no upstream remedy and no community corpus — Stack Overflow will not have your answer.

Extension points, if you must. In the bundle: a custom handler service id per client, event subscribers for request lifecycle, and the data collector. In Laravel: your HandlerStack factory, Guzzle middleware, and the Http client events. Wire new behavior there — never by subclassing the bundle.

Http::fake matches in declaration order and needs explicit patterns. Http::fake(['api.github.com/*' => ...]) will not match api.github.com without a path; a catch-all '*' placed first shadows everything after it. Http::preventStrayRequests() in test setup catches unmocked calls early, which is worth the one line.

Don't let a dependency-resolution experiment become the critical path. If someone insists on evaluating the bundle against a Laravel target, timebox it to a single afternoon and stop the moment Composer fails — which it will. Spend the remaining hour writing the middleware instead; the capability is worth having, this package is not the vehicle.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle