sfynx-project/tool-circuitbreaker-bundle
Symfony bundle implementing the circuit breaker pattern to protect PHP applications from cascading failures. It monitors unreliable service calls, opens the circuit after repeated errors to fail fast, and supports controlled recovery when the service is healthy again.
Read this first: despite the description, this is a Symfony bundle, not a Laravel package. There is no CircuitBreakerServiceProvider, no facade, and no config/circuitbreaker.php. It targets Symfony 2/3-era DI and was last released in January 2017. Before writing any code, decide which of these two paths you're on:
app/Support/CircuitBreaker) and wire them yourself. This is the realistic path for Laravel.If you do proceed with adaptation:
composer require sfynx-project/tool-circuitbreaker-bundle
Then check composer.json and the DependencyInjection/ directory in vendor/sfynx-project/tool-circuitbreaker-bundle to see the actual handler class names and constructor signatures — the README is thin, so the DI extension is your real documentation. Note the bundle will try to register itself via Symfony's Bundle class; you will not use that class at all in Laravel.
First use case: wrap a single flaky outbound dependency — a payment gateway, a shipping-rate API, an SMS provider — behind a breaker, with a cached/fallback response when the breaker is open.
// app/Providers/CircuitBreakerServiceProvider.php
public function register(): void
{
$this->app->singleton(PaymentGatewayBreaker::class, function ($app) {
return new PaymentGatewayBreaker(
store: $app->make(BreakerStore::class), // Redis-backed
threshold: 5,
resetTimeout: 60, // seconds
);
});
}
Then in a job or service:
if (! $breaker->isAvailable()) {
return $this->cachedRates(); // fallback path
}
try {
$result = $client->rates($payload);
$breaker->success();
return $result;
} catch (ConnectionException $e) {
$breaker->failure();
throw $e;
}
Start with one dependency and one breaker. Resist wrapping every HTTP call on day one.
Wrap at the boundary, not in controllers. Put the breaker inside a dedicated client/gateway class (App\Services\Payments\PaymentGatewayClient) so controllers and jobs never see breaker state. This makes it trivial to swap in a fake in tests.
Guzzle / Laravel HTTP middleware. The cleanest day-to-day integration is a middleware that calls success() on 2xx–4xx and failure() on 5xx/timeouts:
Http::withMiddleware(function (callable $handler) use ($breaker) {
return function ($request, $options) use ($handler, $breaker) {
if (! $breaker->isAvailable()) {
throw new BreakerOpenException();
}
return $handler($request, $options)->then(
function ($response) use ($breaker) { $breaker->success(); return $response; },
function ($reason) use ($breaker) { $breaker->failure(); return rejection($reason); }
);
};
})->get('https://rates.example.com/...');
Key your state per dependency, and store it centrally. PHP-FPM gives you a fresh process per request, so in-memory counters are useless. Use Redis or the database with a key like breaker:{service}:{environment}. Do not reuse one breaker across unrelated services — a shipping outage should not open the breaker for payments.
Combine with retries and timeouts, in this order: timeout → retry (bounded, with jitter) → breaker → fallback. A breaker whose wrapped call can hang for 30s is worse than no breaker.
Expose state for ops. Add a /health/dependencies endpoint returning each breaker's state and counters, and a console command:
// php artisan breaker:reset payments
Fallbacks should be explicit types. Return a CachedRates value object or a RatesUnavailable result rather than returning null — otherwise every caller needs null checks and the open state leaks everywhere.
Test by forcing state. Give your breaker class forceOpen() / forceClosed() methods (or inject a fake that implements the same interface) so feature tests don't depend on real failure counts.
each()/create_function-era assumptions. Run composer why-not php 8.2 and expect friction. Treat any code you reuse as a fork you now own.NOASSERTION. That is not an open-source license grant you can rely on. Get legal sign-off before shipping it in a commercial product; the pattern itself is public knowledge and carries no such risk.composer require it into a Laravel app, nothing auto-registers it (good), but if you're in a mixed/legacy setup, the bundle's DI extension may attempt container compilation you don't want.warning with the service name and the triggering exception. Without this, an open breaker looks identical to a logic bug in production.HttpClient-based breaker, a Guzzle middleware like webignition/http-retry paired with a small stateful breaker, or a hand-rolled class in app/Support. You can implement the whole pattern in ~120 lines plus Redis, and you keep the upgrade path.How can I help you explore Laravel packages today?