Replacing String Constants with PHP Backed Enums in Laravel Models
String constants in Eloquent models are a liability. One mistyped 'penidng' and your order flow silently breaks. PHP backed enums — available since PHP 8.1 — solve this, and Laravel's native enum casting means there's almost no migration cost.
The problem with string constants in Laravel models
Here's the pattern I still see in production codebases:
class Order extends Model
{
public const STATUS_PENDING = 'pending';
public const STATUS_PAID = 'paid';
public const STATUS_SHIPPED = 'shipped';
public const STATUS_CANCELLED = 'cancelled';
}
// Somewhere deep in a service class...
if ($order->status === Order::STATUS_PENDING) {
// ...
}
The problems stack up quickly. Constants aren't autocompleted by type — your IDE has no idea what values are valid for $order->status. You can't exhaustively check them with a match without risk of missed cases. And nothing stops a stray $order->status = 'paId' from slipping through.
PHP backed enums in Laravel models are the clean alternative: type-safe, IDE-friendly, and deeply integrated into Eloquent.
Defining a PHP backed enum
A string-backed enum maps each case to a string value. That value is what gets stored in and read from the database column:
// app/Enums/OrderStatus.php
namespace App\Enums;
enum OrderStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
// Optional: human-readable label for UI display
public function label(): string
{
return match($this) {
OrderStatus::Pending => 'Awaiting Payment',
OrderStatus::Paid => 'Payment Received',
OrderStatus::Shipped => 'Shipped',
OrderStatus::Cancelled => 'Cancelled',
};
}
// Optional: Tailwind colour class, status badge colour, etc.
public function color(): string
{
return match($this) {
OrderStatus::Pending => 'yellow',
OrderStatus::Paid => 'green',
OrderStatus::Shipped => 'blue',
OrderStatus::Cancelled => 'red',
};
}
}
Methods on enums are perfectly valid PHP. label() and color() are pure functions — no state, no side effects — and they keep display logic co-located with the type definition rather than scattered across Blade views.
Casting the model attribute with php backed enums laravel models
Casting in Laravel 12 is declared in the casts() method on the model. Point the attribute at your enum class:
// app/Models/Order.php
use App\Enums\OrderStatus;
class Order extends Model
{
protected function casts(): array
{
return [
'status' => OrderStatus::class,
];
}
}
Laravel handles the conversion in both directions automatically. Reading $order->status returns an OrderStatus instance. Assigning $order->status = OrderStatus::Paid and calling save() writes 'paid' to the database column. Your status database column stays a standard varchar — no schema change required if you're migrating from string constants.
$order = Order::find(1);
$order->status; // OrderStatus::Pending (enum instance)
$order->status->value; // 'pending' (the backed string value)
$order->status->label(); // 'Awaiting Payment'
$order->status->color(); // 'yellow'
// Assign and save
$order->status = OrderStatus::Paid;
$order->save(); // writes 'paid' to the DB
Querying by enum case
Eloquent accepts enum instances directly in query builder methods — it resolves the backed value automatically:
// Single status
Order::where('status', OrderStatus::Paid)->get();
// Multiple statuses
Order::whereIn('status', [OrderStatus::Paid, OrderStatus::Shipped])->get();
// Exclude a status
Order::where('status', '!=', OrderStatus::Cancelled)->get();
No more ->where('status', 'paid') string comparisons. Your IDE can autocomplete the cases, and a typo becomes a compile-time error rather than a silent query bug.
Validation with Rule::enum()
Laravel's Rule::enum() validates that an incoming value is a valid backed value for your enum class:
use Illuminate\Validation\Rules\Enum;
$request->validate([
'status' => ['required', new Enum(OrderStatus::class)],
]);
This accepts 'pending', 'paid', 'shipped', and 'cancelled' — the backed string values — and rejects everything else. If you want to restrict to a subset of cases, you can chain ->only():
// Only allow transitions to paid or cancelled from an API endpoint
$request->validate([
'status' => ['required', (new Enum(OrderStatus::class))->only([
OrderStatus::Paid,
OrderStatus::Cancelled,
])],
]);
API resources serialise enums automatically
When you return an Eloquent model with an enum cast inside an API resource, the enum serialises to its backed value with no extra work:
// app/Http/Resources/OrderResource.php
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'status' => $this->status, // → "paid" (backed string value)
'label' => $this->status->label(), // → "Payment Received"
'color' => $this->status->color(), // → "green"
];
}
The JSON output is clean and stable — the consumer gets the string value, not a serialised PHP object.
Gotchas and Edge Cases
from() throws, tryFrom() returns null. When parsing enum values from external input — a webhook payload, a CSV import, a legacy API — use tryFrom() rather than from(). OrderStatus::from('unknown') throws a ValueError. OrderStatus::tryFrom('unknown') returns null, which you can handle gracefully:
$status = OrderStatus::tryFrom($request->input('status'));
if ($status === null) {
// Handle unknown/unsupported status value
}
Null attribute values are not cast. If a status column is nullable and contains null, Laravel won't call the cast — $order->status will be null, not an enum instance. Guard appropriately:
$order->status?->label(); // Safe with null-safe operator
Database column stays as-is. When migrating from string constants, your varchar or string column values don't need to change — provided the existing values match the backed values on your enum cases exactly. If your old values were uppercase ('PENDING') and your enum uses lowercase, you'll need a data migration.
PHP 8.1 minimum. Backed enums require PHP 8.1. Laravel 12 requires PHP 8.2 as a minimum, so you're covered — but worth noting if you have any packages or shared code that targets older PHP versions.
Wrapping Up
Swapping string constants for PHP backed enums in your Laravel models is one of the higher-leverage refactors you can do for almost zero risk. The database schema doesn't change, the migration is a find-and-replace away, and you gain type safety, IDE autocomplete, and exhaustive pattern matching across your entire codebase. Start with the model that has the most status-checking logic and work outward from there.
Steven is a software engineer with a passion for building scalable web applications. He enjoys sharing his knowledge through articles and tutorials.