sereny/nova-searchable-filter
A Laravel Nova filter package that adds searchable select filters to your admin panels, letting you narrow resources by related records through a searchable dropdown instead of long, unwieldy option lists.
This is a small, single-purpose Nova utility: a belongsTo filter that searches related records asynchronously instead of loading every option into a select. With 4 stars, zero dependents, and a last release in August 2023, treat it as a convenience package rather than foundation code. Verify Nova compatibility before adopting it—it was released in the Nova 4 era and may need patching for Nova 5.
Minimal adoption path:
composer require sereny/nova-searchable-filter
vendor/sereny/nova-searchable-filter/README.mdvendor/sereny/nova-searchable-filter/src/php artisan nova:filter UserFilter
apply():
use Illuminate\Http\Request;
use Sereny\NovaSearchableFilter\Filters\SearchableFilter;
class UserFilter extends SearchableFilter
{
public $name = 'User';
// Confirm exact property/method names in the package base class.
public $relationship = 'user';
public $searchColumns = ['name', 'email'];
public function apply(Request $request, $query, $value)
{
return $query->where('user_id', $value);
}
}
public function filters(Request $request)
{
return [
new UserFilter,
];
}
The first real use case is an Order resource with a user_id foreign key and tens of thousands of users. A standard Nova select filter becomes unusable; this package replaces it with a search box that queries only matching users. Test that workflow immediately: open the resource index, type into the filter, watch the network request, select a result, and confirm the index query is constrained correctly.
One filter per relationship, with an explicit apply(). Even if the package can infer the foreign key, keep apply() explicit. It makes the filter readable and protects you if the package’s internals change:
public function apply(Request $request, $query, $value)
{
return $query->where('user_id', $value);
}
Reusable base filter for repeated belongsTo filters. If several resources filter by the same kind of relationship, create an internal abstract filter:
abstract class BelongsToSearchFilter extends SearchableFilter
{
abstract protected function foreignKey(): string;
public function apply(Request $request, $query, $value)
{
return $query->where($this->foreignKey(), $value);
}
}
Then each concrete filter only defines its relationship, search columns, and foreign key.
Scope searchable options to the current user or tenant. A searchable endpoint can easily leak related record names. If the package exposes an options/search query extension point, override it and apply the same authorization scope used elsewhere:
protected function searchQuery(Request $request)
{
return User::query()
->where('tenant_id', $request->user()->tenant_id)
->select(['id', 'name'])
->orderBy('name');
}
Check the base class for the exact method name (options(), searchQuery(), or similar). If no hook exists, override the component’s data endpoint or wrap the relationship with a global scope.
Keep labels meaningful. Search on name, email, or a username, but display something a human recognizes. If users can have duplicate names, include a secondary identifier in the label, such as Jane Doe ([email protected]).
Handle nullable filters deliberately. A clearable filter should send null or an empty value. In apply(), decide whether an empty value means “no constraint”:
public function apply(Request $request, $query, $value)
{
return $value ? $query->where('user_id', $value) : $query;
}
Combine with other filters without alias collisions. If two filters constrain the same relationship, use distinct column aliases or explicit where clauses so Nova’s filter serialization does not overwrite one with the other.
Test the filter logic outside Vue. Add a feature test that invokes apply() with a request and value, then asserts the generated SQL constraints. The searchable dropdown itself is UI; the important behavior is the query it produces.
vendor/sereny/nova-searchable-filter/src before copying code from a blog post or the README.php artisan route:list --path=nova-api to confirm any package endpoints are registered.LIKE '%term%'. That does not use a normal B-tree index well. For large related tables, index the search columns, cap results, require a minimum query length, or move to a dedicated search index. Watch slow-query logs in production.withTrashed() or a separate “selected value” lookup."10" against an integer column can behave differently on PostgreSQL than MySQL. Cast explicitly in apply() if needed.DB::listen() logging around the filter endpoint rather than guessing at the Vue component.apply() in your filter, or publish the Vue component if the package supports it. Direct edits to vendor/ disappear on the next composer install.How can I help you explore Laravel packages today?