Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Backed Enums Laravel Package

webfox/laravel-backed-enums

Adds Laravel-friendly helpers to PHP backed enums: generate labels and option lists, validate enum values, and use them cleanly in forms, APIs, and application logic. A lightweight way to make backed enums more practical in Laravel.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm your baseline. The package targets PHP 8.1+ backed enums and Laravel's container/translation layers, so check composer show webfox/laravel-backed-enums after installing and glance at the tag list — at 44 stars and a 2026-04 release, pin the minor ("^1.0" or whatever range the README declares) rather than tracking dev-main.

composer require webfox/laravel-backed-enums

If the README mentions a publishable config or translation stub, run the publish command once and commit the result so every developer and CI environment resolves labels identically:

php artisan vendor:publish --tag=backed-enums

Where to look first. Read the package's trait list before writing any code — the whole value proposition is compositional traits (HasLabel, HasLocalization, HasDescription, HasColor/HasIcon, comparison helpers, default-case support). Skim the README's trait table, then open vendor/webfox/laravel-backed-enums/src and read the two traits you actually intend to use. They are small; ten minutes there answers every "what does label() fall back to?" question that a README skim won't.

First use case: a status column. Take an existing string column and back it with an enum.

namespace App\Enums;

use Webfox\LaravelBackedEnums\HasLabel;

enum OrderStatus: string
{
    use HasLabel;

    case Pending   = 'pending';
    case Paid      = 'paid';
    case Shipped   = 'shipped';
    case Cancelled = 'cancelled';

    public function label(): string
    {
        return match ($this) {
            self::Pending   => 'Awaiting payment',
            self::Paid      => 'Paid',
            self::Shipped   => 'Shipped',
            self::Cancelled => 'Cancelled',
        };
    }
}

Then wire it into a model, a migration ($table->string('status')->default(OrderStatus::Pending->value) — never a native enum() column), a cast, and a Blade select in one pass:

protected $casts = ['status' => OrderStatus::class];
<select name="status">
    @foreach (OrderStatus::cases() as $case)
        <option value="{{ $case->value }}" @selected($order->status === $case)>
            {{ $case->label() }}
        </option>
    @endforeach
</select>

That single loop is the mental model for the rest of the package: value is for the database and the wire, label() is for humans, and everything else is sugar around that split.

Implementation Patterns

Two-tier enums: domain vs. presentation. Keep the raw backed enum on the model, and let labels live either in a match (simple, static-analysis friendly, no translation loading cost) or in translations (needed the moment a second locale appears). A useful convention that scales: domain enums carry only HasLabel + comparison traits; UI-heavy enums (colors, icons, ordering) are separate enums or a small presenter class. Mixing HasColor into OrderStatus because one dashboard needs a badge color is how enums become dumping grounds.

Localization as the default for anything user-facing. Once the second locale lands, a match statement becomes a merge conflict. Put labels in lang/{locale}/ files and let HasLocalization resolve them. The package derives a translation key from the enum class and case name — the exact convention (class basename, kebab/snake transformation, nested keys) is the one thing worth verifying empirically before you commit a hundred keys:

php artisan tinker
>>> App\Enums\OrderStatus::Pending->label()

Then find that exact string in lang/ and match its shape. Do this before authoring translation files, not after — a wrong key convention silently falls back and you find out in production.

options()-style maps for forms, filters, and admin panels. Whatever the package calls the value→label map (options(), select(), toArray()), it exists so you never hand-roll array_column. Use it for Blade selects, Livewire/Filament/Nova option arrays, and query-string filters:

Select::make('status')->options(OrderStatus::options());

Wrap it in a Blade component (<x-enum-select :enum="OrderStatus::class" />) so every dropdown in the app behaves the same way.

Validation: Rule::enum() plus a values guard. Laravel's native Rule::enum(OrderStatus::class) covers inbound requests. Add the package's value-list helper only where you need to validate against a subset or generate a JS-side allowlist:

$request->validate([
    'status' => ['required', Rule::enum(OrderStatus::class)],
]);

// For a "transition allowed" guard
if (! $order->status->in(OrderStatus::Paid, OrderStatus::Shipped)) { abort(422); }

