Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nova Input Filter Laravel Package

optimistdigital/nova-input-filter

Adds a free-text input filter to Laravel Nova resource indexes, letting users type a value to narrow listings instead of choosing from preset options. Handy for quick name, email, or ID searches in admin panels. Lightweight and easy to register on any Nova resource.

View on GitHub
Deep Wiki
Context7

Getting Started

Install (mind the namespace rename). The repo moved orgs, so the Composer package name and PHP namespace depend on the tag you install:

composer require outl1ne/nova-input-filter   # current line
# older tags: optimistdigital/nova-input-filter, namespace OptimistDigital\...

Laravel auto-discovers the service provider — there are no assets to publish, no migrations, no npm run build. If a tutorial's use statement points at OptimistDigital\NovaInputFilter\InputFilter and your tag is Outl1ne\NovaInputFilter\InputFilter (or vice versa), you'll get a "class not found" that looks like a broken install. Check vendor/outl1ne/nova-input-filter/src/ before debugging anything else.

First use case: an email/ID lookup on one resource index. Generate the shell with Nova's own generator, then reparent it:

php artisan nova:filter EmailInputFilter
// app/Nova/Filters/EmailInputFilter.php
namespace App\Nova\Filters;

use Illuminate\Http\Request;
use Outl1ne\NovaInputFilter\InputFilter;

class EmailInputFilter extends InputFilter
{
    public function name()
    {
        return 'Email contains';
    }

    public function key()
    {
        return 'email'; // becomes the ?email=... query param
    }

    public function apply(Request $request, $query, $value)
    {
        if (blank($value)) {
            return $query; // never skip this guard
        }

        return $query->where('email', 'like', '%' . $value . '%');
    }
}

Register it in the resource:

public function filters(Request $request)
{
    return [
        new \App\Nova\Filters\EmailInputFilter,
    ];
}

That's the whole loop: install → subclass → override apply() → register in filters(). Budget a few hours for the first one, mostly spent validating query behaviour against real data.

Where to look first: InputFilter::apply() is the only extension point that matters. Everything else (label, key, placeholder) is cosmetic. Read the base class source — it's ~30 lines — and you've read the package.

Implementation Patterns

Column-scoped LIKE (the 80% case). One filter per column, name() describes the column, key() stays short and stable:

public function apply(Request $request, $query, $value)
{
    return blank($value) ? $query : $query->where('sku', 'like', "%{$value}%");
}

Multi-column OR search. Ops users often don't know which column holds the value. Group the OR in a closure — see the Gotchas section for why this is non-negotiable:

return $query->where(function ($q) use ($value) {
    $q->where('email', 'like', "%{$value}%")
      ->orWhere('name', 'like', "%{$value}%")
      ->orWhere('reference', 'like', "%{$value}%");
});

Exact/numeric lookup for IDs. The value arrives as a string from the query string. Cast explicitly and short-circuit non-numeric input:

public function apply(Request $request, $query, $value)
{
    $value = trim((string) $value);

    if ($value === '' || !ctype_digit($value)) {
        return $query->whereRaw('1 = 0'); // fail closed, don't silently show everything
    }

    return $query->where('id', (int) $value);
}

Reuse one class across resources via the constructor. Rather than four near-identical classes, parameterise the column:

class ColumnInputFilter extends InputFilter
{
    public function __construct(protected string $column, protected ?string $label = null) {}

    public function name() { return $this->label ?? ucfirst($this->column); }
    public function key()  { return 'input_' . $this->column; }

    public function apply(Request $request, $query, $value)
    {
        return blank($value) ? $query : $query->where($this->column, 'like', "%{$value}%");
    }
}

// in filters():
new ColumnInputFilter('email', 'Email contains'),
new ColumnInputFilter('phone', 'Phone contains'),

Role/policy-gated registration. Filters are just array entries, so gate them where you gate everything else:

public function filters(Request $request)
{
    return array_filter([
        $request->user()->can('viewSensitive', Customer::class)
            ? new SsnInputFilter : null,
        new EmailInputFilter,
    ]);
}

Note this hides the input; it does not replace authorization. Filters narrow Nova's already-scoped index query, so policies still decide the row set — but see the OR-grouping gotcha, which is the one way to escape that scoping.

Combine with built-ins freely. Nova pipes every applied filter; multiple InputFilter instances coexist as long as their key() values are distinct. A common pairing is a SelectFilter for status plus an input filter for free text.

Test apply() directly. You don't need Nova's HTTP stack to get confidence:

$query = Customer::query();
(new EmailInputFilter)->apply(Request::create('/'), $query, 'acme');
$this->assertStringContainsString("like ?", $query->toSql());

Add one feature test per resource hitting the Nova index route with ?email=acme to catch the wiring, and one with ?email= (blank) to prove nothing breaks.

Gotchas and Tips

Always guard blank($value). Nova calls apply() whenever the filter key is present in the query string — including when the user cleared the field and hit "Apply Filter". Without the guard you get LIKE '%%' (harmless but a wasted scan) and, worse, filters that quietly change semantics after a clear.

Never interpolate into whereRaw. The raw string is user input. Use where($col, 'like', $binding) or whereRaw('LOWER(email) like ?', ['%'.strtolower($value).'%']). If you need to escape LIKE metacharacters, addcslashes($value, '%_\\') and add an ESCAPE clause — otherwise a user typing % matches every row and _ silently matches any character.

Wrap every OR group in a closure. ->orWhere(...) at the top level of $query ORs against everything Nova already applied — soft-delete scopes, tenant scopes, the search term, other filters. The result is rows from outside the scoped set. This is the single most likely way to accidentally leak data with this package.

Leading wildcards kill indexes. LIKE '%term%' cannot use a B-tree index on most engines. On small/medium admin tables this is irrelevant; on a high-volume resource it's a full scan every keystroke-apply. Prefer prefix matching ("%{$value}%" → "{$value}%") or exact match for IDs/SKUs, and review EXPLAIN output under realistic row counts before rolling a filter onto a big table.

key() is your URL contract. It's the query-string parameter, so it shows up in browser history, shared links, and support screenshots. Rename the class and the default key may change — override key() and keep it stable and short. Avoid search (Nova's own param) and any key already used by another filter on the same resource; collisions share state silently.

Don't ship a default() unless you mean it. A default value makes the filter applied on first page load, so the index arrives pre-filtered and users think records are missing. Return null/omit it for a "type to filter" experience.

The frontend is the fragile half. The Vue input component ships precompiled — you can't patch it without forking, and a Nova upgrade is the most likely thing to break it (or the server-side base class). Pin the exact version, run CI against your production Nova version, and keep the fallback cost in mind: reimplementing this as an in-house Nova filter is roughly a day, so never let it become load-bearing.

Upgrade checklist. After any Nova/Laravel/PHP bump: load each resource index, apply each filter with blank, normal, wildcard, and Unicode input, and confirm row counts match a hand-written where() in Tinker. That's a 10-minute smoke test that catches nearly every breakage mode this package has.

Extension points beyond apply(): name() (panel label), key() (query param), and — depending on tag — a placeholder property/placeholder() method for the input hint. If the placeholder doesn't render, check the tag's README rather than assuming a bug; the prop wiring changed across versions. For anything more (AND/OR builders, saved filter sets, per-role filter permissions), this package is the wrong tool — build it, don't stretch it.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core