brzuchal/saga
Laravel package implementing the Saga pattern for coordinating long-running, distributed workflows. Helps model multi-step processes with compensating actions, track saga state, and handle failures/retries so complex business transactions stay consistent across services.
Installation
composer require brzuchal/saga
Add the service provider to config/app.php:
'providers' => [
// ...
Brzuchal\Saga\SagaServiceProvider::class,
],
Basic Configuration Publish the config file:
php artisan vendor:publish --provider="Brzuchal\Saga\SagaServiceProvider" --tag="saga-config"
Update config/saga.php with your preferred storage (e.g., database, redis).
First Use Case: Simple Saga Define a saga class:
namespace App\Sagas;
use Brzuchal\Saga\Saga;
class OrderProcessingSaga extends Saga
{
public function execute()
{
$this->validateOrder();
$this->processPayment();
$this->fulfillOrder();
}
protected function validateOrder()
{
// Logic
}
protected function processPayment()
{
// Logic
}
protected function fulfillOrder()
{
// Logic
}
}
Trigger the Saga
use App\Sagas\OrderProcessingSaga;
$saga = new OrderProcessingSaga(['order_id' => 123]);
$saga->run();
Event-Driven Triggers Use Laravel events to kick off sagas:
// In an event listener
event(new OrderCreated($order));
// In a listener
public function handle(OrderCreated $event)
{
$saga = new OrderProcessingSaga(['order_id' => $event->order->id]);
$saga->run();
}
Command-Based Execution Create an Artisan command for manual triggering:
namespace App\Console\Commands;
use App\Sagas\OrderProcessingSaga;
use Illuminate\Console\Command;
class ProcessOrderSaga extends Command
{
protected $signature = 'saga:process-order {order_id}';
public function handle()
{
$saga = new OrderProcessingSaga(['order_id' => $this->argument('order_id')]);
$saga->run();
}
}
Compensation Handling Implement rollback logic in saga steps:
protected function processPayment()
{
try {
// Payment logic
} catch (\Exception $e) {
$this->compensate('refundPayment');
throw $e;
}
}
protected function compensateRefundPayment()
{
// Refund logic
}
Track Saga State Use the built-in state machine:
$this->setState('validated'); // Manually set state
$this->transitionTo('processing'); // Transition to next state
Conditional Logic Use state checks to control flow:
if ($this->getState() === 'paid') {
$this->fulfillOrder();
}
Database Storage
Configure config/saga.php:
'storage' => [
'driver' => 'database',
'table' => 'sagas',
],
Migrate the table:
php artisan vendor:publish --provider="Brzuchal\Saga\SagaServiceProvider" --tag="migrations"
php artisan migrate
Redis Storage For high-performance environments:
'storage' => [
'driver' => 'redis',
'connection' => 'cache',
],
Transaction Boundaries
DB::transaction(function () {
$this->processPayment();
});
State Inconsistency
transitionTo() over setState() to enforce valid state transitions.Error Handling
try {
$this->processPayment();
} catch (\Exception $e) {
Log::error("Payment failed for saga {$this->getId()}", ['error' => $e->getMessage()]);
$this->compensate('refundPayment');
throw $e;
}
Idempotency
if ($this->getState() === 'completed') {
return;
}
Log Saga Execution
Enable logging in config/saga.php:
'logging' => true,
Check logs at storage/logs/laravel.log.
Inspect Saga State Query the storage table directly:
SELECT * FROM sagas WHERE saga_id = 'your-saga-id';
Test Locally
Use the saga:replay command to debug failed sagas:
php artisan saga:replay your-saga-id
Custom Storage
Implement a custom storage driver by extending Brzuchal\Saga\Contracts\SagaStorage.
Event Dispatching Extend the saga to dispatch events at key steps:
protected function processPayment()
{
event(new PaymentProcessed($this->getData()));
// ...
}
Middleware Add middleware to sagas for cross-cutting concerns (e.g., auth, logging):
use Brzuchal\Saga\Contracts\SagaMiddleware;
class LoggingMiddleware implements SagaMiddleware
{
public function handle($saga, $next)
{
Log::info("Saga {$saga->getId()} started");
$next($saga);
Log::info("Saga {$saga->getId()} completed");
}
}
Register in config/saga.php:
'middleware' => [
\App\Middleware\LoggingMiddleware::class,
],
Retry Logic Implement exponential backoff for transient failures:
protected function processPayment()
{
$attempts = 0;
while ($attempts < 3) {
try {
// Payment logic
return;
} catch (\Exception $e) {
$attempts++;
if ($attempts === 3) throw $e;
sleep(2 ** $attempts); // Exponential backoff
}
}
}
How can I help you explore Laravel packages today?