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

Http Batch Guzzle Laravel Package

friendsofouro/http-batch-guzzle

A Laravel/PHP package for batching HTTP requests with Guzzle, letting you group multiple calls and handle responses together for more efficient endpoint interactions.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require friendsofouro/http-batch-guzzle

The package is a thin, opinionated layer over Guzzle that lets you queue many requests and dispatch them concurrently instead of writing Promise\settle() boilerplate by hand. Two things to find first:

  1. The ClientInterface implementation — this is the seam the whole package is built around. Resolve it from the container (app(ClientInterface::class)) rather than instantiating anything directly; that's what makes it swappable in tests.
  2. The service provider / config file — check what default concurrency, timeouts, and handler options are published. Most "why is this slow/hanging" questions are answered there.

First use case — fanning out to an upstream API:

use FriendsOfOuro\HttpBatchGuzzle\ClientInterface;

$client = app(ClientInterface::class);

$responses = $client->batch([
    'users'    => fn () => $client->request('GET', '/users'),
    'orders'   => fn () => $client->request('GET', '/orders'),
    'settings' => fn () => $client->request('GET', '/settings'),
]);

$users = $responses['users']->getBody()->getContents();

Second use case (new in v3.1.0) — swap in RecordingHttpClient in a test and assert on what actually went over the wire. This is the fastest payoff from the release: you get real HTTP-behaviour confidence without mocking Guzzle's internals by hand.

Implementation Patterns

Wrap it, don't sprinkle it. Give the client a single home — e.g. an ApiGateway service that takes ClientInterface in its constructor. Every batch call goes through it. Benefits: one place to tune concurrency, one place to add retry/auth logic, and one line to swap in the recording spy in tests.

Key your batch by intent. The batch API returns results keyed by whatever you index the callables with. Use domain names ('user:42', 'invoice:99') rather than numeric indexes — assertion messages and debugging logs become vastly more readable, and you avoid mismatched ordering assumptions.

Tune concurrency per call site, not globally. Default concurrency is fine for a handful of requests; it's wrong when you're fanning out 500 IDs to a rate-limited third party. Treat concurrency as a per-batch argument in your wrapper ($gateway->fetchMany($ids, concurrency: 10)), and keep the global config as a conservative default.

Use it inside queued jobs for fan-out. A common shape: a job receives a chunk of 20–50 IDs, batches the lookups, persists results, and releases. This keeps each job short and lets the queue's retry semantics (which are better than anything you'd hand-roll around a batch) handle upstream flakiness.

Testing with RecordingHttpClient (v3.1.0). The new spy implements the same ClientInterface, so binding it is a one-liner:

$spy = new RecordingHttpClient();
$this->app->instance(ClientInterface::class, $spy);

// ... exercise the code under test ...

$spy->assertRequestCount(3);
$spy->assertRequested('GET', '/users');

The inspection APIs worth learning immediately:

  • Recorded requests/responses/exceptions — the spy captures all three, so you can assert both the happy path and the failure path from a single run.
  • Filtering by success/failure — lets you write assertAllSucceeded()-style checks or deliberately assert that exactly one call failed without hardcoding which one.
  • Batch operation tracking — individual request/response pairs inside a batch are recorded separately, so you get per-call assertions even though the production code only sees an aggregate result.
  • Chronological interaction history — invaluable when debugging ordering-sensitive logic (auth token fetched before the calls that need it, pagination following a cursor, etc.).

Decorator pattern for retries. Rather than retrying inside the batch (which muddies the semantics of "one request failed"), implement a RetryingClient implements ClientInterface that wraps the real client and re-batches only the failures recorded from the first pass. The spy's failure filtering makes this loop easy to test.

Gotchas and Tips

Batch failure semantics are the #1 source of confusion. A batch typically does not throw on individual request failures — you get a result set where some entries are exceptions. Always inspect each result rather than assuming a clean array of responses. Decide explicitly in your wrapper whether to rethrow, return a partial-result DTO, or collect errors; never let raw exceptions leak to controllers.

The spy only records traffic that flows through it. If any code path resolves a fresh Guzzle client directly (or news up a client instead of injecting ClientInterface), those calls are invisible to RecordingHttpClient and your assertions will silently under-count. Bind the spy before the service under test is resolved from the container — Laravel caches singletons, so a binding made after boot may never be seen.

Don't ship the spy into production wiring. Keep the RecordingHttpClient binding in a test-case base class or a testing-only service provider. It records full request/response payloads including headers — that's a memory leak in a long-running worker and a data-exposure risk if something ever reads it.

Watch memory on large batches. Every recorded response body is retained. When fanning out hundreds of large payloads, either stream to disk, process-and-discard per response, or chunk the work. This is true of the client generally and doubly true in tests using the spy.

Timeouts and retries are not batch-aware. Guzzle's per-request timeout applies, but a single slow endpoint can occupy a concurrency slot and stall the whole batch. Set a connect timeout and a total timeout per request, and consider lowering concurrency for known-flaky upstreams.

Chronological history ≠ keyed lookup. Use the ordered history for sequence assertions (e.g. "token refresh happened before the data calls") and the filtered/keyed APIs for set-based assertions. Mixing the two up produces brittle tests that pass only when concurrency happens to preserve order.

Extension points. The ClientInterface seam is the intended place to extend: decorators for retry, circuit-breaking, caching, logging, or header injection compose cleanly. Anything you'd be tempted to do by subclassing a concrete client is better done as a decorator — that keeps the spy working with your production stack.

Debug a mysteriously empty result set by recording with the spy in a narrow integration test rather than adding dump() to the batch loop; the recorded exceptions almost always name the real problem (DNS, TLS, 429, malformed URI) faster than tracing the aggregate result.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin