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 (e.g., app/Fsm/Enums/OrderStatus.php):
enum OrderStatus implements FsmStateEnum {
case Pending; case Paid; case Shipped; case Delivered;
}
Register FSM Definition
Create a definition class (e.g., app/Fsm/Definitions/OrderStatusFsm.php):
use Fsm\FsmBuilder;
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
use Fsm\Traits\HasFsm;
class Order extends Model {
use HasFsm;
}
Trigger First Transition
$order = Order::create(['status' => OrderStatus::Pending->value]);
$order->fsm()->trigger('pay'); // Transitions to Paid
Order Workflow: Define a simple order status FSM with pending → paid transition. Use trigger() to process payments and validate state changes in real-time.
State Transition
// Direct transition (bypassing events)
$order->transitionFsm('status', OrderStatus::Paid);
// Event-driven transition (recommended)
$order->fsm()->trigger('pay');
Guarded Transitions
FsmBuilder::for(Order::class, 'status')
->from(OrderStatus::Pending)->to(OrderStatus::Paid)
->event('pay')
->guard([PaymentValidator::class, 'validate'])
->build();
Actions & Callbacks
FsmBuilder::for(Order::class, 'status')
->state(OrderStatus::Paid)
->onEntry([SendReceipt::class, 'handle'])
->from(OrderStatus::Pending)->to(OrderStatus::Paid)
->action([LogPayment::class, 'log'])
->build();
Multiple FSMs
// Define separate FSMs for approval and publication
FsmBuilder::for(Document::class, 'approval_status')->...
FsmBuilder::for(Document::class, 'publication_status')->...
// Trigger specific FSM
$doc->fsm('approval_status')->trigger('approve');
StateTransitioned/TransitionFailed for audit trails:
Event::listen(StateTransitioned::class, fn($event) => Log::info($event->model));
->queuedAction(NotifyCustomerJob::class)
$result = $order->fsm()->dryRun('cancel');
| Pattern | Example |
|---|---|
| Terminal States | ->state(OrderStatus::Delivered, fn($state) => $state->isTerminal(true)) |
| Wildcard Transitions | ->from(\Fsm\Constants::STATE_WILDCARD)->to(OrderStatus::Cancelled) |
| Metadata | ->state(OrderStatus::Paid, fn($state) => $state->metadata(['color' => 'green'])) |
Caching Issues
php artisan fsm:cache:clear
php artisan fsm:diagram to visualize FSMs and debug transitions.Transaction Conflicts
use_transactions in config/fsm.php if callbacks fail due to deadlocks.State Validation
initialState matches the database default to avoid null state errors.can() to check transitions before triggering:
if ($order->fsm()->can('pay')) { ... }
Enum vs. String States
value.protected $casts = ['status' => OrderStatus::class];
log_failures in config/fsm.php to capture transition errors.trigger() for debugging:
$order->fsm()->trigger('pay', new class implements ArgonautDTOContract {
public function getUserId() { return auth()->id(); }
});
Custom Guards
class PaymentGuard {
public function __invoke($model, $from, $to, $context) {
return $model->amount > 0;
}
}
Dynamic States
Override FsmBuilder to load states from a database or API.
Event Modifiers
Extend StateTransitioned to add custom logic:
Event::listen(StateTransitioned::class, fn($event) => {
if ($event->toState === OrderStatus::Paid) {
cache()->forever("order_{$event->model->id}_paid", true);
}
});
fsm()->batch() for bulk operations (e.g., updating 1000 orders).event_logging with queue: false for high-throughput systems.transitionFsm() or trigger() to respect guards/actions.How can I help you explore Laravel packages today?