spatie/laravel-model-states
Add robust state behavior to Laravel Eloquent models using the state pattern and state machines. Represent each state as a class, cast states transparently to/from the database, and define clear, safe transitions with configurable state logic.
Begin by installing the package via Composer and publishing its config (though the config is minimal and often unused). Next, define an abstract state class (e.g., PaymentState extends State) in app/States/ and concrete state classes (e.g., Pending, Paid) inside the same directory. Use the HasStates trait on your Eloquent model and cast the state column (e.g., state) to your abstract state class. Set a default state and allowed transitions in the config() method of the abstract class. Start using states immediately via $model->state->transitionTo(...), and leverage methods defined on your state classes.
color(), label(), or isAllowedTo(...) on your state class to hide logic that changes per state (e.g., Paid::color() returns 'green').app/States/Payment/) to keep things organized; the package auto-discovers them if colocated with the abstract class.->allowTransition(...) in config() to define valid state changes—transitions throw exceptions if invalid unless you override onFailedTransition.registerStatesFromDirectory() over manual registration for maintainability across large projects.StateChanged event via stateChangedEvent() to integrate with notification or logging systems.HasStatesContract for precise IDE autocompletion and Psalm/PHPStan support.invoice_status, fulfillment_status) on the same model.::class; define public static $name on concrete states for human-readable DB values (e.g., 'paid'). However, don’t use hyphens in names—they conflict internally.registerStatesFromDirectory().protected $casts = ['state' => MyState::class] will cause the field to serialize as a string, breaking state behavior entirely.transitionTo() creates a new instance; access via $model->state always returns current state.tinyInteger or boolean—this package stores class names or custom names as strings.StateChanged event fires after persistence; if you hook into it, ensure your listeners don’t throw or assume existing state.State::from() or State::castFrom() in tests to instantiate states directly without DB round-trips (e.g., Pending::from($model)).How can I help you explore Laravel packages today?