laravel-model-states: give Eloquent models a state machine
State support for models
At a glance
- What is it?
- spatie/laravel-model-states adds state support to Eloquent models by combining the state pattern with state machines, so each state is its own class with its own behaviour and the allowed transitions are declared in one place. It suits a Laravel application whose records move through a lifecycle, and it asks you to be comfortable with the state pattern before you start.
- Who is it for?
- laravel-model-states suits a Laravel team whose models have a real lifecycle, such as payments, orders or subscriptions, and who want the transition rules to live in code rather than in scattered conditionals. It does not suit a model with two values that behaves fine as a boolean, since a class per state is a lot of ceremony for that, and it does not suit a team that has not met the state pattern.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.
Star history
- spatie/laravel-model-states1,306 ★ · +2
| Repository | 2026-10-20 | 2026-11-07 |
|---|---|---|
| spatie/laravel-model-states | 1.3k | 1.3k |
2 Stars in the last 18 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
Each state is a class, not a string
The package adds state support to models, and the README is upfront about the design. It combines concepts from the state pattern and from state machines, and it recommends that you are familiar with both patterns if you are going to use the package. That recommendation is not a formality. The whole API assumes you already think in those terms.
The example is a Payment model with three possible states, Pending, Paid and Failed. The package lets you represent each state as a separate class, handles serialization of states to the database behind the scenes, and allows for easy state transitions. The motivation in the example is simple, the colour of a payment should differ depending on its state, and a class per state is where that behaviour lives.
The model itself is a normal Eloquent model with a trait and a cast.
use Spatie\ModelStates\HasStates;
class Payment extends Model
{
use HasStates;
protected $casts = [
'state' => PaymentState::class,
];
}The cast is the mechanism. A normal string column is cast to a state object, so the state is a first class value in your code rather than a string you compare against. The consequence is that the state can carry behaviour, and the database column does not have to change at all.
The abstract parent class holds the transition rules, which is the state machine half of the package.
use Spatie\ModelStates\State;
use Spatie\ModelStates\StateConfig;
abstract class PaymentState extends State
{
abstract public function color(): string;
public static function config(): StateConfig
{
return parent::config()
->default(Pending::class)
->allowTransition(Pending::class, Paid::class)
->allowTransition(Pending::class, Failed::class)
;
}
}A default state, and the list of transitions that are permitted. A concrete state then only has to implement its own behaviour.
class Paid extends PaymentState
{
public function color(): string
{
return 'green';
}
}And the usage reads as if the state were an object, not a column.
$payment = Payment::find(1);
$payment->state->transitionTo(Paid::class);
echo $payment->state->color();Installing the package and publishing the config
The package is a normal Composer dependency.
composer require spatie/laravel-model-statesThere is one configuration file, and it is published with an Artisan command.
php artisan vendor:publish --provider="Spatie\ModelStates\ModelStatesServiceProvider" --tag="model-states-config"The published file is short, and its single key is worth reading.
return [
/*
* The fully qualified class name of the default transition.
*/
'default_transition' => Spatie\ModelStates\DefaultTransition::class,
];The key sets the default transition class, which is the strategy used when a model is created without an explicit state. That is a small design decision with large consequences, and the fact that it is a single configurable class rather than a boolean flag means you can replace the whole behaviour of what happens on creation, for example to pick a default state based on the model's other attributes. The name DefaultTransition suggests that the package treats this as a strategy pattern point, and the config is where you swap it.
The full usage documentation is not in the README. The README points at the docs site for the version 2 introduction, which is the versioned documentation for the current major line. The repository also carries a docs/ directory, a config/ directory, a CHANGELOG.md, an UPGRADING.md for version transitions, a phpunit.xml.dist, and separate CI workflows for running the tests and for checking and fixing styling with php-cs-fixer.
Transitions are declared in one place and enforced at runtime
The part that makes this more than a typed enum is the allowTransition calls on the StateConfig. A transition that is not declared is not available, so the set of legal moves is a property of the model rather than a convention.
The design has a consequence that is worth being honest about, because it is the main trade-off. The rules live in PHP, in a static config method on the abstract state class. That means they are checked at runtime, when a transition is attempted, and not at compile time and not by the type system. A transition from Paid to Pending that was never allowed will not compile any differently from one that was allowed; it will fail when the code path runs. For most applications that is the right place for the check, since the legal transitions often depend on data that only exists at runtime. But it does mean the compiler is not helping you here, and a test that exercises every declared transition is what turns the state machine from documentation into a guarantee.
The state pattern side is what lets each state class hold behaviour. The abstract color() method in the example is a placeholder for whatever you want a state to know about itself, and a payment state class is a natural place for things like whether it is terminal, what transitions it should offer in a UI, or how it should be labelled. Because the cast returns a state instance, that behaviour is available anywhere the model is, including in a Blade view or a JSON API resource.
The README notes that the package handles serialization of states to the database behind the scenes, and that the default state is applied through the default transition. Both of those are worth checking against your own model, in particular if your state column already holds values that are not class names, or if you need a state that is not one of the declared classes.
Where laravel-model-states is the wrong tool
Three cases argue against it. The first is a field that has two values and no lifecycle. A boolean column called paid, or a status column with two options, does not need a class per state and a transition table. The ceremony of an abstract parent, three subclasses and a config method is real, and if the state cannot change in interesting ways the package reduces to an enum with extra steps.
The second case is a team that has not worked with the state pattern. The README says plainly that it is recommended you are familiar with both the state pattern and state machines, and the API will make sense to you only if you do. The names in the API, State, StateConfig, DefaultTransition, transitionTo, are pattern vocabulary, and a team meeting them for the first time will spend its first day reading rather than building.
The third case is a workflow whose legal transitions depend on things outside the model. The configuration is static, declared once on the abstract class, and the same rules apply to every instance of the model. If one order can go from Pending to Paid but another cannot, because the amount or the customer tier forbids it, that rule does not fit this package and belongs in a domain service that checks before calling transitionTo.
There is a broader point about what this package does and does not give you. It gives you typed states, declared transitions, and a cast. It does not give you a state history, a trail of who changed a state and when, a UI for choosing transitions, or anything about persistence of the transition log. If the audit trail matters, you need to record it yourself in the transition code path, and the readme says nothing about a hook for that, so it is a question to ask rather than a feature to rely on.
On maintenance, the release history shows 2.14.0 in April 2026, 2.14.1 later that month and 2.14.2 on 2026-07-22, and the last push was on the same day. The 2.x line has been stable for a while, the package is MIT licensed, and the documentation is versioned, with the introduction page for version 2 linked from the README.
Editorial conclusion
laravel-model-states suits a Laravel team whose models have a real lifecycle, such as payments, orders or subscriptions, and who want the transition rules to live in code rather than in scattered conditionals. It does not suit a model with two values that behaves fine as a boolean, since a class per state is a lot of ceremony for that, and it does not suit a team that has not met the state pattern. Before you adopt it, read the version 2 introduction in the documentation, and note that the last push was on 2026-07-22 with the newest release 2.14.2 from the same day.
Frequently asked questions
How do I install laravel-model-states?
Install it with composer require spatie/laravel-model-states, then publish the config with php artisan vendor:publish --provider="Spatie\ModelStates\ModelStatesServiceProvider" --tag="model-states-config".
How are state transitions defined?
In a static config method on the abstract state class, using a StateConfig built from parent::config(), where default() sets the initial state and allowTransition() declares each permitted move, for example from Pending to Paid and from Pending to Failed.
Do I need to know the state pattern to use this package?
The README recommends that you are familiar with both the state pattern and state machines if you are going to use the package, since it combines concepts from both.
What does the published config file control?
It has a single key, default_transition, set to Spatie\ModelStates\DefaultTransition::class, which is the fully qualified class name of the default transition used when a model is created without an explicit state.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/spatie-laravel-model-states)Star history chart for your README
[](https://hysenlabs.com/projects/spatie-laravel-model-states)Was this useful?