optimistdigital/nova-multiselect-filter
A Laravel Nova package that adds a multiselect filter to resource indexes, letting users filter records by multiple options in a single filter. Configurable options and simple integration for Nova admin panels.
Architecture fit. This package is a thin, single-purpose extension of Laravel Nova's native filter contract. It substitutes one class (Outl1ne\NovaMultiselectFilter\MultiselectFilter) for Nova's Laravel\Nova\Filters\Filter and adds exactly one behavior: the filter UI accepts multiple selected values and hands them to apply() as an array instead of a scalar. Everything else — registration in filters(), serialization into the index query string, execution against resource index and count queries — is stock Nova machinery. There is no service provider, no config file, no migration, no asset build step; the Vue component ships precompiled. Architecturally this is about as contained as a dependency gets: it lives entirely in the admin presentation layer and is inert outside Nova's request cycle.
Integration feasibility. Low effort, well-bounded. The implementer extends the package class, implements options() (a key→label map) and apply() (a query constraint), and registers the filter on a resource. Typical work is a few hours for the first filter and minutes for each subsequent one. The genuine friction is not code volume but naming: the vendor has moved from optimistdigital/nova-multiselect-filter to outl1ne/nova-multiselect-filter, and most search results still show the old namespace. That mismatch produces a failure mode where Composer succeeds but the use statement is unresolvable — worth stating in the ticket so the implementer verifies both the Composer name and the PHP namespace from vendor/.
Technical risk. The risk is concentrated in two places, both cheap to test and expensive to discover late:
composer require proves nothing. This must be a manual smoke test on a real index after install and after every Nova upgrade.apply() passes an empty array into whereIn, MySQL returns zero rows, the admin sees an empty table, and the reasonable conclusion is that the app lost their data. Every filter needs an explicit guard returning the query unchanged when nothing is selected.Secondary risks: option keys come back as strings (fine for MySQL implicit casting, a problem on Postgres or UUID primary keys); whereIn excludes NULL rows, so an "unset" bucket needs an explicit sentinel; and filter key() values leak into shareable URLs, so renaming a filter class silently breaks bookmarked views. A "select all" over a large table can also generate a pathological whereIn with thousands of bindings.
Key questions.
SelectFilter already cover multi-select in our version? That would be the cheapest possible outcome and should be checked before anything is installed.outl1ne package in our stack, or does the vendor relationship already exist? Marginal cost differs sharply between those cases.nova-multiselect-field? If so, we've onboarded two Nova-major compatibility windows rather than one.Stack fit. Assumes Laravel Nova, which is itself an assumption worth confirming before anything else. The package is MIT-licensed, PHP-only from the consumer's perspective, and requires no Node toolchain, no published assets, and no queue or scheduler involvement. It touches no models, migrations, routes, auth, or jobs. That means it can be adopted on a legacy Nova install without dragging in unrelated upgrade work — a meaningful property when the alternative (building a local filter) costs roughly a day and creates permanent in-house ownership of the same code.
Migration path. Three steps: composer require outl1ne/nova-multiselect-filter, add the filter class extending MultiselectFilter, add one line to the resource's filters(). The removal path is exactly as short — composer remove, delete the filter class, delete one line from filters(). That reversibility is the strongest argument for adoption: the decision can be made and unmade without ceremony. If any of those three removal steps turns out to be hard, something has leaked out of the admin layer into the application and should be fixed before it calcifies.
Compatibility. Two constraints to pin down before committing:
composer why-not outl1ne/nova-multiselect-filter <nova-version> is the diagnostic when a Nova bump stalls.$values payload shape. Depending on the underlying field component version, selections may arrive as a list of keys or as a keyed key => label map. This should be verified empirically once (a debug log in apply() on first run) and recorded in a code comment, with defensive handling if it turns out to be map-shaped.Pin an explicit version constraint rather than a floating ^x, and commit the lockfile. MIT licensing means forking is legal and inexpensive if upstream goes quiet — a real mitigation given 46 stars and zero dependents.
Sequencing.
SelectFilter already handles multi-select. If it does, stop.apply() directly — instantiating the filter with a NovaRequest and asserting bindings, plus explicit cases for [] and null. No browser needed.whereHas, cached options), which carries the extra operational concerns below.Deferring the expensive "advanced filtering / saved views / query builder" initiative is the point of this sequencing: adoption here buys a visible productivity win now, and usage data from these small filters becomes evidence for whether the larger investment is justified.
Maintenance. The recurring cost is one line in the lockfile and one item on every future Nova-major upgrade checklist. That is genuinely small — but it should be a deliberate decision with a named owner, not a drive-by composer require that surfaces in an audit six months later. The code the team nominally owns is about 40 lines of contract (options(), apply(), column(), key()) plus whatever business logic gets written into each filter. Keeping that business logic out of the filters matters: the definition of "which statuses count as open" belongs on the model or in a query scope, not in a Nova filter class. The moment a filter becomes load-bearing for reporting, it stops being removable in one line — and removability is the entire reason to accept a low-adoption dependency.
Support. Internal-staff audience, low polish expectations, and a narrow failure surface. The first-line diagnostic is the browser Network tab, not the query: a filter that "does nothing" is almost always a component that failed to mount. That means support burden concentrates on rendering, not on data correctness — provided the empty-selection guard is in place everywhere. Without that guard, the support story is much worse, because the symptom looks like data loss.
Scaling. Three ceilings worth naming:
whereIn width. A "select all" can generate thousands of bindings and, at the extreme, exceed max_allowed_packet. Same mitigation: bound the option set.options() cost. It runs on every index render and again on filter apply, and filter apply() also executes against count queries. For relationship-backed options, Cache::remember around the pluck() and busting on the model's saved/deleted events is the single highest-leverage change — and keeping apply() cheap matters because count queries hit it too.None of these are blockers for the intended use case (a handful of static enum values, or a modest relationship list). They define the boundary at which this package stops being the right tool.
Failure modes. Ranked by how bad they look to a user:
| Failure | Symptom | Mitigation |
|---|---|---|
whereIn([]) reaches the query |
Empty table; user believes data was lost | Guard every apply(); unit-test [] and null |
| Component fails to mount after Nova upgrade | Filter does nothing, silently | Smoke-test on a real index as a release checklist item |
| Vendor namespace drift | Composer installs, use unresolvable |
Verify both names from vendor/ |
| String-typed IDs on Postgres/UUID PKs | Wrong or failing matches | Cast explicitly where the column type is strict |
| NULL rows never match | "Unset" records vanish from filtered views | Explicit __null sentinel option, translated in apply() |
key() renamed |
Bookmarked filtered URLs break | Keep the old key as an alias for a release |
The two failure modes that actually generate escalations are the first two — one looks like data loss, the other looks like the feature was never built. Both are eliminated by cheap, specific practices: an empty-array guard in every filter, and treating "component mounts" as a verification step rather than an assumption.
Ramp-up. Very fast, with one prerequisite. The implementer needs to read exactly two things: vendor/outl1ne/nova-multiselect-filter/src/MultiselectFilter.php (the whole contract is ~40 lines) and the README's usage block. Everything beyond that is Nova filter machinery the team already knows. The prerequisite is Nova familiarity — this is not a package to hand to someone who hasn't written a Nova filter before, because the failure modes are silent and the debugging instinct (check the browser, not the query) is learned rather than obvious.
Once three of these exist, extract a base class or a FiltersByEnum trait. That kills the copy-paste and gives one place to fix the empty-selection guard. Ramp-up for the fourth filter should be measured in minutes.
How can I help you explore Laravel packages today?