A powerful Laravel package that implements the Saga Pattern for managing long-running processes and distributed transactions with automatic compensation logic.
- 🚀 Easy to Use: Simple, intuitive API for defining and executing sagas
 - 🔄 Automatic Compensation: Built-in rollback mechanism when steps fail
 - 📊 Step-by-Step Execution: Sequential execution with progress tracking
 - 🎯 Event-Driven: Rich event system for monitoring saga lifecycle
 - đź’ľ Persistent State: Database storage for saga state and context
 - ⚡ Retry Logic: Configurable retry attempts for failed sagas
 - 📝 Comprehensive Logging: Detailed logging for debugging and monitoring
 - đź§Ş Fully Tested: Comprehensive test suite included
 
You can install the package via composer:
composer require eng-mmustafa/laravel-saga-workflowphp artisan vendor:publish --tag="saga-migrations"
php artisan migratephp artisan vendor:publish --tag="saga-config"First, create a step by extending the AbstractStep class:
<?php
namespace App\Sagas\Steps;
use EngMMustafa\LaravelSagaWorkflow\Core\AbstractStep;
class CreateOrderStep extends AbstractStep
{
    public function handle(array $context): array
    {
        // Your business logic here
        $orderId = $this->createOrder($context['customer_id'], $context['items']);
        
        $this->logInfo('Order created successfully', ['order_id' => $orderId]);
        
        return array_merge($context, ['order_id' => $orderId]);
    }
    protected function doCompensate(array $context): void
    {
        // Compensation logic - rollback the order creation
        if (isset($context['order_id'])) {
            $this->cancelOrder($context['order_id']);
            $this->logInfo('Order cancelled during compensation');
        }
    }
    private function createOrder(int $customerId, array $items): int
    {
        // Your order creation logic
        return Order::create([
            'customer_id' => $customerId,
            'items' => $items,
            'status' => 'pending'
        ])->id;
    }
    private function cancelOrder(int $orderId): void
    {
        Order::find($orderId)?->update(['status' => 'cancelled']);
    }
}Create a saga by extending the AbstractSaga class:
<?php
namespace App\Sagas;
use EngMMustafa\LaravelSagaWorkflow\Core\AbstractSaga;
use App\Sagas\Steps\CreateOrderStep;
use App\Sagas\Steps\ProcessPaymentStep;
use App\Sagas\Steps\UpdateInventoryStep;
use App\Sagas\Steps\SendConfirmationStep;
class OrderProcessingSaga extends AbstractSaga
{
    protected function defineName(): string
    {
        return 'OrderProcessing';
    }
    protected function defineSteps(): void
    {
        $this->addStep(new CreateOrderStep())
             ->addStep(new ProcessPaymentStep())
             ->addStep(new UpdateInventoryStep())
             ->addStep(new SendConfirmationStep());
    }
}Execute your saga using the SagaManager:
<?php
namespace App\Http\Controllers;
use App\Sagas\OrderProcessingSaga;
use EngMMustafa\LaravelSagaWorkflow\Core\SagaManager;
use EngMMustafa\LaravelSagaWorkflow\Exceptions\SagaExecutionException;
class OrderController extends Controller
{
    public function __construct(
        private SagaManager $sagaManager
    ) {}
    public function processOrder(Request $request)
    {
        try {
            // Create saga with initial context
            $saga = new OrderProcessingSaga(context: [
                'customer_id' => $request->customer_id,
                'items' => $request->items,
                'payment_method' => $request->payment_method
            ]);
            // Execute the saga
            $result = $this->sagaManager->execute($saga);
            return response()->json([
                'success' => true,
                'saga_id' => $saga->getId(),
                'result' => $result
            ]);
        } catch (SagaExecutionException $e) {
            return response()->json([
                'success' => false,
                'error' => $e->getMessage(),
                'saga_id' => $e->getSagaId()
            ], 422);
        }
    }
    public function getSagaStatus(string $sagaId)
    {
        $status = $this->sagaManager->getStatus($sagaId);
        
        if (!$status) {
            return response()->json(['error' => 'Saga not found'], 404);
        }
        return response()->json($status);
    }
}You can create more sophisticated steps with custom configuration:
class PaymentStep extends AbstractStep
{
    public function __construct(
        private PaymentService $paymentService,
        private string $provider = 'stripe'
    ) {
        parent::__construct('ProcessPayment');
    }
    public function handle(array $context): array
    {
        $paymentResult = $this->paymentService->charge(
            $context['amount'],
            $context['payment_method'],
            $this->provider
        );
        if (!$paymentResult->successful) {
            throw new PaymentFailedException('Payment processing failed');
        }
        return array_merge($context, [
            'payment_id' => $paymentResult->id,
            'transaction_id' => $paymentResult->transaction_id
        ]);
    }
    protected function doCompensate(array $context): void
    {
        if (isset($context['payment_id'])) {
            $this->paymentService->refund($context['payment_id']);
        }
    }
}Listen to saga events for monitoring and logging:
<?php
namespace App\Listeners;
use EngMMustafa\LaravelSagaWorkflow\Events\SagaStarted;
use EngMMustafa\LaravelSagaWorkflow\Events\SagaCompleted;
use EngMMustafa\LaravelSagaWorkflow\Events\SagaFailed;
use Illuminate\Support\Facades\Log;
class SagaEventListener
{
    public function handleSagaStarted(SagaStarted $event): void
    {
        Log::info('Saga started', [
            'saga_id' => $event->getSagaId(),
            'saga_name' => $event->getSagaName()
        ]);
    }
    public function handleSagaCompleted(SagaCompleted $event): void
    {
        Log::info('Saga completed successfully', [
            'saga_id' => $event->getSagaId(),
            'execution_time' => $event->getTotalExecutionTimeInSeconds()
        ]);
    }
    public function handleSagaFailed(SagaFailed $event): void
    {
        Log::error('Saga failed', [
            'saga_id' => $event->getSagaId(),
            'failed_step' => $event->getFailedStepName(),
            'error' => $event->getErrorMessage()
        ]);
    }
}Register the listeners in your EventServiceProvider:
protected $listen = [
    SagaStarted::class => [SagaEventListener::class . '@handleSagaStarted'],
    SagaCompleted::class => [SagaEventListener::class . '@handleSagaCompleted'],
    SagaFailed::class => [SagaEventListener::class . '@handleSagaFailed'],
];public function retrySaga(string $sagaId)
{
    try {
        // Recreate the saga instance
        $saga = new OrderProcessingSaga();
        
        // Retry the saga
        $result = $this->sagaManager->retry($sagaId, $saga);
        
        return response()->json(['success' => true, 'result' => $result]);
        
    } catch (SagaExecutionException $e) {
        return response()->json(['error' => $e->getMessage()], 422);
    }
}You can easily integrate sagas with Laravel queues:
<?php
namespace App\Jobs;
use App\Sagas\OrderProcessingSaga;
use EngMMustafa\LaravelSagaWorkflow\Core\SagaManager;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessOrderSaga implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    public function __construct(
        private array $orderData
    ) {}
    public function handle(SagaManager $sagaManager): void
    {
        $saga = new OrderProcessingSaga(context: $this->orderData);
        $sagaManager->execute($saga);
    }
}The package comes with a comprehensive configuration file. Here are the key options:
return [
    'defaults' => [
        'max_retries' => 3,
        'retryable' => true,
        'timeout' => 300,
    ],
    
    'logging' => [
        'enabled' => true,
        'channel' => 'default',
        'log_steps' => true,
        'log_compensation' => true,
    ],
    
    'events' => [
        'enabled' => true,
        'queue_listeners' => false,
    ],
    
    'cleanup' => [
        'enabled' => true,
        'completed_retention_days' => 30,
        'failed_retention_days' => 90,
    ],
];The package dispatches the following events:
SagaStarted- When saga execution beginsSagaStepCompleted- When a step completes successfullySagaStepFailed- When a step failsSagaCompleted- When saga completes successfullySagaFailed- When saga fails and compensation begins
composer testPlease see CONTRIBUTING for details.
If you discover any security related issues, please email eng.mmustafa@example.com instead of using the issue tracker.
The MIT License (MIT). Please see License File for more information.
Please see CHANGELOG for more information what has changed recently.# laravel-saga-workflow