# Laravel FSM Laravel FSM is a robust, plug-and-play Finite State Machine package for Laravel applications. It provides a fluent API to define states, transitions, guards, actions, and callbacks for managing stateful workflows in Eloquent models. The package supports features like event-driven architecture, comprehensive logging, database transactions, state machine caching, and diagram generation for visualization. The package integrates seamlessly with Laravel's ecosystem including Laravel policies for authorization, queued jobs for async operations, and the Verbs event sourcing library. It's ideal for implementing workflows like order processing (pending -> paid -> shipped -> delivered), user verification flows, content moderation pipelines, and any other business process requiring controlled state transitions with validation logic. ## Installation ```bash composer require christhompsontldr/laravel-fsm ``` ```bash php artisan vendor:publish --provider="Fsm\FsmServiceProvider" --tag="fsm-config" ``` ## FsmBuilder::for() - Create FSM Definition Creates and returns a TransitionBuilder for defining states and transitions on a specific model column. ```php initialState(OrderStatus::Pending) ->state(OrderStatus::Pending) ->state(OrderStatus::Paid) ->state(OrderStatus::Shipped) ->state(OrderStatus::Delivered, fn ($state) => $state->isTerminal(true)) ->state(OrderStatus::Cancelled) ->from(OrderStatus::Pending)->to(OrderStatus::Paid)->event('pay') ->from(OrderStatus::Paid)->to(OrderStatus::Shipped)->event('ship') ->from(OrderStatus::Shipped)->to(OrderStatus::Delivered)->event('deliver') ->from([OrderStatus::Pending, OrderStatus::Paid])->to(OrderStatus::Cancelled)->event('cancel') ->build(); } } ``` ## FsmStateEnum Interface - Define State Enum Implement this interface for type-safe state enums with labels and string backing values. ```php 'Pending Payment', self::Paid => 'Payment Received', self::Shipped => 'In Transit', self::Delivered => 'Delivered', self::Cancelled => 'Cancelled', }; } } ``` ## HasFsm Trait - Enable FSM on Models Add this trait to Eloquent models to enable FSM functionality with automatic initial state setting on creation. ```php \App\Fsm\Enums\OrderStatus::class, ]; } // Usage $order = Order::create(['amount' => 99.99]); // status auto-set to 'pending' // Trigger transitions via events $order->fsm()->trigger('pay'); $order->fsm()->trigger('ship'); // Check if transition is possible if ($order->fsm()->can('deliver')) { $order->fsm()->trigger('deliver'); } // Dry run - preview transition without executing $preview = $order->fsm()->dryRun('cancel'); // Returns: ['can_transition' => true, 'from_state' => 'shipped', 'to_state' => 'cancelled', ...] // Get current state $currentState = $order->getFsmState(); // Returns OrderStatus enum // Direct state transition (bypassing event lookup) $order->transitionFsm('status', OrderStatus::Cancelled); // Check transition possibility with context $order->canTransitionFsm('status', OrderStatus::Shipped, $contextDto); ``` ## TransitionBuilder - Fluent Transition API Define transitions with guards, actions, and callbacks using the fluent builder pattern. ```php initialState(OrderStatus::Pending) // State with callbacks and metadata ->state(OrderStatus::Paid, fn ($state) => $state ->description('Order payment confirmed') ->onEntry([SendReceipt::class, 'handle']) ->metadata(['color' => 'green', 'icon' => 'check']) ->priority(10) ) ->state(OrderStatus::Shipped, fn ($state) => $state ->onEntry([NotifyWarehouse::class, 'dispatch']) ->onExit(fn ($input) => logger()->info('Leaving shipped state')) ->type('intermediate') ->category('fulfillment') ) // Transition with guard ->from(OrderStatus::Pending)->to(OrderStatus::Paid) ->event('pay') ->guard([PaymentValidator::class, '__invoke']) ->description('Process payment for order') // Transition with synchronous action ->from(OrderStatus::Paid)->to(OrderStatus::Shipped) ->event('ship') ->action([NotifyWarehouse::class, 'reserve']) ->after(fn ($input) => logger()->info('Order shipped', ['id' => $input->model->id])) // Transition with queued action ->from(OrderStatus::Shipped)->to(OrderStatus::Delivered) ->event('deliver') ->queuedAction(SendShippingNotification::class) ->onSuccess(fn ($input) => cache()->forget("order:{$input->model->id}")) // Wildcard transition (from any state) ->transition() ->from(Constants::STATE_WILDCARD) ->to(OrderStatus::Cancelled) ->event('force_cancel') ->criticalGuard(fn ($input) => auth()->user()?->isAdmin()) ->onFailure(fn ($input) => logger()->warning('Cancel rejected')) ->add() ->build(); ``` ## TransitionInput DTO - Access Transition Context The TransitionInput DTO provides access to all transition data in guards, actions, and callbacks. ```php model; // Access state information $fromState = $input->fromState; // FsmStateEnum|string|null $toState = $input->toState; // FsmStateEnum|string // Access the triggering event name $event = $input->event; // 'pay', 'ship', etc. // Access custom context DTO $context = $input->context; // ArgonautDTOContract|null // Check transition mode if ($input->isDryRun()) { return true; // Allow dry runs } if ($input->isForced()) { return true; // Forced transition bypasses validation } // Access metadata $priority = $input->getMetadata('priority', 'normal'); // Access transition source $source = $input->getSource(); // 'user', 'system', 'api', 'scheduler' // Validate business logic return $order->payment_confirmed && $order->amount > 0; } } ``` ## Guards - Control Transition Authorization Guards validate whether a transition should be allowed. Multiple guards can be chained. ```php initialState(OrderStatus::Pending) // Simple closure guard ->from(OrderStatus::Pending)->to(OrderStatus::Paid) ->event('pay') ->guard(fn (TransitionInput $input) => $input->model->amount > 0) // Class-based guard with parameters ->from(OrderStatus::Paid)->to(OrderStatus::Shipped) ->event('ship') ->guard([InventoryChecker::class, 'check'], ['warehouse' => 'main']) // Critical guard - stops on failure with high priority ->from(OrderStatus::Shipped)->to(OrderStatus::Delivered) ->event('deliver') ->criticalGuard(fn ($input) => $input->model->shipping_address !== null) // Multiple guards - all must pass ->from(OrderStatus::Pending)->to(OrderStatus::Cancelled) ->event('cancel') ->guard(fn ($input) => $input->model->created_at->diffInHours(now()) < 24) ->guard([RefundPolicy::class, 'canCancel']) // Policy-based guard using Laravel policies ->from(OrderStatus::Paid)->to(OrderStatus::Refunded) ->event('refund') ->policy('refund') // Checks OrderPolicy@refund // Policy with custom parameters ->from(OrderStatus::Shipped)->to(OrderStatus::Returned) ->event('return') ->policyCanTransition(['reason' => 'defective']) ->build(); ``` ## PolicyGuard - Laravel Authorization Integration Integrate FSM transitions with Laravel's policy-based authorization system. ```php id === $order->customer_id; } public function ship(User $user, Order $order): bool { return $user->hasRole('warehouse_staff'); } public function refund(User $user, Order $order): bool { return $user->hasRole('customer_service') && $order->created_at->diffInDays(now()) <= 30; } // Transition-specific authorization public function transitionToRefunded(User $user, Order $order): bool { return $user->can('process_refunds'); } } // In FSM definition FsmBuilder::for(Order::class, 'status') ->from(OrderStatus::Paid)->to(OrderStatus::Refunded) ->event('refund') ->policy('refund') ->build(); // Usage - will check OrderPolicy@refund $order->fsm()->trigger('refund'); // Throws if policy fails ``` ## Actions and Callbacks - Execute Side Effects Execute code during state transitions with precise timing control. ```php initialState(OrderStatus::Pending) // State entry/exit callbacks ->state(OrderStatus::Shipped, fn ($state) => $state ->onEntry(fn (TransitionInput $input) => cache()->put("order:{$input->model->id}:shipped_at", now()) ) ->onExit(fn (TransitionInput $input) => cache()->forget("order:{$input->model->id}:shipped_at") ) ) ->from(OrderStatus::Pending)->to(OrderStatus::Paid) ->event('pay') // Before transition (before state change) ->before(fn ($input) => logger()->info('Starting payment')) // Action during transition ->action([PaymentProcessor::class, 'process']) // Immediate high-priority action ->immediateAction(fn ($input) => $input->model->increment('payment_attempts')) // After transition (after state saved) ->after(fn ($input) => event(new PaymentCompleted($input->model))) // Queued action for heavy operations ->queuedAction(SendReceiptEmail::class) // Success handler ->onSuccess(fn ($input) => metrics()->increment('payments.success')) // Failure handler ->onFailure(fn ($input) => metrics()->increment('payments.failed')) // Cleanup action (low priority, runs last) ->cleanup(fn ($input) => cache()->forget("payment_lock:{$input->model->id}")) ->build(); ``` ## Multiple FSMs per Model Define independent state machines for different model columns. ```php initialState('draft') ->state('draft') ->state('under_review') ->state('approved', fn ($s) => $s->isTerminal(true)) ->state('rejected', fn ($s) => $s->isTerminal(true)) ->from('draft')->to('under_review')->event('submit') ->from('under_review')->to('approved')->event('approve') ->from('under_review')->to('rejected')->event('reject') ->from('rejected')->to('draft')->event('revise') ->build(); // Publication workflow (separate FSM) FsmBuilder::for(Document::class, 'publication_status') ->initialState('unpublished') ->state('unpublished') ->state('published') ->state('archived') ->from('unpublished')->to('published')->event('publish') ->from('published')->to('archived')->event('archive') ->from('archived')->to('published')->event('republish') ->build(); // Usage - address each FSM by column name $document = Document::find(1); $document->fsm('approval_status')->trigger('submit'); $document->fsm('publication_status')->trigger('publish'); // Check states independently $approvalState = $document->getFsmState('approval_status'); $publicationState = $document->getFsmState('publication_status'); // Check transitions independently $canPublish = $document->fsm('publication_status')->can('publish'); $canApprove = $document->fsm('approval_status')->can('approve'); ``` ## State Events - Listen to Transitions Listen to FSM events using Laravel's event system for logging, notifications, and integrations. ```php info('State transitioned', [ 'model' => get_class($event->model), 'model_id' => $event->model->getKey(), 'column' => $event->columnName, 'from' => $event->fromState, 'to' => $event->toState, 'transition' => $event->transitionName, 'timestamp' => $event->timestamp, 'metadata' => $event->metadata, ]); }); // Listen to transition attempts Event::listen(TransitionAttempted::class, function (TransitionAttempted $event) { metrics()->increment("fsm.attempts.{$event->columnName}"); }); // Listen to failures for alerting Event::listen(TransitionFailed::class, function (TransitionFailed $event) { logger()->error('Transition failed', [ 'model' => get_class($event->model), 'from' => $event->fromState, 'to' => $event->toState, 'error' => $event->exception->getMessage(), ]); }); // Listen to successful transitions Event::listen(TransitionSucceeded::class, function (TransitionSucceeded $event) { broadcast(new OrderStatusUpdated($event->model)); }); ``` ## FsmReplayService - Audit and Debug Transitions Replay and analyze transition history for debugging and compliance auditing. ```php getTransitionHistory( Order::class, $orderId, 'status' ); foreach ($history as $log) { echo "{$log->from_state} -> {$log->to_state} at {$log->occurred_at}\n"; } // Replay transitions to determine final state $replay = $replayService->replayTransitions(Order::class, $orderId, 'status'); // Returns: [ // 'initial_state' => 'pending', // 'final_state' => 'delivered', // 'transition_count' => 4, // 'transitions' => [...] // ] // Validate history consistency (detect gaps or corruption) $validation = $replayService->validateTransitionHistory(Order::class, $orderId, 'status'); if (!$validation['valid']) { foreach ($validation['errors'] as $error) { logger()->error("Inconsistent FSM history: {$error}"); } } // Get transition statistics for analytics $stats = $replayService->getTransitionStatistics(Order::class, $orderId, 'status'); // Returns: [ // 'total_transitions' => 4, // 'unique_states' => 5, // 'state_frequency' => ['pending' => 1, 'paid' => 2, ...], // 'transition_frequency' => ['pending -> paid' => 1, ...] // ] ``` ## Artisan Commands - Generate and Visualize FSMs Use artisan commands to scaffold FSM definitions and generate diagrams. ```bash # Generate FSM definition, enum, and test files php artisan make:fsm Payment Order # This creates: # - app/Fsm/PaymentFsm.php (FSM definition) # - app/Enums/PaymentStatus.php (State enum) # - tests/Feature/Fsm/PaymentFsmTest.php (Feature test) # Generate PlantUML diagrams for all FSMs php artisan fsm:diagram storage/app/fsm-diagrams # Generate DOT format diagrams (Graphviz) php artisan fsm:diagram storage/app/fsm-diagrams --format=dot # Clear FSM definition cache php artisan fsm:cache:clear ``` ## Configuration Options Configure FSM behavior through `config/fsm.php`. ```php 'status', // Wrap transitions in database transactions 'use_transactions' => true, // Enable debug logging 'debug' => false, // Definition caching for performance 'cache' => [ 'enabled' => env('FSM_CACHE_ENABLED', false), 'path' => storage_path('framework/cache/fsm.php'), ], // Event logging to fsm_event_logs table 'event_logging' => [ 'enabled' => true, 'queue' => false, // Queue logging for better performance 'auto_register_listeners' => true, ], // Transition logging to fsm_logs table 'logging' => [ 'enabled' => true, 'log_failures' => true, 'channel' => 'stack', // Laravel log channel 'structured' => true, // Pass arrays to logger 'excluded_context_properties' => ['password', 'secret'], 'exception_character_limit' => 65535, ], // Thunk Verbs event sourcing integration 'verbs' => [ 'dispatch_transitioned_verb' => true, 'log_user_subject' => true, ], // FSM discovery paths (null = app/Fsm) 'discovery_paths' => null, // Modular extensions 'modular' => [ 'extensions' => [], 'state_overrides' => [], 'transition_overrides' => [], 'runtime_extensions' => [ 'enabled' => true, 'cache_extensions' => false, ], ], ]; ``` ## Context DTOs - Pass Data to Transitions Pass typed context data through transitions using Argonaut DTOs. ```php 'credit_card', 'transactionId' => 'txn_123456', 'amount' => 99.99, 'notes' => 'Rush order', ]); $order->fsm()->trigger('pay', $context); // Access context in guards/actions FsmBuilder::for(Order::class, 'status') ->from(OrderStatus::Pending)->to(OrderStatus::Paid) ->event('pay') ->guard(function ($input) { $context = $input->context; if ($context instanceof PaymentContext) { return $context->amount >= $input->model->total; } return false; }) ->action(function ($input) { if ($input->context instanceof PaymentContext) { $input->model->update([ 'payment_method' => $input->context->paymentMethod, 'transaction_id' => $input->context->transactionId, ]); } }) ->build(); ``` ## Summary Laravel FSM is best suited for applications requiring controlled, auditable state transitions such as e-commerce order workflows, user verification processes, content publishing pipelines, ticket/issue tracking systems, and approval workflows. The package excels in scenarios where business rules must govern state changes, transitions need logging for compliance, and multiple stakeholders require visibility into process states. Integration follows a straightforward pattern: define state enums implementing `FsmStateEnum`, create definition classes implementing `FsmDefinition` in `app/Fsm/`, add the `HasFsm` trait to your models, and trigger transitions via `$model->fsm()->trigger('event')`. The fluent builder API allows progressive enhancement with guards for validation, actions for side effects, callbacks for logging/notifications, and queued jobs for heavy async operations. For advanced use cases, leverage the replay service for auditing, policy guards for authorization, multiple FSMs per model for parallel workflows, and modular extensions for runtime customization.