mad-web/laravel-enum
A Laravel package that provides a lightweight enum implementation with model attribute casting, validation rules, translations, and helper methods for working with predefined values throughout your application.
Install and publish (one-time):
composer require mad-web/laravel-enum
php artisan vendor:publish --provider="MadWeb\Enum\EnumServiceProvider"
Laravel's package auto-discovery registers the service provider, which in turn registers the make:enum Artisan command and the Eloquent cast. Publishing gives you the config and the lang/en/enums.php file used for labels. If you only need the enum class and cast, you can skip publishing and create the lang file yourself.
Look here first, in this order:
config/enum.php (if published) — namespace/root path for generated enums and label conventions.lang/{locale}/enums.php — the translation file the label helpers read from.MadWeb\Enum\Enum class in vendor/mad-web/laravel-enum/src — this tells you exactly which helpers (getValue(), getValues(), getLabel(), etc.) exist in the version you installed. Confirm names against the README; the package is low-activity so docs and code drift.MadWeb\Enum\Casts\EnumCast or similar) — you'll reference its FQCN in $casts.First use case (the canonical one): a status column on a model, rendered as a human label.
php artisan make:enum UserStatus
// app/Enums/UserStatus.php
namespace App\Enums;
use MadWeb\Enum\Enum;
class UserStatus extends Enum
{
const Active = 'active';
const Pending = 'pending';
const Disabled = 'disabled';
}
// app/Models/User.php
protected $casts = [
'status' => \App\Enums\UserStatus::class, // or explicit cast class: \MadWeb\Enum\Casts\EnumCast::class.':'.UserStatus::class
];
// lang/en/enums.php
return [
'user_status' => [
'active' => 'Active',
'pending' => 'Awaiting approval',
'disabled' => 'Disabled',
],
];
{{ $user->status->getLabel() }}
If getLabel() isn't the method your version ships, expose your own thin method on the enum (see Implementation Patterns) rather than sprinkling __() calls at the call sites.
Use the package cast in $casts so $model->status is an enum instance instead of a raw scalar. Nullable columns need the cast to respect null — verify your version does (older casts sometimes instantiate on null). Prefer explicit cast syntax in one place:
'status' => EnumCast::class . ':' . UserStatus::class,
Then guard reads with ?-> if the column is nullable, or add a default in the migration so it never is.
Expose the enum as a [value => label] map for Blade selects and for API-driven forms:
// app/Enums/UserStatus.php
public static function options(): array
{
return collect(self::getValues())
->mapWithKeys(fn ($v) => [$v => self::from($v)->getLabel()])
->all();
}
<select name="status">
@foreach (\App\Enums\UserStatus::options() as $value => $label)
<option value="{{ $value }}" @selected(old('status', $user->status?->getValue()) === $value)>
{{ $label }}
</option>
@endforeach
</select>
Keep validation tied to the enum, not a duplicated array of strings, so adding a case is a one-line change:
$request->validate([
'status' => ['required', Rule::in(UserStatus::getValues())],
]);
where clauses (this is where casting does not help)Query builder clauses are raw DB values. Pass ->getValue() or the constant, never the enum instance:
User::where('status', UserStatus::Active()->getValue())->get();
User::whereIn('status', [UserStatus::Active()->getValue(), UserStatus::Pending()->getValue()]);
Wrap common ones in a model scope so callers don't have to remember:
public function scopeActive(Builder $q): Builder
{
return $q->where('status', UserStatus::Active()->getValue());
}
Because the enum is a real class, put domain logic there instead of in controllers:
public function isFinal(): bool
{
return in_array($this->getValue(), [self::Disabled], true);
}
public function transitionsTo(self $next): bool
{
return match ($this->getValue()) {
self::Pending => $next->getValue() === self::Active,
self::Active => $next->getValue() === self::Disabled,
default => false,
};
}
is()/equals() (or === on getValue()), never on the object identity of two freshly constructed instances.'status' => UserStatus::Pending()->getValue() is the safe form. If the cast supports assignment of instances you can pass the instance directly, but the explicit value is version-proof.string('status')->default(UserStatus::Pending). Note that a constant in a migration is a code dependency; if you reorder enum values later, old migrations still run fine but new environments will use whatever the current constant is.Serialize the scalar, not the object, to keep payloads stable:
return [
'status' => $this->status?->getValue(),
'status_label' => $this->status?->getLabel(),
];
Keep label keys in lang/enums.php grouped by enum name; that way php artisan lang:publish and translation review flows pick them up. If you need per-locale labels, add lang/es/enums.php and let the package's label helper resolve through __(). Don't put fallback logic at call sites — centralize it in the enum so every screen behaves the same.
public static function labels(): array
{
return array_map(fn ($v) => self::from($v)->getLabel(), self::getValues());
}
public function equals(self $other): bool
{
return $this->getValue() === $other->getValue();
}
where/whereIn/orderBy. Always pass raw values to query clauses.$casts must match how the DB stores the value. String-backed enums on an integer column (or vice versa) will fail on read or silently hydrate differently. Decide once: string values are far more readable in a DB dump and less brittle to reordering, so prefer them unless you have a strong reason.enums.{enum}.{value} — a typo key or a renamed constant produces a raw-key fallback, not an exception. Add an in-app test that iterates getValues() and asserts getLabel() !== $value so missing translations fail CI.Active) and values ('active') are distinct; translation keys and DB values follow the value, not the constant name. Be consistent about camelCase constants and snake/string values.php artisan optimize:clear (or just config:clear + cache:clear) in local and CI, or you'll chase phantom "missing translation" bugs.equals()), not by object identity (===) between two independently created instances.UserStatus::Active()) confuse some static analyzers. Add @method static self Active() docblocks, or stick to constants + a small from() wrapper, so autocompletion and PHPStan keep working.__(), config reads) are hard to debug when triggered during container boot.composer.json ("mad-web/laravel-enum": "^X.Y") and check the changelog before major bumps. If the package is ever abandoned, your enum classes are mostly plain constants plus a base class; you can vendor a ~100-line Enum base into app/Enums and swap a single import to escape it.make:enum namespace/path is config-driven. If the generated class lands in the wrong namespace, fix config/enum.php and regenerate rather than editing the stub.getValue()/getLabel(), not on assertSame($enumInstance, ...) of a new instance. For model tests, use assertDatabaseHas('users', ['status' => UserStatus::Active()->getValue()]).Enum class (subclass it as an app-level base if you want shared helpers), the language file (translate, don't hardcode), and the cast class (extend it if you need e.g. null tolerance or JSON-array casting for multi-select enums). Custom casts are the safest place to add behaviour without forking the package.How can I help you explore Laravel packages today?