metasync-site/nova-has-many-searchable
A Laravel Nova package that adds searchable HasMany relationship support, making it easier to find and manage related records directly from Nova resources. Useful for large related datasets where standard relationship fields become hard to navigate.
Pull the package in with Composer and let Nova discover it:
composer require metasync-site/nova-has-many-searchable
There is no service provider to register manually — Nova auto-discovers fields under vendor/ — but if you run into a missing class after a composer update, do a php artisan nova:publish and clear the config cache.
The first place to look is the field class itself. Open vendor/metasync-site/nova-has-many-searchable/src/ and read the make() / resolve() methods before you wire it into a resource; with only one repo star and no wiki, the source is the documentation.
Minimal wiring on an Account resource that owns many User records:
use MetasyncSite\NovaHasManySearchable\NovaHasManySearchable;
public function fields(Request $request)
{
return [
ID::make()->sortable(),
Text::make('Name'),
NovaHasManySearchable::make('Users')
->searchableColumns(['name', 'email']),
];
}
Your first real use case is almost always "attach/lookup across a large has-many." Drop the field onto the resource, open the detail view, and type into the search box — the field should hit a Nova route that returns JSON and render the matches as selectable rows.
Use it as a picker, not a full relationship manager. The value proposition is narrowing thousands of related rows down to one. Keep the rest of the relationship (create, edit, detach) on Nova's native HasMany, and let this field act as the "attach existing" surface on a pivot-less parent.
// Account has many Users; the field searches the pool
NovaHasManySearchable::make('Users')
->searchableColumns(['name', 'email', 'username'])
->searchDebounce(400) // typical — verify exact method name in src
->perPage(25)
->minSearchLength(2);
Scope the query, don't rely on filtering in the browser. Most of these fields let you pass a query callback. Use it to enforce tenancy and authorization:
NovaHasManySearchable::make('Users')
->searchableColumns(['name', 'email'])
->query(fn ($query) => $query->where('team_id', $request->user()->team_id));
Pair it with dependent fields. Because the result set is JSON-driven, follow-up fields (DependsOn) can key off the selected related model's ID. Wire the selector's ->dependsOn() to the downstream field so the form reloads with the picked record's data instead of forcing a full page reload.
Server-side search across computed columns. If "display name" is first_name . ' ' . last_name, add both raw columns to searchableColumns and let the SQL LIKE do the concatenation, rather than subclassing the field and overriding the search.
Reuse a base resource. Where you need the same searchable picker on several resources, create a small trait:
trait HasSearchableMembers
{
protected function memberPicker(string $name = 'Users'): NovaHasManySearchable
{
return NovaHasManySearchable::make($name)
->searchableColumns(['name', 'email'])
->query(fn ($q) => $q->where('active', true));
}
}
Integrate with actions/lenses. Emit the selected IDs into a custom Nova action (bulk assign, bulk invite). Since the field only produces IDs, keep action payloads small and let the action re-fetch by ID on the server — never trust the client's serialized model.
One star means you are the QA team. Pin the version in composer.json ("metasync-site/nova-has-many-searchable": "~1.0"), commit composer.lock, and read the changelog on every bump. There is no active issue tracker to rely on for regressions.
Nova version drift is the #1 breakage. Nova 4 vs. Nova 5 changed the JS toolchain (Vite vs. Mix, nova.mix vs. webpack.mix). Confirm the compiled assets under dist/ match your Nova build; if the search box renders but returns nothing, view-source the network tab for a 404 on the field's route — that's almost always a missing nova:publish after the major upgrade.
Search columns must be real database columns. Aliased/withCount columns don't exist at the SQL level, so the LIKE fails silently (empty results, no exception). If you must search an aggregate, drop to a ->query() closure that joins a subquery.
Check authorization twice. Nova only checks policies for models it resolves through its own routes; custom search endpoints often bypass the policy unless the field implements it. Before shipping, log in as a low-privilege user and confirm they cannot enumerate records outside their scope — pass an explicit ->query() closure with the tenant/auth filter regardless of what the field does by default.
Foreign key and owner key mismatches. If the relationship isn't hasMany(User::class) with the conventional account_id / id, set both explicitly on the resource (HasMany::make('Users', 'users', User::class)) so the field can derive the right keys. Guessing keys is the second most common cause of "search returns the entire users table."
Performance: the search is a leading-wildcard LIKE '%term%', which is unindexable. Above a few tens of thousands of rows, add a ->query() that pre-filters on a cheap indexed column, or switch the search column to a full-text index and override the search method in a subclass.
Debounce/rate-limit is your friend. A typed search hitting a Nova route on every keystroke will show up in your logs and your DBA's inbox. Set a debounce of 300–500ms and a minSearchLength of 2–3.
Debugging checklist, in order: (1) php artisan nova:publish && php artisan optimize:clear; (2) browser network tab — is the search route 404, 403, or 500?; (3) storage/logs/laravel.log for the SQL it tried; (4) dd() inside the field's resolve() — does it receive the relation you expect?; (5) is the resource's relationship method named exactly what you passed to make()?
Extension points worth knowing. The field subclass is public function, so you can class MyPicker extends NovaHasManySearchable and override resolve() for custom serialization or add ->withMeta() for UI toggles (help text, placeholder). Prefer subclassing over forking — it keeps Composer updates clean.
Don't expect community answers. Search Stack Overflow for Nova custom-field patterns generally (route registration, Nova::script), which apply here even if the package itself has no Q&A. When in doubt, trace the request from routes/api.php in the field's source outward.
How can I help you explore Laravel packages today?