christhompsontldr/laravel-fsm
Robust finite state machine for Laravel with zero-config setup. Define states and transitions with guards, actions, and entry/exit callbacks. Event-driven with comprehensive transition logging, validation, caching, and support for multiple state machines per model column.
Installation:
composer require christhompsontldr/laravel-fsm
php artisan vendor:publish --provider="Fsm\FsmServiceProvider" --tag="fsm-config"
Define States:
Create an enum implementing FsmStateEnum in app/Fsm/Enums:
enum OrderStatus implements FsmStateEnum {
case Pending; case Paid; case Shipped;
public function label(): string { return ucfirst($this->value); }
}
Register FSM Definition:
Create a class in app/Fsm/Definitions implementing FsmDefinition:
class OrderStatusFsm implements FsmDefinition {
public function define() {
FsmBuilder::for(Order::class, 'status')
->initialState(OrderStatus::Pending)
->state(OrderStatus::Pending)
->state(OrderStatus::Paid)
->from(OrderStatus::Pending)->to(OrderStatus::Paid)->event('pay')
->build();
}
}
Add Trait to Model:
class Order extends Model {
use HasFsm;
protected $fillable = ['status'];
}
Trigger Transition:
$order = Order::create(['status' => OrderStatus::Pending->value]);
$order->fsm()->trigger('pay');
Order Workflow: Implement a basic order processing flow with states pending → paid → shipped. Use trigger() to move orders through states and can() to validate transitions before execution.
State Definition:
Use the fluent FsmBuilder API to define states, transitions, and metadata:
FsmBuilder::for(Order::class, 'status')
->initialState(OrderStatus::Pending)
->state(OrderStatus::Paid, fn ($state) => $state->onEntry([SendReceipt::class]))
->from(OrderStatus::Pending)->to(OrderStatus::Paid)->event('pay')
->guard([ValidatePayment::class])
->action([LogPayment::class])
->build();
Multi-Column FSMs: Define independent workflows on the same model:
FsmBuilder::for(Document::class, 'approval_status')
->initialState('draft')
->from('draft')->to('review')->event('submit')
->build();
FsmBuilder::for(Document::class, 'publication_status')
->initialState('unpublished')
->from('unpublished')->to('published')->event('publish')
->build();
Access via:
$doc->fsm('approval_status')->trigger('submit');
Event-Driven Transitions:
Listen to StateTransitioned events for audit trails or side effects:
Event::listen(StateTransitioned::class, function ($event) {
Log::info("Transitioned {$event->model->id} from {$event->fromState} to {$event->toState}");
});
->guard(function ($model, $from, $to) {
return $model->amount > 0;
})
->queuedAction(NotifyCustomerJob::class)
$preview = $order->fsm()->dryRun('pay');
php artisan fsm:diagram
State Metadata: Attach metadata to states for UI/UX (e.g., colors, labels):
->state(OrderStatus::Paid, fn ($state) => $state->metadata(['color' => 'green']))
Terminal States: Mark states as unreachable from others:
->state(OrderStatus::Delivered, fn ($state) => $state->isTerminal(true))
Wildcard Transitions: Handle bulk transitions:
->from(\Fsm\Constants::STATE_WILDCARD)->to(OrderStatus::Cancelled)->event('cancel_all')
Caching:
php artisan fsm:cache:clear
config('app.debug' => true)).Transactions:
use_transactions in config/fsm.php may lead to partial state updates if actions fail mid-transition.State Enums:
FsmStateEnum and include a label() method for human-readable output.Event Order:
Context Data:
trigger() for dynamic transitions:
$order->fsm()->trigger('pay', ['amount' => 100]);
$context->get('amount').Transition Failures:
log_failures in config/fsm.php to log rejected transitions.TransitionFailed events for detailed error context:
Event::listen(TransitionFailed::class, function ($event) {
Log::error("Failed transition: {$event->error}");
});
State Validation:
dryRun() to validate transitions before execution:
if (!$order->fsm()->dryRun('pay')->can_transition) {
throw new \Exception("Invalid transition");
}
Diagram Mismatches:
php artisan fsm:diagram --force
Custom Guards:
FsmGuardContract for reusable validation logic:
class MinAmountGuard implements FsmGuardContract {
public function __invoke($model, $from, $to, $context) {
return $model->amount >= $context->get('min_amount');
}
}
Dynamic States:
FsmStateProvider:
FsmBuilder::for(Order::class)
->stateProvider([DynamicStateProvider::class])
->build();
Event Logging:
TransitionAttempted/StateTransitioned for custom logging:
Event::listen(StateTransitioned::class, function ($event) {
Analytics::track($event->model, 'state_change', [
'from' => $event->fromState,
'to' => $event->toState
]);
});
Caching:
event_logging.queue to defer logging to a queue for high-throughput systems.Batch Transitions:
transitionFsm() for bulk state updates (bypasses event system):
Order::where('status', OrderStatus::Pending->value)
->update(['status' => OrderStatus::Paid->value]);
Lazy Loading:
$order->loadFsm(); // Loads FSM definition on demand
How can I help you explore Laravel packages today?