# laravel-model-status: a status history for Eloquent models

> spatie/laravel-model-status gives an Eloquent model a HasStatuses trait, and the point is that a status is not a single column. Every change is stored as a row with its own reason, so the package can answer what the current status is, what it was before, and why it changed, and it can filter your models by their latest status.

**spatie/laravel-model-status** — Easily add statuses to your models

- Repository: https://github.com/spatie/laravel-model-status
- Website: https://freek.dev/973-a-package-to-assign-statuses-to-eloquent-models
- Stars: 1,056 · Forks: 83
- Language: PHP
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/spatie-laravel-model-status

## A column is not enough once the status changes

Imagine you want to have an Eloquent model hold a status. The README opens by saying it is easily solved by just adding a status field to that model and being done with it. But in case you need a history of status changes, or need to store some extra info on why a status changed, just adding a single field will not cut it.

That is the whole premise, and it is a familiar problem. A moderation queue, an order that moves through states, a subscription that is trialling and then active and then cancelled, a document that is draft and then in review and then approved: in each case the interesting questions are historical, and a column can only hold the present.

The package provides a HasStatuses trait that, once installed on a model, lets you do this:

```php
// set a status
$model->setStatus('pending', 'needs verification');

// set a status using an enum
$model->setStatus(UserStatus::pending);

// set another status
$model->setStatus('accepted');

// specify a reason
$model->setStatus('rejected', 'My rejection reason');

// get the current status
$model->status(); // returns an instance of \Spatie\ModelStatus\Status

// get the previous status
$latestPendingStatus = $model->latestStatus('pending');

$latestPendingStatus->reason; // returns 'needs verification'
```

The reason argument is the differentiator. It is stored alongside the status name, so the history is not just a list of states but a list of decisions, which is what you need when somebody asks why an application was rejected.

The package is MIT licensed, published on Packagist as spatie/laravel-model-status, and the primary language is PHP. The homepage is a blog post by the author on freek.dev about a package to assign statuses to Eloquent models.

## Setting a status, with a reason and with an enum

The setting API is small, and the reason is an optional second argument:

```php
$model->setStatus('status-name');
```

```php
$model->setStatus('status-name', 'optional reason');
```

Enums are accepted too, and there are two methods for it, setStatus and forceSetStatus:

```php
enum UserStatus: string
{
    case pending = 'pending';
    case accepted = 'accepted';
}

$model->setStatus(UserStatus::pending);
```

The storage rule is stated plainly: backed enums are stored using their value, and unit enums are stored using their case name. That is the difference that matters for a database, because a backed enum gives you a stable value that you can change the case name of without migrating data, whereas a unit enum ties the stored string to the case name.

That is also why forceSetStatus exists separately from setStatus, and it comes up in the next section: the two methods differ in whether the enum restriction applies.

The general calling convention is worth noting, because it is looser than it looks. The status name is a plain string, not a constant, so the package does not require you to define your statuses anywhere. You can use whatever strings your domain uses, and the reason field takes any string as well.

## Restricting the statuses a model accepts

You can restrict which statuses a model accepts by overriding the statusEnumClass method:

```php
class YourEloquentModel extends Model
{
    use HasStatuses;

    public function statusEnumClass(): ?string
    {
        return UserStatus::class;
    }
}
```

When statusEnumClass() returns an enum class, setStatus(...) only accepts statuses that belong to that enum. forceSetStatus(...) always bypasses this validation.

So the enum is not only a typing convenience at the call site, it is a validation rule for the model. Once a model declares that its statuses come from UserStatus, a typo in a status name fails at the point of the call rather than becoming a row in the database that nothing will ever match.

The escape hatch matters as much as the rule. There are legitimate reasons to set a status that is not in the enum, an import from an older system, a migration script, a bulk correction, and forceSetStatus is there so those cases do not require reaching into the package internals.

The method returns a nullable string, so a model can simply not declare an enum, and then the restriction does not apply and any string is accepted.

## Reading statuses back, six different ways

The retrieval API is where the package earns its keep, because there are several shapes of the same question and they are all answered.

The property and the method differ, and the difference matters. The status property returns a string with the name of the latest status, while the status() method returns the latest instance of Spatie\ModelStatus\Status, which is the object that carries the reason. latestStatus() is equivalent to status(). And statusEnum() returns the latest enum case when statusEnumClass() is configured, otherwise null, and it also returns null when there is no status yet, or when the latest stored status does not map to the configured enum.

```php
$model->status; // returns a string with the name of the latest status
$model->status(); // returns the latest instance of `Spatie\ModelStatus\Status`
$model->latestStatus(); // equivalent to `$model->status()`
$model->statusEnum(); // returns the latest enum case when statusEnumClass() is configured, otherwise null
```

For a specific status name, latestStatus('pending') returns the Status instance with that name, and the examples show that it accepts an array of names and returns the latest of status 1 or status 2, whichever is later, either as latestStatus(['status 1', 'status 2']) or as latestStatus('status 1', 'status 2').

