/ Content
Every PHP developer I know got excited about enums when they landed in 8.1. Then most of them wrote a Status enum with three cases, used it in one place, and moved on. I was guilty of this too. It took a few months of production code before I really started leaning into what enums can actually do, and they've changed how I model a lot of stuff.
I'll skip the "here's what an enum is" intro and get into the patterns that have actually been useful in my day-to-day work.
Backed Enums and Database Storage
If you're storing enum values in your database (and you should be), you need backed enums. I almost always use string-backed enums because they're human-readable when you're poking around in the database at 11 PM trying to figure out why an order is stuck.
enum OrderStatus: string
{
case Pending = 'pending';
case Processing = 'processing';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
}
The column in your migration should be a string, not an integer. I've seen people use integer-backed enums for "performance" and then struggle when they need to debug a query or hand off the database to someone else. The performance difference is negligible for 99.9% of applications. Save yourself the headache.
Your migration looks like this:
$table->string('status')->default(OrderStatus::Pending->value);
Some folks like to use the enum's value in the default. I prefer this over hardcoding the string because if you ever rename the backing value (rare, but it happens), your migration definitions stay accurate.
Eloquent Casts
Laravel's enum casting is where things start to feel really nice. Instead of comparing strings everywhere, you get type-safe comparisons throughout your codebase.
class Order extends Model
{
protected function casts(): array
{
return [
'status' => OrderStatus::class,
];
}
}
Now $order->status returns an OrderStatus instance, not a string. You can compare with ===, use match expressions, and your IDE actually knows what's going on. No more $order->status === 'pending' scattered across forty files.
The real benefit shows up in conditionals. Instead of string comparisons that can silently break when someone typos "pnding," you get actual type safety:
if ($order->status === OrderStatus::Pending) {
$order->beginProcessing();
}
Try to assign an invalid value and Laravel throws an exception immediately. That alone has caught bugs in my code that would've been silent failures with plain strings.
Implementing Interfaces on Enums
This is where it gets good. Enums can implement interfaces, which means you can define contracts that your enum cases must satisfy. I use this all the time for enums that need to present themselves in the UI.
interface HasLabel
{
public function label(): string;
}
enum OrderStatus: string implements HasLabel
{
case Pending = 'pending';
case Processing = 'processing';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
public function label(): string
{
return match ($this) {
self::Pending => 'Awaiting Review',
self::Processing => 'In Progress',
self::Shipped => 'On Its Way',
self::Delivered => 'Delivered',
self::Cancelled => 'Cancelled',
};
}
public function color(): string
{
return match ($this) {
self::Pending => 'yellow',
self::Processing => 'blue',
self::Shipped => 'indigo',
self::Delivered => 'green',
self::Cancelled => 'red',
};
}
}
Now in your Blade templates you can do $order->status->label() and $order->status->color(). No helper functions. No translation arrays. Everything lives with the enum where it belongs.
I once refactored a project that had a StatusHelper class with 200 lines of switch statements mapping statuses to labels, colors, icons, and descriptions. Replaced the whole thing with methods on the enum. The PR was a net negative line count and the code was way easier to follow.
Enums in Validation Rules
Laravel makes this really simple and I'm surprised more people don't use it. The Enum validation rule ensures the incoming value is a valid backing value for your enum:
use Illuminate\Validation\Rules\Enum;
public function rules(): array
{
return [
'status' => ['required', new Enum(OrderStatus::class)],
];
}
This catches invalid values at the validation layer instead of letting them hit your database or throw an unexpected error deeper in your code. I pair this with a subset check when I need to restrict which values are acceptable in a given context. For example, an admin might be able to set any status, but a customer can only cancel:
'status' => ['required', Rule::in([
OrderStatus::Cancelled->value,
])],
It's not fancy, but it's explicit about what's allowed where.
The State Machine Pattern
This is my favorite use of enums. Instead of scattering transition logic across controllers and services, you put it directly on the enum. Each case knows which states it can transition to.
enum OrderStatus: string
{
case Pending = 'pending';
case Processing = 'processing';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
public function canTransitionTo(self $next): bool
{
return match ($this) {
self::Pending => in_array($next, [self::Processing, self::Cancelled]),
self::Processing => in_array($next, [self::Shipped, self::Cancelled]),
self::Shipped => in_array($next, [self::Delivered]),
self::Delivered, self::Cancelled => false,
};
}
}
Then in your model or service:
public function transitionTo(OrderStatus $newStatus): void
{
if (! $this->status->canTransitionTo($newStatus)) {
throw new InvalidStatusTransition($this->status, $newStatus);
}
$this->update(['status' => $newStatus]);
}
This pattern has saved me from so many bugs. A delivered order can't go back to pending. A cancelled order can't suddenly be shipped. The rules are encoded in one place and enforced everywhere. Before enums, I'd use a dedicated state machine package, which is fine for complex workflows. But for most CRUD applications, this enum-based approach is more than enough and it keeps your dependency count low.
A Few Things I've Picked Up
Always add a values() helper method if you find yourself calling array_column(MyEnum::cases(), 'value') more than once. Enums are also great for config-driven behavior. I've used them to map notification channels, permission levels, and feature flags. And don't fight the type system. If a method needs an enum, type-hint the enum, not the string. Let PHP do the work.
Enums aren't just syntactic sugar over constants. They're a modeling tool. The state machine pattern alone has cleaned up half a dozen codebases for me. Give them a real shot beyond the basic Status with three cases.