ejsmont-artur/php-circuit-breaker
Laravel-friendly PHP circuit breaker implementation to add resiliency to external service calls. Supports configurable failure thresholds, timeouts and recovery, helping prevent cascading failures when APIs or dependencies are down or slow.
Installation
composer require ejsmont-artur/php-circuit-breaker
Add to composer.json if using a custom namespace:
"autoload": {
"psr-4": {
"App\\": "app/",
"CircuitBreaker\\": "vendor/ejsmont-artur/php-circuit-breaker/src/"
}
}
Run composer dump-autoload.
Basic Usage
use CircuitBreaker\CircuitBreaker;
use CircuitBreaker\CircuitBreakerFactory;
$factory = new CircuitBreakerFactory();
$breaker = $factory->createCircuitBreaker(
'myService',
3, // maxFailures
1000, // resetTimeoutMs
1000 // failureThresholdDurationMs
);
$result = $breaker->execute(function() {
return $this->callExternalService();
});
First Use Case: Wrapping External API Calls
$breaker = $factory->createCircuitBreaker('apiService', 3, 5000, 2000);
$response = $breaker->execute(function() {
return Http::get('https://external-api.com/data');
});
Dependency Injection (Laravel)
// In a service provider
$this->app->singleton('circuitBreaker', function ($app) {
$factory = new CircuitBreakerFactory();
return $factory->createCircuitBreaker(
'database',
5,
10000,
3000
);
});
// In a controller/service
$breaker = app('circuitBreaker');
Dynamic Circuit Breaker Configuration
Use a config file (config/circuit-breakers.php) to define breakers:
return [
'services' => [
'api' => [
'max_failures' => 3,
'reset_timeout_ms' => 10000,
'failure_threshold_duration_ms' => 5000,
],
'database' => [
'max_failures' => 5,
'reset_timeout_ms' => 15000,
'failure_threshold_duration_ms' => 8000,
],
],
];
Load dynamically:
$config = config('circuit-breakers.services');
$factory = new CircuitBreakerFactory();
$breaker = $factory->createCircuitBreaker(
'api',
$config['api']['max_failures'],
$config['api']['reset_timeout_ms'],
$config['api']['failure_threshold_duration_ms']
);
Logging Failures
Extend the CircuitBreaker class to log failures:
use CircuitBreaker\CircuitBreaker;
use Psr\Log\LoggerInterface;
class LoggingCircuitBreaker extends CircuitBreaker
{
protected $logger;
public function __construct(
string $name,
int $maxFailures,
int $resetTimeoutMs,
int $failureThresholdDurationMs,
LoggerInterface $logger
) {
parent::__construct($name, $maxFailures, $resetTimeoutMs, $failureThresholdDurationMs);
$this->logger = $logger;
}
protected function onFailure()
{
$this->logger->warning("Circuit breaker '{$this->name}' failed");
}
}
Retry Logic with Exponential Backoff
Combine with a retry library (e.g., php-retry) for sophisticated retries:
use Retry\Retry;
$retry = Retry::withMaxAttempts(3)
->withDelay(100)
->withMultiplier(2)
->attempt(function () use ($breaker) {
return $breaker->execute(function () {
return $this->callExternalService();
});
});
Middleware for API Routes Create middleware to wrap API calls:
namespace App\Http\Middleware;
use Closure;
use CircuitBreaker\CircuitBreakerFactory;
class CircuitBreakerMiddleware
{
protected $factory;
public function __construct(CircuitBreakerFactory $factory)
{
$this->factory = $factory;
}
public function handle($request, Closure $next)
{
$breaker = $this->factory->createCircuitBreaker(
'api',
config('circuit-breakers.api.max_failures'),
config('circuit-breakers.api.reset_timeout_ms'),
config('circuit-breakers.api.failure_threshold_duration_ms')
);
return $breaker->execute(function () use ($next) {
return $next($request);
});
}
}
Register in app/Http/Kernel.php:
protected $middleware = [
\App\Http\Middleware\CircuitBreakerMiddleware::class,
];
Queue Workers Use circuit breakers in queue jobs to prevent cascading failures:
use CircuitBreaker\CircuitBreakerFactory;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
class ProcessPayment implements ShouldQueue
{
use Queueable, SerializesModels, InteractsWithQueue;
protected $breaker;
public function __construct(CircuitBreakerFactory $factory)
{
$this->breaker = $factory->createCircuitBreaker(
'paymentService',
3,
10000,
5000
);
}
public function handle()
{
$this->breaker->execute(function () {
// Process payment logic
});
}
}
Testing Mock the circuit breaker in tests:
$mockBreaker = Mockery::mock('CircuitBreaker\CircuitBreaker');
$mockBreaker->shouldReceive('execute')
->once()
->andReturn('mocked_result');
$this->app->instance('circuitBreaker', $mockBreaker);
State Management
$factory = new CircuitBreakerFactory();
$breaker = $factory->createCircuitBreakerWithStorage(
'redis',
'myService',
3,
10000,
5000,
new Redis()
);
Timeout Handling
resetTimeoutMs is not a delay before tripping. It’s the time the breaker stays open after tripping.failureThresholdDurationMs to control how quickly the breaker trips after failures.Exception Handling
$breaker->execute(function () {
try {
return $this->callExternalService();
} catch (\Exception $e) {
// Ensure exceptions bubble up
throw $e;
}
});
Thread Safety
Configuration Overrides
Check Breaker State Add a helper method to inspect the breaker:
public function getStatus(): string
{
return $this->isOpen() ? 'OPEN' : 'CLOSED';
}
Log status during debugging:
\Log::debug("Circuit breaker status: {$breaker->getStatus()}");
Simulate Failures Force a failure to test the breaker:
$breaker->forceFailure(); // Simulate a failure
$result = $breaker->execute(function () {
return "This will fail";
});
// $result should be null (or a fallback value)
Monitor Reset Time
Use isOpen() and getTimeToReset() to debug timing:
if ($breaker->isOpen()) {
$resetTime = $breaker->getTimeToReset();
\Log::info("Breaker will reset in {$resetTime}ms");
}
How can I help you explore Laravel packages today?