spatie/laravel-model-status
Add status history to any Eloquent model with Spatie’s HasStatuses trait. Set statuses (strings or enums), store reasons/extra info, and retrieve current or previous statuses via convenient helpers like status() and latestStatus().
composer require spatie/laravel-model-status.php artisan vendor:publish --provider="Spatie\ModelStatus\ModelStatusServiceProvider" --tag="migrations" followed by php artisan migrate.HasStatuses trait to any Eloquent model needing status tracking:
use Spatie\ModelStatus\HasStatuses;
class Order extends Model
{
use HasStatuses;
}
$order->setStatus('pending');
$currentStatus = $order->status(); // Returns \Spatie\ModelStatus\Status instance
setStatus() for type-safe status transitions:
enum OrderStatus: string
{
case Pending = 'pending';
case Processing = 'processing';
case Shipped = 'shipped';
}
$order->setStatus(OrderStatus::Processing);
And enforce status constraints by implementing statusEnumClass():
public function statusEnumClass(): ?string
{
return OrderStatus::class;
}
$order->setStatus('shipped', 'Courier: Fedex, tracking #FX123');
// Get all pending orders
Order::currentStatus('pending')->get();
// Exclude cancelled or delivered orders
Order::otherCurrentStatus(['cancelled', 'delivered'])->get();
$order->hasStatus('pending') // true only if latest status matches
$order->hasEverHadStatus('shipped') // true if *any* history contains shipped
$order->hasNeverHadStatus('delivered') // useful for onboarding new flows
StatusUpdated to trigger notifications, logs, or external syncs:
Event::listen(function (StatusUpdated $event) {
if ($event->newStatus->name === 'shipped') {
dispatch(new ShipOrderJob($event->model));
}
});
status vs status(): $model->status returns the name (string); $model->status() returns the full Status object (with reason, created_at, etc.). Use the method for inspection.forceSetStatus() skips isValidStatus() checks entirely — only use in seeders, migrations, or admin actions.statusEnum() returns null if no statuses exist, or if the latest status name doesn’t map to the configured enum.value, unit enums use name. Avoid renames without migration strategy.setStatus() repeatedly in loops. Use statuses() for iteration over large batches.statuses table (e.g., rename model_id), update model_primary_key_attribute in config/model-status.php.deleteStatus('name') removes all instances of that status, not just the latest. Use with caution in audit-sensitive apps.StatusUpdated event to test status transition logic without DB writes:
Event::fake();
$order->setStatus('shipped');
Event::assertDispatched(StatusUpdated::class, fn ($e) => $e->newStatus->name === 'shipped');
How can I help you explore Laravel packages today?