sakanjo/laravel-easy-enum
Simplify PHP enums in Laravel with sakanjo/laravel-easy-enum. This lightweight package provides helpers for labels, values, options, validation rules, and Eloquent/Blade integration, reducing boilerplate and keeping enum-driven code clean and readable.
This is a thin convenience layer over PHP 8.1 backed enums — no service provider config, no migrations, no facade. Install it and you get a trait/base class that adds the helpers you'd otherwise rewrite in every project: values(), labels(), options(), toArray(), and an enum-aware validation rule.
composer require sakanjo/laravel-easy-enum
Before writing code, do two things:
vendor/sakanjo/laravel-easy-enum/src — it's a handful of files, and reading them takes less time than debugging a wrong assumption about method behaviour.First use case — kill magic strings in a model:
enum OrderStatus: string
{
use Sakanjo\EasyEnum\Concerns\HasEnum; // verify FQCN in README
case Pending = 'pending';
case Paid = 'paid';
case Cancelled = 'cancelled';
}
class Order extends Model
{
protected $casts = [
'status' => OrderStatus::class, // native Laravel cast, not package-specific
];
}
<select name="status">
@foreach (OrderStatus::options() as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
If that Blade snippet works, the package has already paid for itself. If it doesn't, check whether your version returns ['pending' => 'Pending'] or an array of ['value' => ..., 'label' => ...] — that's the single most common point of friction.
Keep native casts, use the package for presentation and validation. Eloquent's enum cast is first-party and well-tested; don't route it through a third-party trait. The package's value is the read-side helpers.
Validation. Laravel ships Illuminate\Validation\Rules\Enum since 9.x. Use it, and only reach for the package rule if it adds something you actually want (typically label-aware messages):
$request->validate([
'status' => ['required', Rule::enum(OrderStatus::class)],
]);
Query building. Always unwrap before hitting the query builder, and lean on values() for whereIn:
Order::whereIn('status', OrderStatus::values())->get();
// single value
Order::where('status', OrderStatus::Paid->value)->first();
State transitions belong on the enum, not the package. Add your own methods to the enum directly — this is where backed enums earn their keep:
public function canTransitionTo(self $next): bool
{
return match ($this) {
self::Pending => $next === self::Paid || $next === self::Cancelled,
self::Paid => $next === self::Cancelled,
self::Cancelled => false,
};
}
API responses. Laravel serialises a backed enum to its value inside toArray()/json(), so API resources need nothing extra. If you want labels in the payload, map explicitly — don't rely on the package's toArray() leaking into your API contract.
Filament / Nova / Livewire. options() is the natural feed for select fields. Wire it once in a form schema and reuse. Note that Livewire needs the enum cast on the model property to round-trip correctly.
Grouping related enums. Once you have three or four of these, add a small app/Enums convention and a base enum class of your own that uses the package trait, so a future swap is one file, not twenty.
options() returns empty, check you declared : string or : int.__toString(). It's tempting for Blade, but it breaks match, json_encode, and any code that expects ->value. Use ->value and ->name explicitly; let options() handle display.labels(), options(), values(), toArray() all sit on the trait. If you also define toArray() on the enum for a custom payload, you'll shadow the trait method and get confusing results. Pick one source of truth.name (PaidLate → Paid late or similar). The moment you need Paid late (overdue) or translations, stop using the derived helper and implement label(): string yourself with a match. Mixing both styles in one codebase is the main readability cost here.app/Support/ and you're done. Don't let it creep into your domain layer — keep it at the edges (views, form options, validation messages).options() is a real risk. "sakanjo/laravel-easy-enum": "^1.0" with a committed composer.lock is the sane default.dd(OrderStatus::cases(), OrderStatus::options()) in a Tinker session settles 90% of "the helper doesn't do what I expected" questions faster than reading the README twice.OrderStatus::options() keys cover a select is enough; you don't need coverage of the vendor trait itself.How can I help you explore Laravel packages today?