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
Requires
- php: ^8.1|^8.2|^8.3
- illuminate/console: ^10.0|^11.0|^12.0
- illuminate/database: ^10.0|^11.0|^12.0
- illuminate/events: ^10.0|^11.0|^12.0
- illuminate/queue: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
- ramsey/uuid: ^4.2
Requires (Dev)
- laravel/pint: ^1.0
- mockery/mockery: ^1.5
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.0|^11.0
This package is auto-updated.
Last update: 2025-10-26 17:31:34 UTC
README
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