spatie/laravel-model-states
Add state and state machine behavior to Eloquent models. Represent each state as its own class, automatically cast and store states in the database, and define clean, safe transitions and state-specific behavior in your Laravel apps.
Install the package:
composer require spatie/laravel-model-states
Add the trait to your model:
use Spatie\ModelStates\HasStates;
class Payment extends Model
{
use HasStates;
}
Define a database column (e.g., state) and cast it:
protected $casts = [
'state' => PaymentState::class,
];
Create an abstract state class (e.g., PaymentState) and concrete states (e.g., Pending, Paid):
abstract class PaymentState extends State { ... }
class Pending extends PaymentState { ... }
Configure states in the abstract class:
public static function config(): StateConfig
{
return parent::config()
->default(Pending::class)
->allowTransition(Pending::class, Paid::class);
}
$payment = Payment::find(1);
$payment->state->transitionTo(Paid::class); // Valid transition
$payment->save(); // Persists state to DB
State Transitions
transitionTo() to move between states (enforced by allowTransition rules).Payment::find(1)->state->transitionTo(Paid::class).State-Aware Logic
class Paid extends PaymentState {
public function getDiscount(): float { return 0.1; }
}
$payment->state->getDiscount().Event Handling
StateChanged events (or custom events via stateChangedEvent):
$payment->state->transitionTo(Paid::class); // Fires StateChanged
Validation
canTransitionTo() to check allowed transitions:
if ($payment->state->canTransitionTo(Refunded::class)) { ... }
state column (e.g., string) to your table.$name for cleaner JSON:
$payment->state->name; // Returns 'paid' instead of '\App\States\Paid'
$payment->state->transitionTo(Paid::class);
$this->assertEquals('green', $payment->state->color());
State Resolution
public static $name = 'paid') must resolve back to classes.Circular Dependencies
A → B → A) unless explicitly allowed.Database Serialization
$name) in the DB. Ensure your casts match the column type.Event Timing
StateChanged events fire after the transition but before saving. Use saved() if you need post-save logic.allowTransition rules in StateConfig.registerStatesFromDirectory).$name properties (if used) are unique and valid.Custom Transitions
transitionTo() in a state class to add pre/post-transition logic:
public function transitionTo(Paid::class): void {
$this->model->applyDiscount();
parent::transitionTo(Paid::class);
}
Dynamic State Rules
canTransitionTo() to implement dynamic rules (e.g., time-based):
public function canTransitionTo(Refunded::class): bool {
return $this->model->created_at->lte(now()->subDays(30));
}
State Metadata
class Paid extends PaymentState {
public function __construct(public string $transactionId) {}
}
$payment->state->transactionId.config(['default_transition' => CustomTransition::class]).config() allows runtime changes.How can I help you explore Laravel packages today?