getStatusNames() returns a collection of all available status names for the model, which is what you use to populate a dropdown.

Two more accessors cover the collection rather than the current value. The statuses relation returns all associated statuses of a model, and hasStatus('status1') returns a boolean, true or false, for whether the model has that status.

## Querying models by their latest status

Two query scopes do the filtering, and the second one is the one with a subtlety worth reading.

The currentStatus scope returns models that have a status with the given name:

```php
$allPendingModels = Model::currentStatus('pending');

//or array of statuses
$allPendingModels = Model::currentStatus(['pending', 'initiated']);
$allPendingModels = Model::currentStatus('pending', 'initiated');
```

The otherCurrentStatus scope returns all models that do not have a status with the given name, including any model that does not have any statuses associated with them:

```php
$allNonPendingModels = Model::otherCurrentStatus('pending');
$allNonInitiatedOrPendingModels = Model::otherCurrentStatus(['initiated', 'pending']);
$allNonInitiatedOrPendingModels = Model::otherCurrentStatus('initiated', 'pending');
```

That inclusion clause is the subtlety. A model that has never had a status at all is not pending, and a naive implementation of a not-pending filter would exclude it. Whether you want that depends on your domain, and the package has chosen the inclusive behaviour.

Both scopes accept a single name, an array of names to include, or several names as separate arguments, which covers the cases where you are filtering on one state and the cases where a record in any of several states should match.

## Installation, the migration and the two config keys

The package installs through Composer:

```bash
composer require spatie/laravel-model-status
```

The migration has to be published, and the command names the service provider and the tag:

```bash
php artisan vendor:publish --provider="Spatie\ModelStatus\ModelStatusServiceProvider" --tag="migrations"
```

Then the table is created:

```bash
php artisan migrate
```

And optionally the config file, published with the config tag:

```bash
php artisan vendor:publish --provider="Spatie\ModelStatus\ModelStatusServiceProvider" --tag="config"
```

The published file is config/model-status.php, and it holds two settings:

```php
return [

    /*
     * The class name of the status model that holds all statuses.
     *
     * The model must be or extend `Spatie\ModelStatus\Status`.
     */
    'status_model' => Spatie\ModelStatus\Status::class,

    /*
     * The name of the column which holds the ID of the model related to the statuses.
     *
     * You can change this value if you have set a different name in the migration for the statuses table.
     */
    'model_primary_key_attribute' => 'model_id',

];
```

Both keys exist for adoption reasons rather than aesthetics. The status_model key lets you replace the Status model with your own, as long as it is or extends Spatie\ModelStatus\Status, which is how you add columns of your own to the statuses table. The model_primary_key_attribute key is the foreign key column on the statuses table, and the comment says you can change it if you used a different name in the migration.

That second key is the one to think about before you install into an existing database with a statuses table, since the package's migration creates the table and you may need to reconcile the column name.

## Conclusion

Adopt laravel-model-status if your models have a lifecycle, because a status that changes over time with a reason attached does not fit in a single column, and the package gives you the history, the reasons and the query scopes in one trait. Do not adopt it for a boolean flag, since a single column is simpler and cheaper for a two-state model, and mind the storage cost, since every status change is a row. Before you start, decide whether you need the enum restriction, because overriding statusEnumClass changes what setStatus accepts, and note that the status model and the foreign key column name are configurable, which matters if you are introducing the package into a database that already has a statuses table.

## FAQ

### Why would I use laravel-model-status instead of a status column?

Because a single field only holds the present. The package stores every status change as a row with its own optional reason, so you can get the current status, the previous one, the reason it changed, and filter models by their latest status.

### How do I install spatie/laravel-model-status?

Run composer require spatie/laravel-model-status, publish the migration with php artisan vendor:publish --provider="Spatie\ModelStatus\ModelStatusServiceProvider" --tag="migrations", run php artisan migrate, and optionally publish the config file with the config tag.

### How do I restrict which statuses a model accepts?

Override the statusEnumClass method on the model and return your enum class. setStatus then only accepts statuses that belong to that enum, while forceSetStatus always bypasses the validation.

### How do I find all models whose latest status is pending?

Use the currentStatus scope, which accepts a single name, an array of names or several names as arguments. The otherCurrentStatus scope returns models without that status, and it also includes models that have no statuses at all.

### What can I configure in laravel-model-status?

The published config/model-status.php has two keys: status_model, the class that holds all statuses, which must be or extend Spatie\ModelStatus\Status, and model_primary_key_attribute, the column on the statuses table holding the related model ID, which you can change if you named it differently in the migration.

## Sources

- [License: MIT](https://github.com/spatie/laravel-model-status/blob/main/LICENSE)
- [Project website](https://freek.dev/973-a-package-to-assign-statuses-to-eloquent-models)
- [README](https://github.com/spatie/laravel-model-status/blob/main/README.md)
- [Releases](https://github.com/spatie/laravel-model-status/releases)
- [spatie/laravel-model-status on GitHub](https://github.com/spatie/laravel-model-status)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/spatie-laravel-model-status
