sourceboat/laravel-enumeration
A Laravel package for defining and using enumerations in PHP, with support for validation rules, model casting, and comparing enum values in a clean, type-safe way across your application.
1. Install and confirm it loads
composer require sourceboat/laravel-enumeration
Laravel's package auto-discovery should register the service provider. If the package ships a config file, publish it:
php artisan vendor:publish --provider="Sourceboat\LaravelEnumeration\LaravelEnumerationServiceProvider"
Check php artisan list — if the package exposes a generator (commonly make:enumeration or enum:make), it will show up there.
2. Where to look first
vendor/sourceboat/laravel-enumeration/src/ — the base enumeration class is the thing you will extend for every enum in your app. Read it before reading the README examples; most of the magic lives in inherited static methods.eloquent/enumeration, so also skim vendor/eloquent/enumeration/src/ for the underlying Enumerable / Enumeration interfaces. If a method on your enum seems "missing", it is probably inherited from that parent library.toArray, values, keys, fromValue, etc.) — APIs of enumeration packages diverge in small but breaking ways, so copy signatures from the README rather than from memory of similar packages.3. First use case: replace a class-constant + array lookup
The canonical day-one win is a status column that is currently guarded by an array of strings used in three places (migration default, validation rule, Blade select). Define one enum and delete the other three:
namespace App\Enums;
use Sourceboat\LaravelEnumeration\Enumeration;
class UserStatus extends Enumeration
{
const ACTIVE = 'active';
const SUSPENDED = 'suspended';
const DELETED = 'deleted';
}
Then use it in a Rule::in, a <select>, and a model cast (see below). If that single refactor works end-to-end, the package is worth keeping; if it fights you, you have lost ten minutes.
Model casts. The main reason to pick a Laravel-specific enumeration over a bare PHP one is attribute casting. Check the README for whether the package uses a Laravel Castable class or a legacy getXAttribute accessor pattern — the API differs between package versions, and this is the single most version-sensitive piece of code you will write.
class User extends Model
{
protected $casts = [
'status' => UserStatus::class,
];
}
$user->status; // UserStatus instance (or constant value, depending on version)
UserStatus::ACTIVE; // "active"
Validation. Expose the values once and reuse:
'status' => ['required', Rule::in(UserStatus::values())],
Wrapping this in a custom rule (EnumExists(UserStatus::class)) pays off the moment you have ten enums; it keeps controllers from importing every enum class.
Presentation / localization. Keep human labels next to the machine values, not in Blade conditionals:
public static function labels(): array
{
return [
self::ACTIVE => __('Statuses.active'),
self::SUSPENDED => __('Statuses.suspended'),
self::DELETED => __('Statuses.deleted'),
];
}
If the base class already provides a label/description hook, override it rather than adding a parallel static method — two competing label APIs in one codebase is a common rot pattern.
Form / API plumbing. Add one small presenter layer so the enum never leaks into Blade directly:
// App\Support\EnumOptions::for(UserStatus::class) => [['value' => 'active', 'label' => 'Active'], ...]
Query scopes and query building. Because values are plain scalars, where('status', UserStatus::ACTIVE) works with no cast overhead. For multi-status filters, add a scope that accepts an array of enum instances and maps to ->value/getValue() in one place — that mapping call is exactly the kind of thing that changes between enum package versions, so isolate it.
Persistence / migration defaults. Use $table->string('status')->default(UserStatus::ACTIVE) (or ->default(UserStatus::ACTIVE->value) if the cast object isn't scalar-safe in your migration context). Keep enums as strings in the database; storing integer ordinals makes every future insert/delete reordering a data migration.
Testing. Add a single test that asserts values() equals the DB column's allowed set, and a factory state per enum value (->state(UserStatus::SUSPENDED)) so feature tests read like prose instead of magic strings.
Feature flags / state machines. Enumerations compose well with transition maps:
const TRANSITIONS = [
self::ACTIVE => [self::SUSPENDED, self::DELETED],
self::SUSPENDED => [self::ACTIVE, self::DELETED],
];
Keeping transition tables inside the enum (rather than in a service) makes canTransitionTo() discoverable and unit-testable without the container.
Verify the package is actually maintained before betting on it. It currently shows no stars and no dependents, which means: no community answers on Stack Overflow, no third-party blog posts to debug against, and a real chance the eloquent/enumeration base it wraps has moved on. Pin the version in composer.json, and read composer.lock — if it drags in a stale eloquent/enumeration, check that constraint against your PHP version. For a package this small, the fallback (bensampo/laravel-enum or a hand-rolled abstract class Enum) is a day of work; do not let it become an unmovable dependency in your domain layer.
Equality is the #1 source of bugs. Depending on the version, an enum attribute may come back as an object, a string, or a lazily-cast value. $user->status === UserStatus::ACTIVE will silently be false when $user->status is an object and UserStatus::ACTIVE is a string constant. Use the package's comparison helper (is(), equals()) if it provides one, and never use === on cast attributes until you've proven what type comes back with dd(get_debug_type($user->status)).
Serialization. $model->toArray() / API resources will emit whatever the cast produces. If it emits {} or the class name instead of "active", add #[Castable]/JsonSerializable handling or cast in the resource — test json_encode($user) explicitly rather than assuming.
Config and cache quirks. If the package publishes a config file, any cached config (php artisan config:cache) will mask edits during local debugging — the classic "my change does nothing" trap. Also re-run composer dump-autoload after generating enums if the generator writes files into a non-PSR-4 path.
Extension points worth using. The base class is the extension point: add labels(), colors(), toSelectArray(), random(), isValid() as traits applied to all your enums (App\Enums\Concerns\HasLabels). Do not fork the vendor class — one composer update erases it.
Do not add behaviour that belongs elsewhere. Enums are a good home for identity and static metadata (labels, transitions, allowed values) and a bad home for behaviour that needs dependencies (sending mail on status change, charging a card). The moment you want to inject a service into an enum, you want a service class.
Debugging checklist when an enum "doesn't work": (1) composer dump-autoload; (2) get_debug_type($value) to confirm object-vs-string; (3) confirm the cast key spelling matches the column; (4) confirm the constant value matches what's actually in the DB (a bad seeder writing 'Active' vs 'active' will fail lookup silently, returning null rather than throwing); (5) check whether the base library throws or returns null for unknown values — configure that deliberately, because it decides whether bad data surfaces as a 500 or a confusing null three layers deep.
How can I help you explore Laravel packages today?