datomatic/nova-enum-field
A Laravel Nova field for working with PHP enums—display them as badges or labels, edit via select inputs, and filter Nova resources by enum values. Supports backed and pure enums, nullable values, and customizable colors for clean, type-safe admin panels.
Install, then verify compatibility before you write a line of code.
composer require datomatic/nova-enum-field
Two gates matter more than the feature list:
composer.json constraint against your installed Nova major (4 vs. 5) before installing. This is the single question that decides whether the package is usable in your app.Pin ^1.11.1 or later. That tag fixes exception #8, thrown when EnumFilter / EnumBooleanFilter is applied to a resource index rendered through a relation (e.g. an "Orders" panel on a customer detail page). Most admin panels eventually hit that path.
Define a backed enum that implements HasLabel.
The field needs a string or int backing value, and labels come from Nova's own contract — not from a package-specific interface:
use Laravel\Nova\Fields\HasLabel;
enum Status: string implements HasLabel
{
case Draft = 'draft';
case Published = 'published';
public function label(): string
{
return match ($this) {
self::Draft => 'Draft',
self::Published => 'Published',
};
}
}
Wire it into a resource and its filter in the same commit.
use Datomatic\NovaEnumField\Enum;
use Illuminate\Validation\Rule;
Enum::make('Status')
->enum(Status::class)
->rules('required', Rule::enum(Status::class));
use Datomatic\NovaEnumField\EnumFilter;
public function filters(Request $request): array
{
return [EnumFilter::make('status')->enum(Status::class)];
}
Where to look first.
src/ — the package is deliberately thin: Fields/Enum.php, Filters/EnumFilter.php, Filters/EnumBooleanFilter.php. Treat the source as the documentation; it is a few hundred lines and readable in a coffee break.tests/ — the best reference for the exact field-name/meta contract Nova expects, and the filter tests are the canonical example of the relation-index case.First real use case: delete a hand-maintained options array.
// before — two sources of truth, drift guaranteed
Select::make('Status')->options([
'draft' => 'Draft',
'published' => 'Published',
]);
// after — one source of truth, reused by index, detail, form, and filter
Enum::make('Status')->enum(Status::class);
The payoff is not the field itself; it is that index tables, detail views, create/update forms, and filters all resolve the same label() values from the same enum.
Keep the model cast as the source of truth and let the field follow it. This is the pattern everything else hangs off:
protected $casts = ['status' => Status::class];
With the cast in place, filter comparisons, index display, and validation all flow through the enum rather than raw strings. The field becomes a presentation adapter, not a second place to declare valid values.
Use stock Nova modifiers — the field composes with the normal field API. Resist the urge to look for package-specific flags; there usually aren't any:
Enum::make('Status')
->enum(Status::class)
->hideWhenCreating()
->hideFromIndex()
->help('Set automatically after review');
Pair with Rule::enum for validation. The package renders the select; it does not replace Laravel's validation rule:
->rules('nullable', Rule::enum(Status::class))
This rejects any stored value that isn't a valid case — which is exactly the failure mode you get when someone edits a row outside Nova or a background job writes a stale string.
Nullable statuses need all three pieces aligned. ->nullable() on the field, 'nullable' in the rules, and a nullable column in the migration. The filter's null bucket is only meaningful if the underlying column can actually hold null — check the migration, not just the field declaration.
Pick the filter class by cardinality, not by habit.
EnumFilter.EnumBooleanFilter.use Datomatic\NovaEnumField\EnumBooleanFilter;
EnumBooleanFilter::make('is_active')->enum(ActiveState::class);
The wrong class renders perfectly fine and then silently never narrows the query — a bug that costs an afternoon because nothing errors.
Reuse the same filter instance in metrics and cards. Because the filter emits backing values rather than labels, EnumFilter::make('status')->enum(Status::class) works identically in resource filters and in any card or metric that queries by status. No translation layer, no parallel mapping table.
Treat relation-rendered indexes as a first-class case. Filters mounted on a HasMany / MorphMany related resource (one with its own filters()) travel a different request shape than a top-level index. Since 1.11.1 both filter classes handle it. If you pin an older tag or maintain a fork, this is the first suspect for a hard 500 on an otherwise-fine filter.
For DB-level sorting, keep an explicit sortable column. In grouped index views the enum's label() drives headers, which is what you want. But Nova's default sort on an enum-cast attribute doesn't map cleanly to SQL — sort on the underlying column.
Extend through withMeta or a subclass. Anything the fluent API doesn't expose can go through meta, or you can override component() on a subclass:
Enum::make('Status')
->enum(Status::class)
->withMeta(['placeholder' => 'Choose…']);
Split your tests along the same seam as the package.
Status::from(), label() for every case.Nova::filtered()-style helpers to assert the filter returns the correct query constraint.Day-to-day checklist for a new enum-backed column. Once you've done it twice it takes minutes:
HasLabel on the enum with a match arm per case.Enum::make(...)->enum(...)->rules('nullable', Rule::enum(...)) in fields().filters() and confirm it's reachable via availableFilters().EnumFilter / EnumBooleanFilter is applied to an index rendered through a relation. The tell: a 500 on a nested/detail index while the resource's own index works fine. No config changes needed — it's a straight bug fix.whereIn. The field will render and look correct while the filter silently fails to narrow. Back with string at minimum.HasLabel::label() first, then falls back to the case name. If labels read Draft-with-a-fallback feel, verify the interface is actually implemented on the enum — a description() alone won't drive select options.int-backed enum stored in a VARCHAR column passes validation fine but produces filter queries that never match. Audit the migration before you start debugging the field.->options() with ->enum(). enum() populates the options internally; a later options() call quietly overwrites them. You will then chase a phantom mapping bug for an hour. Grep for both calls on the same field.Resource::filters() and the resource must be reachable via availableFilters(). A missing filter simply disappears from the UI with no error — check reachability before assuming the class is broken.dist/; publishing is not required. If the field renders as an empty box, run php artisan nova:publish and clear the Nova cache before filing a package bug.meta. The field's meta JSON (Nova's debug output or the Vue inspector) shows whether options and labels map correctly — far faster than reading the compiled component.src/. The classes are thin enough that copying a snippet's namespace from a blog post is a real risk. Verify against the source and composer.json autoload.composer update in a branch, and keep a fork as a realistic plan — the surface is a few hundred lines, so maintenance in-repo is cheap and low-drama. The 1.11.1 patch is a positive signal about triage, but a patch release doesn't upgrade the risk class.Select with an options array is about twenty lines and not worth a dependency, a version pin, or a fork plan. Adopt when the repetition starts to hurt.How can I help you explore Laravel packages today?