Try Live
Add Docs
Rankings
Pricing
Docs
Install
Install
Docs
Pricing
More...
More...
Try Live
Rankings
Enterprise
Create API Key
Add Docs
Laravel FSM
https://github.com/christhompsontldr/laravel-fsm
Admin
A robust, plug-and-play Finite State Machine (FSM) package for Laravel applications with advanced
...
Tokens:
74,331
Snippets:
732
Trust Score:
7.2
Update:
1 month ago
Context
Skills
Chat
Benchmark
86.2
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# 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 <?php namespace App\Fsm\Definitions; use App\Fsm\Enums\OrderStatus; use App\Models\Order; use Fsm\Contracts\FsmDefinition; use Fsm\FsmBuilder; class OrderStatusFsm implements FsmDefinition { public function define(): void { FsmBuilder::for(Order::class, 'status') ->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 <?php namespace App\Fsm\Enums; use Fsm\Contracts\FsmStateEnum; enum OrderStatus: string implements FsmStateEnum { case Pending = 'pending'; case Paid = 'paid'; case Shipped = 'shipped'; case Delivered = 'delivered'; case Cancelled = 'cancelled'; public function label(): string { return match($this) { self::Pending => '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 <?php namespace App\Models; use Fsm\Traits\HasFsm; use Illuminate\Database\Eloquent\Model; class Order extends Model { use HasFsm; protected $fillable = ['status', 'amount', 'customer_id']; protected $casts = [ 'status' => \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 <?php use App\Fsm\Enums\OrderStatus; use App\Models\Order; use App\Guards\PaymentValidator; use App\Actions\SendReceipt; use App\Actions\NotifyWarehouse; use App\Jobs\SendShippingNotification; use Fsm\Constants; use Fsm\FsmBuilder; FsmBuilder::for(Order::class, 'status') ->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 <?php use Fsm\Data\TransitionInput; class PaymentValidator { public function __invoke(TransitionInput $input): bool { // Access the model being transitioned $order = $input->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 <?php use App\Models\Order; use App\Fsm\Enums\OrderStatus; use Fsm\FsmBuilder; use Fsm\Data\TransitionInput; FsmBuilder::for(Order::class, 'status') ->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 <?php namespace App\Policies; use App\Models\Order; use App\Models\User; class OrderPolicy { public function pay(User $user, Order $order): bool { return $user->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 <?php use App\Models\Order; use App\Fsm\Enums\OrderStatus; use Fsm\FsmBuilder; use Fsm\Data\TransitionInput; FsmBuilder::for(Order::class, 'status') ->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 <?php use App\Models\Document; use Fsm\FsmBuilder; // Approval workflow FsmBuilder::for(Document::class, 'approval_status') ->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 <?php use Fsm\Events\StateTransitioned; use Fsm\Events\TransitionAttempted; use Fsm\Events\TransitionFailed; use Fsm\Events\TransitionSucceeded; use Illuminate\Support\Facades\Event; // Listen to successful state changes Event::listen(StateTransitioned::class, function (StateTransitioned $event) { logger()->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 <?php use Fsm\Services\FsmReplayService; use App\Models\Order; $replayService = app(FsmReplayService::class); // Get complete transition history $history = $replayService->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 <?php // config/fsm.php return [ // Default state column name when not specified 'default_column_name' => '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 <?php use App\Models\Order; use Fsm\Data\Dto; use YorCreative\LaravelArgonautDTO\ArgonautDTOContract; class PaymentContext extends Dto implements ArgonautDTOContract { public string $paymentMethod; public string $transactionId; public float $amount; public ?string $notes = null; } // Pass context when triggering transitions $order = Order::find(1); $context = PaymentContext::from([ 'paymentMethod' => '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.