Type-Safe Laravel Models with Native PHP Enums
Magic strings like 'pending' and 'paid' scattered through a codebase are a classic source of bugs. A typo passes silently, and there is no single place that documents the valid values. Backed enums, native to PHP since 8.1, solve this, and Eloquent casts them cleanly.
Define a Backed Enum
namespace App\Enums;
enum InvoiceStatus: string
{
case Draft = 'draft';
case Pending = 'pending';
case Paid = 'paid';
case Void = 'void';
}
Cast It on the Model
Point the model's cast at the enum class and Eloquent handles the conversion in both directions.
namespace App\Models;
use App\Enums\InvoiceStatus;
use Illuminate\Database\Eloquent\Model;
class Invoice extends Model
{
protected function casts(): array
{
return [
'status' => InvoiceStatus::class,
];
}
}
Now reads give you a real enum instance, and writes accept one:
$invoice = Invoice::find(1);
if ($invoice->status === InvoiceStatus::Paid) {
// ...
}
$invoice->status = InvoiceStatus::Void;
$invoice->save();
The database still stores the plain string 'void', but your PHP code works with a type the IDE and static analyzers understand.
Give Enums Behavior
Enums are more than a list of constants. They can hold methods, which is the perfect home for logic that used to live in scattered match statements or helper functions.
enum InvoiceStatus: string
{
case Draft = 'draft';
case Pending = 'pending';
case Paid = 'paid';
case Void = 'void';
public function label(): string
{
return ucfirst($this->value);
}
public function isFinal(): bool
{
return in_array($this, [self::Paid, self::Void], true);
}
public function color(): string
{
return match ($this) {
self::Draft => 'gray',
self::Pending => 'amber',
self::Paid => 'green',
self::Void => 'red',
};
}
}
In a Blade view this reads beautifully:
<span class="badge badge-{{ $invoice->status->color() }}">
{{ $invoice->status->label() }}
</span>
Validate Against the Enum
Laravel's Enum validation rule ensures incoming request data maps to a real case, so bad values never reach your model:
use Illuminate\Validation\Rules\Enum;
use App\Enums\InvoiceStatus;
$request->validate([
'status' => ['required', new Enum(InvoiceStatus::class)],
]);
Bonus: List All Cases
Backed enums expose a static cases() method, which is handy for building select menus without maintaining a separate array:
$options = collect(InvoiceStatus::cases())
->mapWithKeys(fn ($status) => [$status->value => $status->label()]);
Once you cast a column to an enum, entire categories of bugs disappear. There are no more typos in status strings, your editor autocompletes valid values, and related behavior lives next to the data it describes. It is one of the highest-value refactors you can make in an older Laravel codebase.
Further Reading
- Laravel documentation, "Eloquent: Mutators & Casting", Enum Casting (laravel.com/docs/13.x/eloquent-mutators#enum-casting)
- PHP Manual, "Enumerations" (php.net/manual/en/language.enumerations.php)