Comparison helpers in policies and state machines. Use is(), isNot(), in(), notIn() instead of ===/in_array($x, [...], true) — the win is readability in guard clauses and consistency when someone later swaps the enum for a richer object:

public function cancel(User $user, Order $order): bool
{
    return $user->is($order->user) && $order->status->is(OrderStatus::Pending);
}

Type-hint against the package's enum interface (if it ships one) rather than the concrete enum in shared helpers, so generic code can accept any backed enum:

function labelFor(\Webfox\LaravelBackedEnums\BackedEnum $case): string { return $case->label(); }

Default case for new models. Rather than sprinkling OrderStatus::Pending through controllers, mark a default case with the package's default attribute/static (check which your version supports) and read it once in the model:

protected $attributes = ['status' => OrderStatus::Pending->value];

// or, if the package exposes it:
$order->status ??= OrderStatus::default();

Keep the migration default and the enum default in sync — they're two separate truths and drift is silent.

Testing pattern. A single data provider over Enum::cases() catches the two failures that actually happen: a missing translation key and a match arm that throws UnhandledMatchError.

it('has a label for every case', function (OrderStatus $status) {
    expect($status->label())->toBeString()->not->toBeEmpty()
        ->not->toContain('enums.');
})->with(OrderStatus::cases());

Run that with the locale forced to each supported language, since label() resolves through the translator.

Integration tips. Cast enums in API resources deliberately: ->value in JSON, label() only in admin/HTML surfaces. In Filament/Nova, enum casts plus options() give you badges and filters nearly for free. In seeders and factories, use the enum (OrderStatus::Paid) rather than the raw string — it keeps refactors honest and lets grep find usages.

Gotchas and Tips

Translation caching bites immediately. Laravel caches compiled translations; after adding or renaming keys, run php artisan optimize:clear (or at minimum php artisan view:clear) before concluding the package is broken. In CI, a stale bootstrap/cache is the usual culprit for "works locally, fails in the pipeline."

Missing keys may render the raw key. Depending on fallback behavior, an unresolved label surfaces as enums.order_status.pending in your UI. That's a better failure mode than a silent empty string, but it means the "no key" test above is mandatory, not optional.

Locale state vs. static memoization. If labels are cached in a static array for performance, a locale switch mid-request (or an Octane worker handling two locales) can serve stale English to a French user. Either don't cache, or key the cache by app()->getLocale() and clear it in a test setUp().

Trait method collisions. Adding HasLabel to an enum that already declares label() is fine in PHP but produces confusing precedence; adding two traits that both define label() is a fatal error. Decide once: match or trait, not both, in the same enum.

toArray()/toJson() leaking into APIs. If the enum implements JsonSerializable via a package trait, response()->json($order) may emit a label or an object where clients expect a string. Assert the serialized shape in a feature test before shipping.

Never make a native enum() DB column. Use string() with an application-level default. Adding a case in 2027 then requires a migration on MySQL and is awkward on Postgres; the enum belongs in PHP, the column is a string.

Comparison subtleties. is() is null-safe-ish and reads better, but in() with an empty array is silently false — fine, but don't use it as a validation substitute. And remember $status->value ?? null fails for a null enum; guard with $status?->value.

Attribute-based overrides have precedence. If the package supports per-case attributes (e.g. a #[Label]), know the resolution order (attribute → translation → method → case name, or whatever the README states) before you debug why one case ignores its translation.

Static analysis friction. PHPStan/Psalm will flag label() on a plain \BackedEnum. Annotate with the package interface and add @method docblocks on magic statics like options() if the package relies on __callStatic.

Extension point: your own trait. Rather than editing vendor code, write app/Enums/Concerns/HasBadge.php that calls $this->label() and $this->color() and composes with the package traits — that's the intended seam and it survives upgrades.

Small-package risk management. With 44 stars and a single maintainer, treat the package as a dependency with a real bus factor: keep vendor-specific calls behind one or two app-level helpers (enum_label($case)), so replacing the package with 40 lines of your own trait is an afternoon rather than a refactor. Read the diff before bumping the minor version, and pin in composer.json.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin