ptplugins/filament-auto-filters
Filament Auto Filters automatically generates table filters for Filament resources, reducing boilerplate and keeping admin panels consistent. Configure filters with minimal setup and give users faster, more intuitive ways to narrow down records.
distinct_max_options before you have real data:
composer require <vendor>/<package>
php artisan vendor:publish --tag=<package>-config
distinctOptionsUsing (the filter/field you attach to a table or form). That callback is the single extension point you'll spend most of your time in.distinct_max_options to a sane cap (e.g. 50–200) up front. It guards both flat lists and associative maps, and it is the difference between a usable select and a dropdown with 40k rows.->distinctOptionsUsing(fn () => Order::query()->distinct()->pluck('status')->all())
You get natural sort and the raw value as the label, with no extra work.Flat list (unchanged behavior). Return an array of scalars. The package natural-sorts them and uses each value as its own label. This is still the right default for statuses, countries, and slugs — don't reach for the map just because it exists.
Associative value => label map (new in 1.4.1). Return a keyed array when the display string differs from the stored value:
->distinctOptionsUsing(function () {
return Order::query()
->distinct()
->pluck('total') // raw values
->mapWithKeys(fn ($v) => [$v => number_format((float) $v, 2)])
->all();
})
The order and labels you return are kept as-is — the package will not re-sort or relabel. That makes this mode the tool for:
value => '$1,250.00').'P' => 'Pending').Own your ordering in map mode. Because natural sort is skipped, build the map from an ordered query (orderBy('total')) or sort in PHP before keying it. Sorting after mapWithKeys is fine too, but don't assume the package will fix it for you.
Keep the callback cheap and side-effect free. It runs when options are resolved, potentially on every render. Prefer a single distinct()->pluck() over N+1 lookups, and memoize per request if you call it from more than one place:
->distinctOptionsUsing(fn () => Cache::remember('order.statuses', 300, fn () => ...))
Respect the cap deliberately. distinct_max_options still applies in map mode, so a formatted map can be truncated exactly like a flat list. If you know the domain is small and bounded (statuses, tiers), raise it; if it's user-generated (amounts, tags), keep it low and pair the filter with a search/autocomplete affordance instead of trying to render everything.
Integrate at the filter, not the query. The callback only produces options; the selected key is what gets applied to the query. Keep the key equal to the actual column value so filtering stays correct — the label is cosmetic.
'' => 'None' entry will not survive. If you need an explicit "empty/none" option, use a real sentinel key (e.g. '__none') and translate it in your query scope, or handle empty separately.'10' to 10 when used as an array key. For decimal amounts this is usually harmless, but for zero-padded codes ('007') or IDs you rely on as strings, cast deliberately or prefix the key. This is the most common source of "the filter sends the wrong value" bugs after switching to the map.$1,000.00). That's allowed, but it makes the select ambiguous to users — format with enough precision to disambiguate.distinct_max_options applies to both modes. When a map silently loses entries, check the count after blank-dropping and keying against the threshold before debugging anything else.dd($callback())) — check count, key types, and ordering in one shot. If options look right but filtering fails, the problem is the key type or the query scope, not the resolver.distinctOptionsUsing may return; existing flat-list callbacks keep natural sort and value-as-label. No config or migration changes required — you can adopt the map incrementally, one filter at a time.How can I help you explore Laravel packages today?