astrotomic/laravel-guzzle
A lightweight Laravel integration for the Guzzle HTTP client. Configure Guzzle clients through Laravel's config and service container, resolve them via dependency injection, and make outbound HTTP requests with a familiar, framework-native setup.
This release (2.4.0) is a maintenance release: PHP 8.1 support was added, illuminate/support was bumped, and the rest is CI/dependency hygiene. There are no breaking changes and no new public API, so an existing 2.3.x integration keeps working — but if you are on Laravel 9 / PHP 8.1, ^2.4 is the constraint you want.
composer require astrotomic/laravel-guzzle:^2.4
php artisan vendor:publish --provider="Astrotomic\LaravelGuzzle\GuzzleServiceProvider" --tag=config
First things to look at, in order:
config/guzzle.php — the package's whole value proposition is that your Guzzle client is configured from config instead of being hand-built in every service. Read the keys before writing any HTTP code; this is the file you will actually edit day-to-day.Guzzle facade (aliased in the package's service provider) — your entry point for ad-hoc calls.GuzzleHttp\Client — this is what makes the package composable with your own classes via constructor injection.First use case: replace a hand-rolled new Client([...]) somewhere in your app with config + injection, then confirm the base URI and timeout come from the environment.
// config/guzzle.php — set from env, not hardcoded
'base_uri' => env('API_BASE_URI'),
'timeout' => env('API_TIMEOUT', 10),
'headers' => ['Accept' => 'application/json'],
use GuzzleHttp\Client;
use Astrotomic\LaravelGuzzle\Facades\Guzzle;
// via container (preferred — swappable in tests)
$client = app(Client::class);
// via facade (fine for tinker, Artisan commands, one-off scripts)
$response = Guzzle::get('users');
Constructor injection for anything testable. Because the client is a container singleton, injecting GuzzleHttp\Client is the integration point. Bind a fake or point base_uri at a local stub in your test suite rather than mocking the facade.
final class UserApi
{
public function __construct(private Client $http) {}
public function find(int $id): array
{
return json_decode(
$this->http->get("users/{$id}")->getBody()->getContents(),
true,
512,
JSON_THROW_ON_ERROR
);
}
}
Don't re-new Client() inside services. The moment a second Guzzle client is constructed by hand, you've got two timeout/header policies and the config file becomes a lie. If you genuinely need a different upstream, keep it explicit and named — bind a second client under its own key in a service provider that reads a different config array, and inject Client $githubClient with a #[Target]-style alias or a small factory.
Wrap the client, don't leak it. The most common durable pattern with this package is a thin per-API adapter class (as above) that owns URL building, decoding, and error mapping. The package gives you config; your adapter gives you a domain API, and the rest of the app never sees Guzzle types.
Pair with retry/middleware in config, not in call sites. Guzzle's HandlerStack / retry middleware belongs in the config array (or a small service provider decorating the singleton) so every call benefits. Per-call ['timeout' => 60] overrides sprinkled through controllers are the usual rot vector.
Artisan commands and health checks. The facade is convenient for php artisan code where container injection is awkward; Guzzle::request('HEAD', '/') in a health:check command is a legitimate use.
Test strategy. Two layers: (a) pure unit tests of your adapter with a mocked Client; (b) one or two integration tests against Http::fake()-style stubs or a local server where you assert the config actually reaches the client (base URI, auth header, timeout). The config plumbing is the package's risk surface, so test it once explicitly.
illuminate/support. If you're on PHP 8.0 or older, 2.3.x is still fine — don't rush; if you're on PHP 8.1, 2.4 is the first release that officially supports you.base_uri vs relative paths. Guzzle's base_uri resolution is easy to get wrong: a base URI without a trailing slash plus a leading-slash path discards any path component of the base. If you set https://api.example.com/v1, call users (no leading slash) or make the base URI end in /.config() changes at runtime (e.g. Config::set() in a test) won't affect an already-resolved client. Reset the container binding in tests or rebuild the app.timeout is not connect_timeout. The former doesn't cover DNS/TCP setup. For flaky networks set both; otherwise you'll see hangs well past your configured timeout.'headers' => [] and no headers key can behave differently once middleware is involved. Be explicit in config rather than relying on merge semantics.RequestException/ConnectException by default. If you don't catch them, an upstream 500 becomes a 500 in your own app with a confusing stack trace. Decide once — http_errors => false and check status, or catch and map to a domain exception.'debug' => true in config dumps request/response to stdout — invaluable locally, catastrophic in production logs (auth headers included). Gate it on env('APP_DEBUG') and consider Guzzle's Middleware::history() for a structured record instead.$this->app->extend(Client::class, ...)) to add logging, tracing, or a retry stack without forking the package or editing vendor config.composer normalize / CI churn in this release is not a signal. The bulk of 2.4.0's diff is Dependabot and GitHub Actions bumps. The package is small and stable; treat it as configuration glue, not as something that will grow features, and don't expect new methods between minor versions.How can I help you explore Laravel packages today?