eng-mmustafa/laravel-saga-workflow

A powerful Laravel package implementing the Saga Pattern for managing long-running distributed transactions with automatic compensation logic. Build robust, fault-tolerant applications with step-by-step transaction management and automatic rollback capabilities.

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/eng-mmustafa/laravel-saga-workflow

dev-master 2025-10-26 17:31 UTC

This package is auto-updated.

Last update: 2025-10-26 17:31:34 UTC


README

Latest Version on Packagist Total Downloads License

A powerful Laravel package that implements the Saga Pattern for managing long-running processes and distributed transactions with automatic compensation logic.

Features

  • ๐Ÿš€ 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

Installation

You can install the package via composer:

composer require eng-mmustafa/laravel-saga-workflow

Publish and run migrations

php artisan vendor:publish --tag="saga-migrations"
php artisan migrate

Publish configuration file (optional)

php artisan vendor:publish --tag="saga-config"

Quick Start

1. Create a Step

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']);
    }
}

2. Create a Saga

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());
    }
}

3. Execute the Saga

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);
    }
}

Advanced Usage

Custom Step Configuration

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']);
        }
    }
}

Event Listeners

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'],
];

Retry Failed Sagas

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);
    }
}

Queue Integration

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);
    }
}

Configuration

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,
    ],
];

Available Events

The package dispatches the following events:

  • SagaStarted - When saga execution begins
  • SagaStepCompleted - When a step completes successfully
  • SagaStepFailed - When a step fails
  • SagaCompleted - When saga completes successfully
  • SagaFailed - When saga fails and compensation begins

Testing

composer test

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security related issues, please email eng.mmustafa@example.com instead of using the issue tracker.

Credits

License

The MIT License (MIT). Please see License File for more information.

Changelog

Please see CHANGELOG for more information what has changed recently.# laravel-saga-workflow