dimita / laravel-business-orchestration-engine
A Laravel library for business orchestration including Saga Pattern, Workflow, Event Sourcing, Versioning, Rule Engine, and more.
Package info
github.com/DIMITA/laravel-business-orchestration-engine
pkg:composer/dimita/laravel-business-orchestration-engine
Requires
- php: ^8.1
- laravel/framework: ^10.0|^11.0
Requires (Dev)
- orchestra/testbench: ^8.0
- phpunit/phpunit: ^10.0
README
A comprehensive Laravel package for business orchestration, including Saga Pattern, Workflow Management, Event Sourcing, Versioning, Rule Engine, and Dependency Management.
Note: This package is based on battle-tested code patterns I've been using in production for years. I've packaged it to make these proven patterns easily reusable across projects.
Requirements
- PHP: ^8.1 or higher
- Laravel: ^10.0 or ^11.0
- Database: MySQL 5.7+, PostgreSQL 10+, or SQLite 3.8+
- Extensions:
ext-json- JSON support for payload serializationext-pdo- Database connectivity
Optional Requirements
For enhanced saga orchestration capabilities:
- Queue Driver: Redis, Database, or SQS for asynchronous step execution
- Cache Driver: Redis or Memcached for performance optimization
- Message Queue (optional): RabbitMQ for distributed saga coordination
Table of Contents
- Installation
- Features
- Independent vs Combined Usage
- Usage Guide
- Real-World Use Cases
- Architecture
- Testing
Installation
Install the package via Composer:
composer require dimita/laravel-business-orchestration-engine
Publish the configuration and migrations:
php artisan vendor:publish --provider="Dimita\\BusinessOrchestration\\BusinessOrchestrationServiceProvider"
php artisan migrate
Features
✨ 7 Powerful Engines
| Engine | Description | Use Case |
|---|---|---|
| Saga Pattern | Distributed transactions with automatic compensation | Complex business processes (orders, payments) |
| Workflow Engine | State machine with guards and transitions | Document validation, approval processes |
| Event Sourcing | Append-only event store | Audit trail, historical state reconstruction |
| Versioning | Immutable model snapshots | Change history, rollback capability |
| Rule Engine | Business rule evaluation via AST | Dynamic discounts, business validation |
| Sync Engine | Multi-device synchronization | Offline-first apps, mobile sync |
| Dependency Engine | Business constraint management | Pre-delete validation, dependency graphs |
Independent vs Combined Usage
This package offers maximum flexibility - use engines independently or together based on your needs.
Independent Usage
Perfect when you only need specific functionality:
use Dimita\BusinessOrchestration\Core\VersionEngine; use Dimita\BusinessOrchestration\Core\RuleEngine; use Dimita\BusinessOrchestration\Core\SyncEngine; // Method 1: Direct class resolution $versionEngine = app(VersionEngine::class); $versionEngine->snapshot($model); // Method 2: Using aliases $ruleEngine = app('rule-engine'); $rule = $ruleEngine->rule('MyRule') ->when('amount', '>', 1000) ->then(fn($ctx) => ['discount' => 10]); // Method 3: Dependency injection (recommended) class OrderController { public function __construct( private VersionEngine $version, private RuleEngine $rules ) {} public function process(Order $order) { $this->version->snapshot($order); $discountRule = $this->rules->getMatchingRules(['amount' => $order->total]); } }
Combined Usage
Use the main facade when working with multiple engines:
$orchestration = app('business-orchestration'); // Access any engine $saga = $orchestration->saga(); $workflow = $orchestration->workflow(); $version = $orchestration->version(); $rules = $orchestration->rule(); $sync = $orchestration->sync(); $eventSourcing = $orchestration->eventSourcing(); $dependency = $orchestration->dependency();
Available Aliases
// All engines are available via these aliases: app('saga-engine') // SagaEngine app('workflow-engine') // WorkflowEngine app('sync-engine') // SyncEngine app('version-engine') // VersionEngine app('event-sourcing-engine') // EventSourcingEngine app('rule-engine') // RuleEngine app('dependency-engine') // DependencyEngine
When to Use Each Approach
| Approach | Best For |
|---|---|
| Independent (Class) | Type safety, IDE autocompletion, single-engine projects |
| Independent (Alias) | Quick prototyping, flexibility, simpler syntax |
| Dependency Injection | Production code, testability, SOLID principles |
| Combined Facade | Complex workflows using multiple engines |
Configuration for Selective Loading
Improve performance by loading only the engines you need. Edit config/business-orchestration.php:
'engines' => [ 'saga' => true, // Enable Saga Pattern 'workflow' => true, // Enable Workflow Engine 'sync' => false, // Disable Sync Engine (not needed) 'version' => true, // Enable Versioning 'event_sourcing' => false, // Disable Event Sourcing (not needed) 'rule' => true, // Enable Rule Engine 'dependency' => false, // Disable Dependency Engine (not needed) ],
Or use environment variables in your .env:
# Enable only the engines you need ORCHESTRATION_SAGA_ENABLED=true ORCHESTRATION_WORKFLOW_ENABLED=true ORCHESTRATION_SYNC_ENABLED=false ORCHESTRATION_VERSION_ENABLED=true ORCHESTRATION_EVENT_SOURCING_ENABLED=false ORCHESTRATION_RULE_ENABLED=true ORCHESTRATION_DEPENDENCY_ENABLED=false
Benefits of selective loading:
- Reduced memory footprint
- Faster application bootstrap
- Cleaner service container
- Only load what you actually use
Note: If you try to use a disabled engine, you'll get a clear error message:
RuntimeException: SyncEngine is not enabled. Enable it in config/business-orchestration.php
Usage Guide
1. Saga Pattern
When to use? When you have a business transaction involving multiple services and need to rollback (compensate) on failure.
Simple Example - Order Processing
use Dimita\BusinessOrchestration\BusinessOrchestration; // Define your saga steps class ValidateOrderStep { public function execute($payload) { $order = Order::find($payload['order_id']); if (!$order->isValid()) { throw new \Exception('Invalid order'); } return true; } } class ChargePaymentStep { public function execute($payload) { $order = Order::find($payload['order_id']); // Charge payment $payment = PaymentGateway::charge($order->total); if (!$payment->success) { throw new \Exception('Payment failed'); } return true; } } class ShipOrderStep { public function execute($payload) { $order = Order::find($payload['order_id']); // Ship the order ShippingService::ship($order); return true; } } // Start the saga synchronously $saga = BusinessOrchestration::saga()->startSaga('OrderProcessing', [ 'validate' => ValidateOrderStep::class, 'charge' => ChargePaymentStep::class, 'ship' => ShipOrderStep::class, ], ['order_id' => 123]); // If a step fails, completed steps will be automatically compensated
Advanced Compensation
Define compensation logic for each step to properly rollback changes:
class ChargePaymentStep { public function execute($payload) { $order = Order::find($payload['order_id']); $payment = PaymentGateway::charge($order->total); if (!$payment->success) { throw new \Exception('Payment failed'); } return true; } // Define compensation logic public function compensate($payload) { $order = Order::find($payload['order_id']); // Refund the payment PaymentGateway::refund($order->total); // Update order status $order->update(['status' => 'payment_refunded']); } }
Asynchronous Execution
Execute sagas asynchronously using Laravel queues:
// Start saga asynchronously (returns immediately) $saga = BusinessOrchestration::saga()->startSagaAsync('OrderProcessing', [ 'validate' => ValidateOrderStep::class, 'charge' => ChargePaymentStep::class, 'ship' => ShipOrderStep::class, ], ['order_id' => 123]); // Check saga status later $status = BusinessOrchestration::saga()->getSagaStatus($saga->id); echo $status['status']; // PENDING, RUNNING, COMPLETED, COMPENSATED echo $status['completed_steps'] . '/' . $status['total_steps'];
Resume After Crash
// If your server crashes during execution, resume the saga $sagaEngine = BusinessOrchestration::saga(); $sagaEngine->resumeSaga($sagaId);
Cancel Running Saga
// Cancel a saga that's pending or running $sagaEngine = BusinessOrchestration::saga(); $sagaEngine->cancelSaga($sagaId);
Saga Status Flow
PENDING- Saga created, not started yetRUNNING- Saga currently executingCOMPLETED- All steps completed successfullyFAILED- A step failed (before compensation)COMPENSATED- Completed steps have been rolled back after failureCANCELLED- Saga was manually cancelledCOMPENSATION_FAILED- Compensation encountered an error (step-level)
2. Workflow Engine
When to use? When you need a state machine to manage transitions between different business states.
Simple Example - Contract Validation
use Dimita\BusinessOrchestration\BusinessOrchestration; $workflow = BusinessOrchestration::workflow(); // Define possible transitions $workflow->defineTransition('submit', 'draft', 'submitted'); $workflow->defineTransition('review', 'submitted', 'in_review'); $workflow->defineTransition('approve', 'in_review', 'approved'); $workflow->defineTransition('reject', 'in_review', 'rejected'); $workflow->defineTransition('revise', 'rejected', 'draft'); // Use workflow on a model $contract = Contract::find(1); $builder = $workflow->for($contract); // Check if transition is possible if ($builder->can('approve')) { $builder->apply('approve'); } // Get current state echo $builder->getState(); // 'approved'
With Guards (Conditions)
// Define transition with condition $workflow->defineTransition( 'auto_approve', 'submitted', 'approved', 'return $context["amount"] < 1000;' // Guard expression ); // Transition only possible if amount < 1000
Complex Workflow - Ticket Management
// Draft -> Open -> In Progress -> (Resolved | Closed) // ↓ // On Hold $workflow->defineTransition('open', 'draft', 'open'); $workflow->defineTransition('start', 'open', 'in_progress'); $workflow->defineTransition('hold', 'in_progress', 'on_hold'); $workflow->defineTransition('resume', 'on_hold', 'in_progress'); $workflow->defineTransition('resolve', 'in_progress', 'resolved'); $workflow->defineTransition('close', 'resolved', 'closed'); $ticket = Ticket::find(1); $builder = $workflow->for($ticket); // Apply transitions through lifecycle $builder->apply('open'); $builder->apply('start'); $builder->apply('hold'); $builder->apply('resume'); $builder->apply('resolve'); $builder->apply('close');
Advanced Features
Register Workflow Definition
// Define a complete workflow with configuration $workflow->registerWorkflow('order_approval', [ 'transitions' => [ ['name' => 'submit', 'from' => 'draft', 'to' => 'pending'], ['name' => 'approve', 'from' => 'pending', 'to' => 'approved'], ['name' => 'reject', 'from' => 'pending', 'to' => 'rejected'], ] ]);
Event Hooks
// Execute custom logic before transitions $workflow->beforeTransition('approve', function($instance) { Log::info("Approving workflow for {$instance->model_type}"); // Send notification, update related records, etc. }); // Execute custom logic after transitions $workflow->afterTransition('approve', function($instance) { Mail::to($user)->send(new ApprovalConfirmation()); });
Get Available Transitions
$builder = $workflow->for($document); // Get all transitions available from current state $availableTransitions = $builder->getEnabledTransitions(['amount' => 500]); // Returns: ['approve', 'reject', 'request_changes']
Check Workflow State
// Check if model is in specific state if ($workflow->isInState($order, 'approved')) { // Process approved order } // Get all possible states in the workflow $allStates = $workflow->getAllStates(); // Returns: ['draft', 'pending', 'approved', 'rejected']
Force State Change
// Override guards and force a state change (use carefully) $builder->forceTransition('cancelled', 'Manual cancellation by admin');
3. Event Sourcing
When to use? When you need to keep a complete history of all changes and be able to rebuild state at any point in time.
Simple Example - Shopping Cart
use Dimita\BusinessOrchestration\BusinessOrchestration; $es = BusinessOrchestration::eventSourcing(); // Store events $es->storeEvent('cart-123', 'CartCreated', [ 'user_id' => 456, 'created_at' => now() ]); $es->storeEvent('cart-123', 'ItemAdded', [ 'product_id' => 789, 'quantity' => 2, 'price' => 29.99 ]); $es->storeEvent('cart-123', 'ItemAdded', [ 'product_id' => 101, 'quantity' => 1, 'price' => 49.99 ]); $es->storeEvent('cart-123', 'CartCheckedOut', [ 'total' => 109.97, 'payment_method' => 'credit_card' ]); // Rebuild cart state $cart = $es->rebuildAggregate('cart-123', function($state, $event) { switch ($event['event_type']) { case 'CartCreated': return [ 'user_id' => $event['payload']['user_id'], 'items' => [], 'total' => 0, 'status' => 'active' ]; case 'ItemAdded': $state['items'][] = $event['payload']; $state['total'] += $event['payload']['price'] * $event['payload']['quantity']; return $state; case 'CartCheckedOut': $state['status'] => 'checked_out'; return $state; default: return $state; } }); print_r($cart); // Array ( // 'user_id' => 456, // 'items' => [...], // 'total' => 109.97, // 'status' => 'checked_out' // )
Retrieve All Events
$events = $es->getEvents('cart-123'); foreach ($events as $event) { echo "{$event['event_type']} at version {$event['version']}\n"; }
Advanced Features
Projectors - Create Read Models
Projectors listen to events and create read models (projections) for querying:
class OrderTotalProjector { // Called when MoneyAdded event is stored public function onMoneyAdded($event) { $account = Account::findOrFail($event->aggregate_id); $account->increment('balance', $event->payload['amount']); } // Called when MoneySubtracted event is stored public function onMoneySubtracted($event) { $account = Account::findOrFail($event->aggregate_id); $account->decrement('balance', $event->payload['amount']); } } // Register the projector $es->addProjector(OrderTotalProjector::class); // Now when you store events, projector will automatically update read models $es->storeEvent('account-123', 'MoneyAdded', ['amount' => 100]);
Reactors - Handle Side Effects
Reactors respond to events with side effects (emails, notifications, etc.):
class SendEmailReactor { public function onOrderPlaced($event) { // Send confirmation email Mail::to($event->payload['email'])->send(new OrderConfirmation($event)); } } // Register the reactor $es->addReactor(SendEmailReactor::class); // Reactor will handle side effects asynchronously $es->storeEvent('order-456', 'OrderPlaced', ['email' => 'customer@example.com']);
Event Replay
Rebuild projections by replaying all events:
// Replay all events through projectors $count = $es->replay(); echo "Replayed {$count} events"; // Replay only specific aggregate $count = $es->replay('account-123'); // Replay through specific projectors only $count = $es->replay(null, [OrderTotalProjector::class]);
Snapshots for Performance
Create snapshots to avoid replaying thousands of events:
// Create a snapshot of current state $cart = $es->rebuildAggregate('cart-123', $reducer); $es->snapshot('cart-123', $cart); // Retrieve latest snapshot instead of rebuilding from all events $cart = $es->getLatestSnapshot('cart-123'); if (!$cart) { // No snapshot exists, rebuild from events $cart = $es->rebuildAggregate('cart-123', $reducer); }
Metadata and Event Queries
// Store event with metadata $es->storeEvent('order-789', 'OrderShipped', ['tracking_number' => 'ABC123'], ['user_id' => auth()->id(), 'ip_address' => request()->ip()] ); // Get events by type $shippedOrders = $es->getEventsByType('OrderShipped', 10); // Get latest version number $latestVersion = $es->getLatestVersion('order-789'); // Get events from specific version $newEvents = $es->getEvents('order-789', $fromVersion = 5);
4. Versioning
When to use? When you need to keep snapshots of your models to rollback or view history.
Simple Example - Document Versioning
use Dimita\BusinessOrchestration\BusinessOrchestration; $version = BusinessOrchestration::version(); $document = Document::find(1); // Create snapshot before modification $version->snapshot($document); // Modify document $document->content = 'New content'; $document->save(); // Create another snapshot $version->snapshot($document); // Modify again $document->content = 'Even newer content'; $document->save(); // Create third snapshot $version->snapshot($document); // View all versions $versions = $version->getVersions($document); // 3 versions available // Restore to version 2 $version->restore($document, 2); echo $document->content; // 'New content'
Use Case - Contract Audit Trail
$contract = Contract::find(1); // Create snapshot at each important change $contract->status = 'draft'; $contract->save(); $version->snapshot($contract); $contract->status = 'submitted'; $contract->save(); $version->snapshot($contract); $contract->status = 'approved'; $contract->amount = 50000; $contract->save(); $version->snapshot($contract); // View complete history $versions = $version->getVersions($contract); foreach ($versions as $v) { echo "Version {$v['version']}: Status = {$v['snapshot']['status']}\n"; }
Advanced Versioning Features
Version Diffing
Compare differences between two versions:
// Create two versions $document->content = 'First version'; $document->save(); $version->snapshot($document); $document->content = 'Second version'; $document->price = 100; $document->save(); $version->snapshot($document); // Get differences between versions 1 and 2 $diff = $version->diff($document, 1, 2); /* Result: [ 'added' => ['price' => 100], 'removed' => [], 'changed' => [ 'content' => [ 'old' => 'First version', 'new' => 'Second version' ] ] ] */
Field Exclusion
Exclude sensitive or unnecessary fields from versioning:
// Exclude timestamps and sensitive data $version->excludeFields(['password', 'remember_token', 'last_login_at']) ->snapshot($user); // Only critical fields are versioned
Include Hidden Fields
Include normally hidden fields in version snapshots:
$user->makeHidden(['password']); // Hidden by default // Include hidden fields in version $version->includeHiddenFields(['password', 'api_token']) ->snapshot($user);
Revert to Previous Version
Quick revert to N versions back:
// Revert 1 version back $version->revert($document); // Revert 3 versions back $version->revert($document, 3); // Latest version is now the restored one
Version Metadata
Store contextual information with versions:
$version->snapshot($document, [ 'user_id' => auth()->id(), 'reason' => 'Legal compliance update', 'ip_address' => request()->ip() ]);
Version Utilities
// Get latest version number $latestVersion = $version->getLatestVersion($document); // e.g., 15 // Check if specific version exists if ($version->hasVersion($document, 5)) { // Version 5 exists } // Get total version count $count = $version->getVersionCount($document); // e.g., 15 // Get version by hash $versionModel = $version->getVersionByHash($document, $hash); // Delete all versions $deletedCount = $version->purge($document);
5. Rule Engine
When to use? When you have business rules that change frequently and you want to manage them without modifying code.
Simple Example - Discount Rules
use Dimita\BusinessOrchestration\BusinessOrchestration; $ruleEngine = BusinessOrchestration::rule(); // Create rule: If amount > 500, apply discount $discountRule = $ruleEngine->createRule('BigOrderDiscount', [ 'type' => 'comparison', 'left' => 'amount', 'op' => '>', 'right' => 500 ], ['type' => 'discount', 'value' => 10]); // Evaluate rule $order = ['amount' => 750, 'customer_id' => 123]; if ($ruleEngine->evaluate($discountRule, $order)) { echo "Discount applicable!"; // Apply discount }
Rules with Custom Actions
// Create rule with callable action $rule = $ruleEngine->createRule('VIPCustomerRule', [ 'type' => 'comparison', 'left' => 'customer_tier', 'op' => '==', 'right' => 'VIP' ], ['type' => 'action', 'name' => 'apply_vip_benefits']); // Execute dynamic action $ruleObject = new stdClass(); $ruleObject->action = function($context) { // Send VIP email Mail::to($context['email'])->send(new VIPWelcome()); // Apply discount return ['discount' => 20]; }; if ($ruleEngine->evaluate($rule, $customer)) { $result = $ruleEngine->executeAction($ruleObject, $customer); }
Supported Operators
// Numeric comparisons '>' // Greater than '<' // Less than '==' // Equal to // Examples $rule1 = $ruleEngine->createRule('AgeCheck', [ 'type' => 'comparison', 'left' => 'age', 'op' => '>', 'right' => 18 ], ['type' => 'allow']); $rule2 = $ruleEngine->createRule('StockCheck', [ 'type' => 'comparison', 'left' => 'stock', 'op' => '<', 'right' => 10 ], ['type' => 'reorder']);
Advanced Rule Engine Features
Fluent Rule Builder
Create rules using a fluent, readable syntax:
// Fluent API for rule creation $rule = $ruleEngine->rule('HighValueOrder') ->when('total', '>', 1000) ->and('customer_type', '==', 'premium') ->priority(10) ->then(function($context) { // Apply free shipping return ['free_shipping' => true]; }); // Rule is automatically saved and can be evaluated $context = ['total' => 1500, 'customer_type' => 'premium']; $result = $ruleEngine->evaluate($rule, $context); // true
Extended Operator Support
// All supported operators '==' '===' '!=' '!==' // Equality '>' '>=' '<' '<=' // Comparison 'in' 'contains' // Array operations 'starts_with' 'ends_with' // String operations // Examples $rule = $ruleEngine->createRule('EmailCheck', [ 'type' => 'comparison', 'left' => ['type' => 'variable', 'name' => 'email'], 'op' => 'ends_with', 'right' => ['type' => 'literal', 'value' => '@company.com'] ], ['action' => 'approve']); $rule = $ruleEngine->createRule('RoleCheck', [ 'type' => 'comparison', 'left' => ['type' => 'variable', 'name' => 'role'], 'op' => 'in', 'right' => ['type' => 'literal', 'value' => ['admin', 'moderator']] ], ['action' => 'grant_access']);
Logical Operations
Combine multiple conditions with AND/OR/NOT:
// Complex rule with logical operations $rule = $ruleEngine->createRule('ComplexDiscount', [ 'type' => 'logical', 'operator' => 'AND', 'left' => [ 'type' => 'comparison', 'left' => ['type' => 'variable', 'name' => 'amount'], 'op' => '>', 'right' => ['type' => 'literal', 'value' => 500] ], 'right' => [ 'type' => 'logical', 'operator' => 'OR', 'left' => [ 'type' => 'comparison', 'left' => ['type' => 'variable', 'name' => 'is_member'], 'op' => '==', 'right' => ['type' => 'literal', 'value' => true] ], 'right' => [ 'type' => 'comparison', 'left' => ['type' => 'variable', 'name' => 'coupon_code'], 'op' => '!=', 'right' => ['type' => 'literal', 'value' => null] ] ] ], ['discount' => 15], 5);
Rule Macros
Define reusable rule patterns:
// Register a macro for common rule pattern $ruleEngine->macro('premium_customer', function($ruleEngine) { return $ruleEngine->rule('PremiumCustomer') ->when('tier', '==', 'premium') ->and('active', '==', true); }); // Use the macro $premiumRule = $ruleEngine->executeMacro('premium_customer', [$ruleEngine]) ->then(function($context) { return ['special_offer' => true]; });
Custom Operators
Register your own operators:
// Register custom operator $ruleEngine->registerOperator('divisible_by', function($left, $right, $context) { return $left % $right === 0; }); // Use custom operator $rule = $ruleEngine->createRule('BulkOrder', [ 'type' => 'comparison', 'left' => ['type' => 'variable', 'name' => 'quantity'], 'op' => 'divisible_by', 'right' => ['type' => 'literal', 'value' => 12] ], ['bulk_discount' => 10]);
Rule Groups
Organize and evaluate rules in groups:
// Create multiple rules $rule1 = $ruleEngine->rule('FreeShipping') ->when('total', '>', 100) ->then(fn($ctx) => ['free_shipping' => true]); $rule2 = $ruleEngine->rule('TenPercentOff') ->when('total', '>', 500) ->then(fn($ctx) => ['discount' => 10]); // Add to group $ruleEngine->addToGroup('checkout_rules', $rule1->id); $ruleEngine->addToGroup('checkout_rules', $rule2->id); // Evaluate entire group (AND logic) $passed = $ruleEngine->evaluateGroup('checkout_rules', $context, 'AND'); // Evaluate with OR logic $passed = $ruleEngine->evaluateGroup('checkout_rules', $context, 'OR');
Priority-Based Evaluation
Rules with higher priority execute first:
// Create rules with priorities $rule1 = $ruleEngine->rule('CriticalRule') ->when('status', '==', 'urgent') ->priority(100) ->then(fn($ctx) => ['priority' => 'high']); $rule2 = $ruleEngine->rule('NormalRule') ->when('status', '==', 'normal') ->priority(10) ->then(fn($ctx) => ['priority' => 'normal']); // Batch evaluate with priority ordering $results = $ruleEngine->evaluateBatch([1, 2, 3], $context); // Returns results sorted by priority descending
Built-in Functions
Use built-in functions in rule conditions:
// Function-based rules $rule = $ruleEngine->createRule('EmptyCartCheck', [ 'type' => 'function', 'name' => 'empty', 'args' => [['type' => 'variable', 'name' => 'cart_items']] ], ['action' => 'show_empty_message']); // Other built-in functions: // - empty, isset, is_null // - count (with comparison)
Get Matching Rules
Find all rules that pass for a context:
$context = ['amount' => 750, 'customer_type' => 'premium']; // Get all matching rules $matchingRules = $ruleEngine->getMatchingRules($context); foreach ($matchingRules as $rule) { echo "Matched rule: {$rule->name}\n"; $ruleEngine->executeAction($rule, $context); }
6. Sync Engine
When to use? To synchronize data between multiple devices (mobile app, web, etc.) with offline support.
Simple Example - Mobile Sync
use Dimita\BusinessOrchestration\BusinessOrchestration; $sync = BusinessOrchestration::sync(); // On server, log each change $task = Task::find(1); $sync->logChange($task, 'INSERT', [ 'title' => 'New task', 'status' => 'pending' ]); // Later, modification $task->status = 'in_progress'; $task->save(); $sync->logChange($task, 'UPDATE', [ 'status' => 'in_progress' ]); // Another modification $task->status = 'completed'; $task->save(); $sync->logChange($task, 'UPDATE', [ 'status' => 'completed' ]);
Retrieving Changes (Mobile Client)
// Mobile client requests changes since last sync $lastSyncVersion = 5; // Version from last sync $deltas = $sync->getDeltas( 'App\\Models\\Task', $taskId, $lastSyncVersion ); // Client receives only changes after version 5 foreach ($deltas as $delta) { echo "Version {$delta['version']}: {$delta['operation']}\n"; // Apply changes locally applyChange($delta); }
Offline Scenario
// 1. Mobile app syncs $clientVersion = 0; $deltas = $sync->getDeltas('App\\Models\\Task', 1, $clientVersion); // Receives all modifications // 2. Client goes offline and makes local modifications // Modifications stored locally // 3. Client comes back online // Send local modifications to server foreach ($localChanges as $change) { $sync->logChange($model, $change['operation'], $change['fields']); } // 4. Get new changes from server $newClientVersion = 15; // Version after upload $newDeltas = $sync->getDeltas('App\\Models\\Task', 1, $newClientVersion);
Advanced Sync Engine Features
Conflict Resolution
Handle conflicts when data changes on both client and server:
// Synchronize with conflict detection $sourceModel = Task::find(1); $targetModel = Task::find(1); // Simulating different state $result = $sync->sync($sourceModel, $targetModel); /* Result: [ 'conflicts' => [ 'status' => [ 'source' => 'completed', 'target' => 'in_progress' ] ], 'synced_fields' => ['title', 'status', 'description'] ] */
Conflict Strategies
Choose how conflicts are resolved:
// Latest wins (default) $sync->setConflictStrategy('latest_wins') ->sync($source, $target); // Source always wins $sync->setConflictStrategy('source_wins') ->sync($source, $target); // Target always wins $sync->setConflictStrategy('target_wins') ->sync($source, $target); // Merge values (for arrays and strings) $sync->setConflictStrategy('merge') ->sync($source, $target);
Custom Conflict Handlers
Register custom handlers for specific fields:
// Register custom handler for 'tags' field $sync->registerConflictHandler('tags', function($values, $source, $target) { // Merge tags from both sources uniquely $sourceTags = $values['source']; $targetTags = $values['target']; return array_unique(array_merge($targetTags, $sourceTags)); }); // Now when syncing, 'tags' conflicts use custom handler $sync->sync($source, $target);
Selective Field Sync
Sync only specific fields:
// Sync only title and description $result = $sync->sync($source, $target, ['title', 'description']); // Other fields are ignored
Batch Synchronization
Sync multiple model pairs at once:
$pairs = [ ['source' => $task1, 'target' => $task1Remote], ['source' => $task2, 'target' => $task2Remote], ['source' => $task3, 'target' => $task3Remote], ]; $results = $sync->batchSync($pairs, ['title', 'status']); foreach ($results as $index => $result) { if (isset($result['error'])) { echo "Pair {$index} failed: {$result['error']}\n"; } else { echo "Pair {$index} synced: {$result['synced_fields']}\n"; } }
Sync Checkpoints
Create named checkpoints for restore points:
// Create checkpoint before major changes $sync->checkpoint($task, 'before_bulk_update'); // Make changes $task->status = 'archived'; $task->save(); // Restore from checkpoint if needed $sync->restoreCheckpoint($task, 'before_bulk_update'); // List all checkpoints $checkpoints = $sync->getCheckpoints($task); /* [ [ 'version' => 15, 'name' => 'before_bulk_update', 'created_at' => '2025-01-15 10:30:00' ], ... ] */
Sync Status Monitoring
Track synchronization activity:
$status = $sync->getSyncStatus($task); /* Result: [ 'total_syncs' => 42, 'last_sync' => '2025-01-15 14:30:00', 'current_version' => 42, 'operations' => [ 'synced' => 35, 'checkpoint' => 7 ] ] */
Model Comparison
Compare two models without syncing:
// Detect differences $differences = $sync->compare($modelA, $modelB); /* Result: [ 'title' => [ 'source' => 'Task A', 'target' => 'Task B' ], 'status' => [ 'source' => 'completed', 'target' => 'pending' ] ] */ // Compare specific fields only $differences = $sync->compare($modelA, $modelB, ['title', 'status']);
Apply Deltas
Apply a set of changes to a model:
$deltas = [ [ 'operation' => 'update', 'changed_fields' => ['status' => 'completed', 'completed_at' => now()] ], [ 'operation' => 'update', 'changed_fields' => ['priority' => 'high'] ] ]; $updatedModel = $sync->applyDeltas($task, $deltas);
Metadata Tracking
Track sync context and metadata:
$sync->logChange($task, 'synced', ['status'], [ 'device_id' => 'mobile-123', 'user_id' => auth()->id(), 'app_version' => '2.1.0' ]);
Cleanup Old Logs
Purge old synchronization logs to save space:
// Keep only last 100 logs per model type $deletedCount = $sync->purgeOldLogs('App\\Models\\Task', 100); echo "Deleted {$deletedCount} old sync logs";
7. Dependency Engine
When to use? To manage business dependencies and prevent deletions that would violate constraints.
Simple Example - Prevent Deletion
use Dimita\BusinessOrchestration\BusinessOrchestration; $dep = BusinessOrchestration::dependency(); // Define that Category cannot be deleted if Products exist $dep->addDependency( 'App\\Models\\Product', 'App\\Models\\Category', 'prevent_delete' ); // Before deleting a category $categoryId = 5; if (!$dep->checkDeletion('App\\Models\\Category', $categoryId)) { return response()->json([ 'error' => 'Cannot delete category with existing products' ], 422); } // Otherwise, delete Category::destroy($categoryId);
Complex Dependency Graph
// Define dependency graph $dep->addDependency('App\\Models\\OrderItem', 'App\\Models\\Order', 'cascade_delete'); $dep->addDependency('App\\Models\\Order', 'App\\Models\\Customer', 'prevent_delete'); $dep->addDependency('App\\Models\\Product', 'App\\Models\\Category', 'prevent_delete'); $dep->addDependency('App\\Models\\OrderItem', 'App\\Models\\Product', 'prevent_delete'); // Get all dependencies for a model $dependencies = $dep->getDependencies('App\\Models\\Product'); // Before deletion, check all constraints if (!$dep->checkDeletion('App\\Models\\Customer', $customerId)) { throw new \Exception('Customer has active orders'); }
Dependency Rule Types
// Prevent delete - Block deletion $dep->addDependency('Child', 'Parent', 'prevent_delete'); // Cascade delete - Delete children $dep->addDependency('Child', 'Parent', 'cascade_delete'); // Soft delete - Soft delete children $dep->addDependency('Child', 'Parent', 'soft_delete'); // Custom rule - Custom business rule $dep->addDependency('Child', 'Parent', 'custom_business_rule');
Real-World Use Cases
E-commerce - Complete Order Process
// 1. Saga for order processing $saga = BusinessOrchestration::saga()->startSaga('OrderProcessing', [ 'validate_cart' => ValidateCartStep::class, 'reserve_stock' => ReserveStockStep::class, 'charge_payment' => ChargePaymentStep::class, 'create_shipment' => CreateShipmentStep::class, 'send_confirmation' => SendConfirmationStep::class, ], ['order_id' => $order->id]); // 2. Workflow for order status $workflow = BusinessOrchestration::workflow(); $workflow->defineTransition('pay', 'pending', 'paid'); $workflow->defineTransition('ship', 'paid', 'shipped'); $workflow->defineTransition('deliver', 'shipped', 'delivered'); $builder = $workflow->for($order); $builder->apply('pay'); // 3. Event Sourcing for audit $es = BusinessOrchestration::eventSourcing(); $es->storeEvent("order-{$order->id}", 'OrderCreated', $order->toArray()); $es->storeEvent("order-{$order->id}", 'PaymentReceived', $payment->toArray()); $es->storeEvent("order-{$order->id}", 'OrderShipped', $shipment->toArray()); // 4. Versioning for order history $version = BusinessOrchestration::version(); $version->snapshot($order); // At each important step // 5. Business rules for discounts $rule = BusinessOrchestration::rule()->createRule('FirstOrderDiscount', [ 'type' => 'comparison', 'left' => 'order_count', 'op' => '==', 'right' => 1 ], ['discount' => 15]); // 6. Sync for customer mobile app $sync = BusinessOrchestration::sync(); $sync->logChange($order, 'UPDATE', ['status' => 'shipped']); // 7. Dependencies to prevent invalid deletions $dep = BusinessOrchestration::dependency(); $dep->addDependency('App\\Models\\OrderItem', 'App\\Models\\Order', 'cascade_delete');
SaaS - Subscription Management
// Subscription workflow $workflow = BusinessOrchestration::workflow(); $workflow->defineTransition('activate', 'trial', 'active'); $workflow->defineTransition('cancel', 'active', 'cancelled'); $workflow->defineTransition('suspend', 'active', 'suspended'); $workflow->defineTransition('reactivate', 'suspended', 'active'); // Event sourcing for billing history $es = BusinessOrchestration::eventSourcing(); $es->storeEvent("subscription-{$sub->id}", 'SubscriptionStarted', [...]); $es->storeEvent("subscription-{$sub->id}", 'PaymentProcessed', [...]); $es->storeEvent("subscription-{$sub->id}", 'UpgradedToPro', [...]); // Rules for automatic upgrades $upgradeRule = $ruleEngine->createRule('AutoUpgrade', [ 'type' => 'comparison', 'left' => 'usage', 'op' => '>', 'right' => 80 ], ['action' => 'suggest_upgrade']);
Architecture
Package Structure
src/
├── Core/ # Main engines
│ ├── SagaEngine.php
│ ├── WorkflowEngine.php
│ ├── EventSourcingEngine.php
│ ├── VersionEngine.php
│ ├── RuleEngine.php
│ ├── SyncEngine.php
│ └── DependencyEngine.php
├── Models/ # Eloquent models
│ ├── Saga.php
│ ├── SagaStep.php
│ ├── WorkflowInstance.php
│ ├── WorkflowTransition.php
│ ├── EventStore.php
│ ├── ModelVersion.php
│ ├── SyncLog.php
│ ├── Rule.php
│ └── Dependency.php
├── Drivers/ # Multi-driver support
│ ├── DatabaseDriver.php
│ ├── RedisDriver.php
│ └── QueueDriver.php
└── BusinessOrchestration.php # Main facade
Design Principles
- Human-friendly logic: Clear and intuitive API
- Production-ready: Error handling, compensation, crash recovery
- Persistent: Everything saved to database
- Extensible: Multi-driver support (DB, Redis, Queue)
- Testable: 122 tests, 100% coverage
Architecture Diagrams
1. Saga Pattern Flow
┌─────────────────────────────────────────────────────────────────┐
│ SAGA ORCHESTRATION │
└─────────────────────────────────────────────────────────────────┘
Success Flow:
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ PENDING │ -> │ RUNNING │ -> │ RUNNING │ -> │COMPLETED│
│ │ │ Step 1 │ │ Step 2 │ │ │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
Failure + Compensation Flow:
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────────┐
│ PENDING │ -> │ RUNNING │ -> │ FAILED │ -> │ COMPENSATED │
│ │ │ Step 1✓ │ │ Step 2✗ │ │ Rollback 1✓ │
└─────────┘ └─────────┘ └─────────┘ └──────────────┘
Database Schema:
sagas saga_steps
├── id ├── id
├── name ├── saga_id (FK)
├── status ├── step_name
├── payload (JSON) ├── status
├── current_step ├── executed_at
└── timestamps ├── compensated_at
├── error
└── timestamps
2. Workflow State Machine
┌─────────────────────────────────────────────────────────────────┐
│ WORKFLOW ENGINE │
└─────────────────────────────────────────────────────────────────┘
State Transition Graph:
submit
┌─────────┐ ─────────> ┌───────────┐
│ draft │ │ submitted │
└─────────┘ <───────── └───────────┘
revise │
│ review
v
┌───────────┐
reject │ in_review │ approve
┌───────┴───────────┴────────┐
│ │
v v
┌──────────┐ ┌──────────┐
│ rejected │ │ approved │
└──────────┘ └──────────┘
Database Schema:
workflow_instances workflow_transitions
├── id ├── id
├── model_type ├── instance_id (FK)
├── model_id ├── from_state
├── state ├── to_state
└── timestamps ├── transition_name
├── context (JSON)
└── timestamps
3. Event Sourcing Stream
┌─────────────────────────────────────────────────────────────────┐
│ EVENT SOURCING │
└─────────────────────────────────────────────────────────────────┘
Event Stream (Append-Only):
Time ─────────────────────────────────────────────────────>
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│OrderCreated │─>│PaymentCharged│─>│ItemsShipped │─>│OrderDelivered│
│ v1 │ │ v2 │ │ v3 │ │ v4 │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
Aggregate Rebuild:
Initial State: {}
+ OrderCreated -> {status: 'pending', total: 100}
+ PaymentCharged -> {status: 'paid', total: 100, payment_id: 123}
+ ItemsShipped -> {status: 'shipped', tracking: 'ABC123'}
+ OrderDelivered -> {status: 'delivered', delivered_at: '2024-01-01'}
Database Schema:
event_store
├── id
├── aggregate_id
├── event_type
├── event_data (JSON)
├── version
├── metadata (JSON)
└── created_at
4. Version Control System
┌─────────────────────────────────────────────────────────────────┐
│ VERSIONING ENGINE │
└─────────────────────────────────────────────────────────────────┘
Snapshot Timeline:
Model State: {name: "Doc1", status: "draft"}
│
v snapshot()
Version 1: {name: "Doc1", status: "draft", hash: "abc123"}
│
│ model.update({status: "published"})
v snapshot()
Version 2: {name: "Doc1", status: "published", hash: "def456"}
│
│ model.update({name: "Doc1-Updated"})
v snapshot()
Version 3: {name: "Doc1-Updated", status: "published", hash: "ghi789"}
│
│ restore(version: 1)
v
Restored: {name: "Doc1", status: "draft"}
Database Schema:
model_versions
├── id
├── model_type
├── model_id
├── version
├── snapshot_data (JSON)
├── hash
└── created_at
5. Rule Engine AST Evaluation
┌─────────────────────────────────────────────────────────────────┐
│ RULE ENGINE │
└─────────────────────────────────────────────────────────────────┘
Rule Definition (AST):
Business Rule: "If order total > 100 AND customer_type == 'VIP', apply 20% discount"
AST Structure:
{
type: "logical",
operator: "AND",
left: {
type: "comparison",
left: "order_total",
op: ">",
right: 100
},
right: {
type: "comparison",
left: "customer_type",
op: "==",
right: "VIP"
}
}
Evaluation Flow:
Context: {order_total: 150, customer_type: "VIP"}
├─> Evaluate left: 150 > 100 = true
├─> Evaluate right: "VIP" == "VIP" = true
└─> AND(true, true) = true -> Execute action
Database Schema:
rules
├── id
├── name
├── condition_ast (JSON)
├── action (JSON)
└── timestamps
6. Sync Engine Delta Synchronization
┌─────────────────────────────────────────────────────────────────┐
│ SYNC ENGINE │
└─────────────────────────────────────────────────────────────────┘
Multi-Device Sync:
Server Client A Client B
│ │ │
│ v1: INSERT {name: "Item1"} │ │
├────────────────────────────────>│ Sync from v0 │
│ │ Receives: [v1] │
│ │ │
│ v2: UPDATE {status: "active"} │ │
├────────────────────────────────>│ Sync from v1 │
│ │ Receives: [v2] │
│ │ │
│ │ ├─> Sync from v0
│ │ │ Receives: [v1, v2]
│ v3: UPDATE {price: 99} │ │
├────────────────────────────────>│ Sync from v2 │
│ │ Receives: [v3] │
├───────────────────────────────────────────────────────────>│ Sync from v2
│ │ │ Receives: [v3]
Database Schema:
sync_log
├── id
├── model_type
├── model_id
├── operation (INSERT|UPDATE|DELETE)
├── version
├── changed_fields (JSON)
└── created_at
7. Dependency Graph System
┌─────────────────────────────────────────────────────────────────┐
│ DEPENDENCY ENGINE │
└─────────────────────────────────────────────────────────────────┘
Dependency Graph:
┌──────────┐
│ User │
└────┬─────┘
│ has_many (dependency: prevent_delete)
├────────────┬────────────┬────────────┐
v v v v
┌─────────┐ ┌────────┐ ┌─────────┐ ┌─────────┐
│ Order │ │Profile │ │Comments │ │Payments │
└─────────┘ └────────┘ └─────────┘ └─────────┘
Deletion Check Flow:
canDelete(User #123)?
├─> Check Orders: 5 orders exist -> BLOCKED
├─> Check Profile: 1 profile exists -> BLOCKED
├─> Check Comments: 12 comments exist -> BLOCKED
└─> Check Payments: 3 payments exist -> BLOCKED
Result: Cannot delete User #123 (has dependencies)
Database Schema:
dependencies
├── id
├── source_model
├── target_model
├── dependency_type
├── created_at
└── updated_at
Overall System Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Application Layer │
│ (Controllers, Services, Commands) │
└──────────────────────┬───────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────┐
│ BusinessOrchestration Facade │
│ ->saga() ->workflow() ->eventSourcing() ->version() │
│ ->rule() ->sync() ->dependency() │
└──────────────────────┬───────────────────────────────────────────┘
│
┌───────────────┼───────────────┐
v v v
┌────────────┐ ┌────────────┐ ┌────────────┐
│ Saga │ │ Workflow │ │ Event │
│ Engine │ │ Engine │ │ Sourcing │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ │ │
┌────────────┐ ┌────────────┐ ┌────────────┐
│ Version │ │ Rule │ │ Sync │
│ Engine │ │ Engine │ │ Engine │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ │ │
└───────────────┼───────────────┘
v
┌─────────────────────────────────────────────────────────────────┐
│ Driver Layer │
│ DatabaseDriver │ RedisDriver │ QueueDriver │
└──────────────────────┬───────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────┐
│ Persistence Layer │
│ MySQL │ PostgreSQL │ SQLite │ Redis │ RabbitMQ │
└─────────────────────────────────────────────────────────────────┘
Testing
The package includes 122 tests covering all use cases:
# Run all tests vendor/bin/phpunit # With details vendor/bin/phpunit --testdox # Result OK (122 tests, 252 assertions)
Test Coverage by Engine
- SagaEngine: 16 tests (compensation, resume, states)
- WorkflowEngine: 16 tests (transitions, guards, states)
- RuleEngine: 18 tests (evaluation, AST, actions)
- DependencyEngine: 17 tests (graphs, constraints)
- EventSourcingEngine: 16 tests (events, rebuild, versioning)
- VersionEngine: 19 tests (snapshots, restore, audit)
- SyncEngine: 20 tests (deltas, offline, incremental)
Configuration
Publish the configuration file:
php artisan vendor:publish --provider="Dimita\\BusinessOrchestration\\BusinessOrchestrationServiceProvider"
Available in config/business-orchestration.php:
return [ /* |-------------------------------------------------------------------------- | Enabled Engines |-------------------------------------------------------------------------- | | Configure which engines should be loaded and available in your application. | Set to false to disable an engine completely and improve performance. | By default, all engines are enabled. | */ 'engines' => [ 'saga' => env('ORCHESTRATION_SAGA_ENABLED', true), 'workflow' => env('ORCHESTRATION_WORKFLOW_ENABLED', true), 'sync' => env('ORCHESTRATION_SYNC_ENABLED', true), 'version' => env('ORCHESTRATION_VERSION_ENABLED', true), 'event_sourcing' => env('ORCHESTRATION_EVENT_SOURCING_ENABLED', true), 'rule' => env('ORCHESTRATION_RULE_ENABLED', true), 'dependency' => env('ORCHESTRATION_DEPENDENCY_ENABLED', true), ], /* |-------------------------------------------------------------------------- | Storage Drivers |-------------------------------------------------------------------------- | | Configure how orchestration data is stored and retrieved. | Supports: database, redis, queue | */ 'drivers' => [ 'default' => env('BUSINESS_ORCHESTRATION_DRIVER', 'database'), 'database' => [ 'connection' => env('DB_CONNECTION', 'mysql'), ], 'redis' => [ 'connection' => env('REDIS_CONNECTION', 'default'), ], 'queue' => [ 'connection' => env('QUEUE_CONNECTION', 'sync'), ], ], ];
Configuration Examples
Scenario 1: E-commerce application (needs Saga, Workflow, Versioning)
'engines' => [ 'saga' => true, // For order processing 'workflow' => true, // For order status transitions 'sync' => false, // No mobile sync needed 'version' => true, // For order audit trail 'event_sourcing' => false, // Not needed 'rule' => true, // For discount rules 'dependency' => false, // Not needed ],
Scenario 2: Mobile-first app with offline support
'engines' => [ 'saga' => false, 'workflow' => false, 'sync' => true, // Critical for mobile sync 'version' => true, // Version tracking 'event_sourcing' => true, // Event history 'rule' => false, 'dependency' => false, ],
Scenario 3: Enterprise workflow system
'engines' => [ 'saga' => true, // Multi-step processes 'workflow' => true, // State machines 'sync' => false, 'version' => true, // Document versioning 'event_sourcing' => true, // Complete audit trail 'rule' => true, // Business rules 'dependency' => true, // Constraint management ],
Support
- Documentation: This README
- Issues: GitHub Issues
- Tests: 100% coverage, 122 tests
License
MIT License - Free for commercial and open-source projects.
Changelog
v1.0.0 (2025)
- ✅ Saga Pattern with automatic compensation
- ✅ Workflow Engine with guards
- ✅ Event Sourcing with rebuild
- ✅ Versioning with snapshots
- ✅ Rule Engine with AST
- ✅ Sync Engine for multi-device
- ✅ Dependency Engine for business constraints
- ✅ 122 tests with 100% coverage
- ✅ Production-ready with complete error handling
Made with ❤️ for the Laravel community
Based on production-tested patterns I've refined over years of building enterprise applications.