foxbytehq/laravel-backed-enums
A Laravel package that adds handy helpers to PHP backed enums, including human-readable labels, validation rules, select options, comparisons, and Eloquent-friendly utilities so you can use enums confidently across forms, requests, and models.
Install
composer require foxbytehq/laravel-backed-enums
The package is auto-discovered; there is no service provider to register by hand. If the release ships a config file, publish it once:
php artisan vendor:publish --tag="backed-enums-config"
Where to look first
src/ — the traits are small and readable. Skim them before adopting, especially the comparison and translation traits, so you know exactly what is()/label() normalize.tests/ — the package's own test suite is the best executable documentation for edge cases (raw values vs. cases, missing translations).First use case — a status enum on a model
namespace App\Enums;
use Foxbytehq\BackedEnums\Traits\BackedEnum; // confirm trait name in README
enum OrderStatus: string
{
use BackedEnum;
case Pending = 'pending';
case Paid = 'paid';
case Refunded = 'refunded';
}
Cast it on the model:
protected $casts = [
'status' => OrderStatus::class,
];
Add translations (key layout is configurable; the default is typically enums.{enum}.{value}):
// lang/en/enums.php
return [
'order_status' => [
'pending' => 'Awaiting payment',
'paid' => 'Paid',
'refunded' => 'Refunded',
],
];
Then in a Blade view:
<span class="badge">{{ $order->status->label() }}</span>
That is the 10-minute win: no more match statements or __('...') chains in every view, and the label follows app()->setLocale() for free.
1. Rich domain comparisons instead of ===
The value of the comparison trait is readability at call sites and tolerance for raw input:
// before
if ($order->status === OrderStatus::Paid || $order->status === OrderStatus::Refunded) { ... }
// after
if ($order->status->in(OrderStatus::Paid, OrderStatus::Refunded)) { ... }
Use is() / isNot() in policies, guards, and listeners; use in() / notIn() when branching on several states. Keep the enum as the single place that encodes "which states count as settled" — put it behind a helper method on the enum itself so the rule lives in one file:
public function isSettled(): bool
{
return $this->in(self::Paid, self::Refunded);
}
2. API resources and Inertia props
Don't leak raw enum objects to the client — shape them explicitly:
return [
'status' => $order->status->value,
'status_label'=> $order->status->label(),
];
For select inputs, build options once and cache the array per request:
public function options(): array
{
return collect(OrderStatus::cases())
->map(fn ($c) => ['value' => $c->value, 'label' => $c->label()])
->all();
}
If the package exposes options() / toArray() / values(), prefer those over hand-rolled collect(cases()) loops, but verify the shape it returns — toArray() typically yields value => label, which is exactly what Form::select or a <select> loop wants.
3. Validation
Pair the enum with Laravel's enum rule and the package's value list:
$request->validate([
'status' => ['required', Rule::enum(OrderStatus::class)],
]);
For a "filter by any of these" query string, validate against values() so a new case is accepted automatically:
'statuses' => ['array'],
'statuses.*' => [Rule::in(OrderStatus::values())],
This is the pattern that pays for itself: adding a case to the enum propagates to validation, select boxes, and translations without touching controllers.
4. Localized output at request time
Resolve labels inside the request/response cycle, never at boot or in a cached static. label() reading __() at call time means the same enum serialises correctly for different Accept-Language headers — good for multi-locale APIs, but it also means you must not memoize labels in a long-running singleton if you switch locales per request.
5. Testing
Factory states read better with enums:
Order::factory()->create(['status' => OrderStatus::Paid]);
$this->assertDatabaseHas('orders', ['status' => OrderStatus::Paid->value]);
Assert labels explicitly in feature tests when the label is user-facing copy:
$this->get('/orders')->assertSee(OrderStatus::Paid->label());
This catches missing translation keys before production — a missing key usually renders the key string itself, which is easy to miss in review.
6. Extension points
Enums can carry their own methods alongside the package traits, so put app-specific behavior (colors, icons, transition maps) directly on the enum rather than in a helper. If you need shared behavior across many enums (e.g. a color() with a sensible default), define App\Enums\Concerns\HasColor and use it alongside the package trait.
composer show foxbytehq/laravel-backed-enums and read that tag's README; don't copy snippets from an older blog post.label() starts returning something unexpected after you "just added a helper," check for a name collision. Don't shadow label(), is(), in(), values().is() with raw input. Comparison against a request string works only if the package normalizes the type. For an int-backed enum, $case->is($request->input('status')) compares an int to a string. Cast at the boundary ((int) $request->input(...)) or use OrderStatus::tryFrom($value) and compare cases.php artisan optimize:clear on deploy). In tests, missing keys render the key path — search rendered HTML for enums. to catch regressions.defaultLabel() or a match fallback in a wrapper trait.toArray(). Backed enums serialise to their scalar value in json_encode/response()->json(); the localized label is not included automatically. Always build API payloads explicitly.where('status', OrderStatus::Paid) works; whereIn('status', $cases) needs OrderStatus::values() (or array_column($cases, 'value')). Don't pass an array of enum objects to the query builder and hope.string/integer columns, not native ENUM columns. Adding a case should be a code change only; a DB ENUM turns it into a migration and a locking schema change.values() or options() without docblocks. Add @return list<string> style annotations in a local wrapper trait to keep level 6+ clean.app() inside enum methods works but makes unit tests fragile. Keep enum methods pure — pass dependencies in.php artisan tinker (OrderStatus::Paid->label()) after app()->setLocale('fr'). That isolates "translation missing" from "trait not applied" from "wrong locale in the request."How can I help you explore Laravel packages today?