webrek/laravel-state-machine
Installation:
composer require webrek/laravel-state-machine
Publish the migration for transition history (if needed):
php artisan vendor:publish --provider="Webrek\StateMachine\StateMachineServiceProvider" --tag="migrations"
php artisan migrate
Define a State Machine:
Create a class extending Webrek\StateMachine\StateMachine (e.g., app/Models/OrderStatus.php):
use Webrek\StateMachine\StateMachine;
use Webrek\StateMachine\Transition;
class OrderStatus extends StateMachine
{
public function states(): array { return ['pending', 'paid', 'shipped', 'cancelled']; }
public function transitions(): array {
return [
'pay' => new Transition('pending', 'paid'),
'ship' => new Transition('paid', 'shipped'),
'cancel' => new Transition(['pending', 'paid'], 'cancelled'),
];
}
}
Attach to Eloquent Model:
Use the HasStateMachine trait in your model (e.g., app/Models/Order.php):
use Webrek\StateMachine\HasStateMachine;
class Order extends Model
{
use HasStateMachine;
protected $stateMachine = OrderStatus::class;
protected $stateColumn = 'status';
}
First Transition:
$order = Order::find(1);
$order->transition('pay'); // Enforces rules and updates state
php artisan test to verify the package works in your environment.dd($order->getStateMachine()->getErrors()) to inspect transition failures.Define States and Transitions:
class OrderStatus extends StateMachine {
public function states() { return ['draft', 'submitted', 'approved', 'rejected']; }
public function transitions() {
return [
'submit' => new Transition('draft', 'submitted'),
'approve' => new Transition('submitted', 'approved', [
'guard' => fn(Order $order) => $order->isValid(),
]),
];
}
}
guard in Transition).onTransition:
public function transitions() {
return [
'ship' => (new Transition('paid', 'shipped'))
->onTransition(fn(Order $order) => event(new OrderShipped($order))),
];
}
Transition in Controllers:
public function pay(Order $order) {
$order->transition('pay');
return redirect()->route('orders.show', $order);
}
try {
$order->transition('pay');
} catch (InvalidTransition $e) {
return back()->withErrors($e->getMessage());
}
Querying by State:
$pendingOrders = Order::whereState('pending')->get();
transition() in API controllers with Problem responses for errors:
try {
$order->transition('cancel');
} catch (InvalidTransition $e) {
return response()->problem($e->getMessage(), status: 400);
}
$order->dispatchSync(fn() => $order->transition('ship'));
assertState() in PHPUnit:
$order->transition('pay');
$this->assertState($order, 'paid');
states() to fetch from a config or database.whenInState() in guards:
'cancel' => new Transition(['paid', 'shipped'], 'cancelled')
->guard(fn(Order $order) => $order->whenInState('paid')->canCancel()),
transition_history table:
$order->transition('pay'); // Logs to DB automatically
Missing Migration:
php artisan vendor:publish --tag="migrations" and php artisan migrate.State Column Mismatch:
$stateColumn isn’t set, the package defaults to state, which may conflict with existing columns.protected $stateColumn = 'custom_status'; in your model.Guard Logic Errors:
false vs. throwing an exception).throw new InvalidTransition() in guards for consistency:
'guard' => fn(Order $order) => $order->isValid() ?: throw new InvalidTransition('Invalid order'),
Circular Dependencies:
A → B → A) may cause infinite loops in guards.allowSelfTransitions(false) in the StateMachine constructor.Event Binding:
->onTransition(fn(Order $order) => event(new OrderEvent($order, ['reason' => 'manual'])))
dd($order->getStateMachine()->getCurrentState());
dd($order->getStateMachine()->getAllowedTransitions());
APP_DEBUG=true) to see transition logs in the console.$order->shouldReceive('isValid')->andReturn(false);
$this->expectException(InvalidTransition::class);
$order->transition('approve');
Custom Guard Classes: Create reusable guard logic:
class ValidOrderGuard {
public function __invoke(Order $order) {
return $order->isValid();
}
}
Use in transitions:
'approve' => new Transition('submitted', 'approved', [
'guard' => new ValidOrderGuard(),
]),
State Machine Events:
Extend the StateMachine class to dispatch custom events:
class OrderStatus extends StateMachine {
protected function onTransition(Order $order, string $transition) {
event(new CustomOrderEvent($order, $transition));
}
}
Database-Driven States:
Dynamically load states from a states table:
public function states(): array {
return State::query()->pluck('name')->toArray();
}
Soft Transitions: For async workflows, use a queue job to handle transitions:
$order->transitionSync('pay'); // Sync version for immediate feedback
TransitionJob::dispatch($order, 'pay'); // Async version
How can I help you explore Laravel packages today?