vyuldashev/nova-column-filter
A Laravel Nova filter that lets you filter resources directly by their table columns. Configure which columns are searchable and in what order, giving users a quick, native way to narrow down resource lists without building custom filter classes.
Prerequisites — check these before touching code. This package is only meaningful inside an installed Laravel Nova app (Nova is a commercial, per-site-licensed dependency). Confirm your Nova major version first: open the package's composer.json and read the laravel/nova and illuminate/support constraints. Nova's Filter contract and its generated stubs changed across v3/v4/v5, and a package with 9 stars and 0 dependents has almost certainly not tracked those changes. If the constraint doesn't match your Nova major, stop here — the source read-through is still worth an hour, but don't wire it in.
Install (pinned, not floating).
composer require vyuldashev/nova-column-filter:"<exact-version>"
Resist the caret range. With no downstream users there is nobody to absorb a breaking patch, and a surprise composer update in CI is an outage for your admin tooling. Remember Nova resolves from nova.laravel.com, so your pipeline needs Nova credentials or the install fails at resolution time.
Where to look first, in this order:
vendor/vyuldashev/nova-column-filter/src/ — the whole package is a handful of files; read all of them end to end, it takes under an hour.Laravel\Nova\Filters\Filter — specifically its apply(NovaRequest $request, $query, $value) and options(Request $request) methods. Confirm exactly what $value looks like and what options() renders.filters() method of the Nova resource you intend to filter.app/Nova/Filters/ stub, side by side — you need to see what boilerplate this is actually removing.First use case — pick the least critical internal resource. Do not start on a PII-heavy or tenant-scoped screen. Pick an internal back-office index where operators currently ask for a "filter by status" or "filter by created date" and where a wrong result costs nothing. Register the filter and immediately prove it constrains the query.
Registering it. In the resource:
use Laravel\Nova\Http\Requests\NovaRequest;
use Vyuldashev\NovaColumnFilter\ColumnFilter;
public function filters(NovaRequest $request): array
{
return [
ColumnFilter::make('Status'),
];
}
The exact factory signature is the one thing you must verify in source before copying this: some Nova filter packages take the database column as the second argument to make(), others use a ->column('status') chain. Read src/ and match what ships — don't guess, and don't copy a snippet from a blog post.
Prove it works before you trust it. The failure mode that matters is the silent no-op — the filter renders in the panel, the user picks a value, and the index query is unchanged. Log the SQL:
// temporarily, in the resource or a middleware
\DB::listen(fn ($q) => logger($q->sql, $q->bindings));
Select a filter value in the UI and confirm the emitted select ... where status = ? is actually there. Then confirm it's still there after you remove a filter value. That check is your smoke test — write it down and keep it, because it's the thing you'll re-run after every Nova upgrade.
Treat this as a filter base class you register per column, not a single global filter. The value is in repetition: one resource, three columns, one array, no bespoke classes.
public function filters(NovaRequest $request): array
{
return [
ColumnFilter::make('Status'), // enum / string, exact match
ColumnFilter::make('Email'), // partial lookup -> LIKE
ColumnFilter::make('Created At'), // date
ColumnFilter::make('Is Active'), // boolean
];
}
Match the filter type to the column, not to the label. A LIKE %...% filter on an indexed varchar is fine for a support "find the customer by partial email" workflow and terrible on an unindexed text column at scale. For enums and booleans, prefer Nova's built-in SelectFilter / BooleanFilter — they are supported, they render a better UI, and they don't create a maintenance obligation. Reach for ColumnFilter when the column set is long-tailed and stable enough to be boring, and bespoke when the requirement is relationship-shaped or authorization-sensitive.
The support-workflow pattern (the highest-value use of this package). Three recurring tickets collapse into one capability:
LIKE.Each of these is currently an engineer-day of hand-written Filter subclass work. Each becomes a line here. That's the whole pitch, and it's a real one — but only once the number of resources needing it is large enough that per-resource filter classes are actual drag.
Composition. Nova ANDs multiple active filters together. Your apply() should append exactly one where and never orWhere at the top level — an unparenthesized orWhere inside a filter will widen the result set past whatever indexQuery scoping the resource already applied. If the package does anything conditional in apply(), read it and confirm every branch is an AND.
Scoping and indexQuery. Nova applies authorization and per-resource indexQuery scoping to the index query, then hands it to filters. Your filter's where lands after that. Verify explicitly, don't assume: create two users with different visibility, log in as the narrower one, filter on an unindexed column, and confirm the result set is still constrained by the tenant/auth scope. A filter must never be a path to wider visibility.
State and URL round-tripping. Nova serializes active filter values into the index URL and persists per-user filter state. Two things to check against the source: that $value deserializes back from a string cleanly (date formats are the usual offender — a date range that re-parses to a different timezone is a support ticket waiting to happen), and that the filter's key() is stable so persisted state from before an upgrade doesn't silently stop matching.
Testing. Two levels, both cheap:
// unit: exercise apply() directly on a query builder
$query = Model::query();
(new ColumnFilter('status'))->apply(
NovaRequest::create('/'), $query, 'active'
);
$this->assertStringContainsString('where "status"', $query->toSql());
Plus one feature test that hits the Nova index endpoint as a real user and asserts rows are excluded. The unit test proves the SQL shape; the feature test proves Nova actually invokes it. You want both, because the silent no-op after an upgrade breaks the second one while leaving the first green.
Vendoring is the intended end state. At 9 stars and 0 dependents, the honest read is that this is source code to evaluate, not a dependency to trust. Once you've signed off on the source, copy the filter class into app/Nova/Filters/ under your own namespace (verify the license permits it — it's MIT as far as Nova packages go, but check), drop the Composer requirement, and own roughly 150 lines. One file you can read end to end in an afternoon is cheaper to own than to track — and it turns every future Nova upgrade from a dependency gamble into an explicit decision.
Column name and operator must be developer-supplied — never request-derived. This is the one non-negotiable. A filter bound to a raw database column, where the column name or SQL operator comes from the request, is simultaneously a SQL-injection surface and a column-enumeration surface. Open the source and confirm both are hard-coded or drawn from a constant array inside the resource. If the package accepts an arbitrary string and passes it through to where(), either wrap it in an allowlist yourself or fork it and add one. Add a code-review checklist item for this so it's caught on every new filter registration.
Enforce indexed columns only. A generic column filter makes it trivially easy for someone to expose an unindexed column on a table with millions of rows, and the resulting LIKE '%term%' scan will take down the admin panel before anyone links it back to the filter. Make "indexed column" a review requirement alongside the allowlist check.
The silent no-op is the failure mode to guard against. After a Nova major bump, the filter may still render — options populate, the UI looks fine — but no longer constrain the query. Admins then make decisions on unfiltered data, which is worse than the filter being visibly broken. Keep the SQL-logging smoke test from Getting Started and run it after every Nova upgrade. Budget a few hours per Nova major for this; it's a real line item and it's larger than the package's acquisition cost.
Raw columns leak database semantics, not business meaning. Nullable flags, soft-delete state, enum integer values, and JSON paths all surface directly to operators. A user filtering "status = 1" has no idea whether that means active, pending, or archived. If the actual requirement is "filter by curated business dimensions," Nova's built-in SelectFilter and BooleanFilter already do this and better — this package adds nothing there.
JSON and array columns are a trap. Filtering a JSON column with a plain where will either error or silently match nothing depending on the driver. If the package doesn't special-case JSON path syntax, don't register those columns; write a bespoke filter with the correct whereJsonContains / -> operator instead.
Don't use it for relationship filters, multi-model selects, or authorization-aware scoping. Those need joins or subqueries against related tables, and a generic column filter will fight you on query construction and on composing with Nova's authorization pipeline. Bespoke filter, every time.
Not a search replacement. If the requirement is full-text relevance, use Nova search / Laravel Scout. LIKE %term% returns rows in arbitrary order and gets slower linearly; it is a lookup aid, not a search engine.
Composer resolution is a deployment risk. Because Nova is served from a private repository, a constraint conflict introduced by this package turns into a failed composer update in the pipeline — which is an admin-tooling outage. Pin the exact version, resolve it in staging first, and confirm your CI image has Nova credentials.
Extension points worth knowing. The filter class is small enough that targeted forks are cheap: add operator support (=, like, in, between) if the shipped version only does equality; add schema-based column validation against the model's getConnection()->getSchemaBuilder()->getColumnListing(); add JSON-path handling. Each is a handful of lines — but if you find yourself making more than two such changes, that's the signal to own the file outright rather than patch around it.
Keep it off customer-facing and PII-adjacent screens. Confine it to internal admin resources. The blast radius of an unmaintained dependency in an internal index screen is bounded to operator confusion; the blast radius on a customer-facing surface or a screen handling regulated data is not.
How can I help you explore Laravel packages today?