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

outl1ne/nova-input-filter

A Laravel Nova filter that adds a free-text input field to the filters panel, letting users type a value to filter resource listings without building a custom filter class each time.

View on GitHub
Deep Wiki
Context7

Getting Started

Prerequisites in one line each. You need a licensed Nova 4 or 5 install — the package ships a compiled Vue component built against Nova's frontend API, so Nova's major version is your hard compatibility ceiling. There is no published support matrix, so read vendor/outl1ne/nova-input-filter/composer.json constraints after install rather than trusting the README. Realistic floor: PHP 8.1+, Laravel 10–12. If you're on Nova 3 or a Nova fork, stop here.

Install (and then read, do not paste).

composer require outl1ne/nova-input-filter

Nothing else: no provider registration, no vendor:publish, no npm run build. That is the entire setup cost, and it is why this package wins its niche — it is a drop-in text input for the Nova filter panel.

Then spend five minutes on the source, because the source is the documentation:

vendor/outl1ne/nova-input-filter/src/InputFilter.php

You are looking for two things: the parent class (confirm the apply() contract against your installed Nova version) and the component property (which Vue component gets mounted). That reading time is your whole onboarding budget, and it's non-optional if you intend to own the dependency rather than merely consume it.

First working filter. Scaffold the class with Nova's generator and then change the parent:

php artisan nova:filter EmailFilter
namespace App\Nova\Filters;

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

class EmailFilter extends InputFilter
{
    public function name(): string
    {
        return 'Email contains';
    }

    public function apply(Request $request, $value)
    {
        if (blank($value)) {
            return $request;
        }

        return $request->where('email', 'like', '%'.$value.'%');
    }
}
public function filters(Request $request): array
{
    return [
        new EmailFilter,
    ];
}

The one line people get wrong: the parent is Outl1ne\InputFilter\InputFilter, not Laravel\Nova\Filters\Filter. Extending Nova's base class gives you a select-style filter with no input control — the panel renders, but wrong.

First day-to-day use case. Pick the resource your support or ops team currently files tickets against — User, Order, Shipment — and the column they ask engineering to look up: email, order number, tracking code, SKU. That is precisely the shape this package addresses. A fixed-option select filter degrades into a 500-item dropdown on those columns; a free-text input does not. Land it on one resource, pilot it for a week, and watch whether ad-hoc SQL requests to the team drop.

Verify it landed before you tell anyone. Load the index, open the filter panel, type, apply, then check four things:

  • The active-filter chip displays something legible (not a blank chip).
  • The value round-trips through the URL (?email_filter=...) on refresh and on back/forward navigation.
  • The emitted SQL matches your intent — DB::listen(), Telescope, or ->toSql() with bindings.
  • The empty case behaves as you decided it should (see Gotchas — this is the one users will misread).

If the panel renders an empty box or nothing at all, suspect the compiled Vue asset, not your PHP.

Implementation Patterns

1. Choose the search shape deliberately — they are not interchangeable

Four shapes, wildly different semantics and performance:

// Contains — fuzzy, matches user expectation, worst for indexes
return $request->where('email', 'like', '%'.$value.'%');

// Prefix — index-friendly, right for SKUs, codes, tracking numbers
return $request->where('sku', 'like', $value.'%');

// Exact — indexed equality, ideal for order numbers and IDs
return $request->where('order_number', $value);

Multi-column, grouped. Standardise on this when the input is meant to be a general "search" across a record:

public function apply(Request $request, $value)
{
    if (blank($value)) {
        return $request;
    }

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

The closure is not stylistic. Without it, orWhere escapes your filter and ORs against the entire resource query — silently returning rows that sibling filters were supposed to exclude. This is the most common bug in Nova filters, full stop.

2. Always bind parameters

apply() is your code, under your review boundary. The package supplies no query logic and therefore no protection — vendor code cannot save you here. Eloquent's where('col', 'like', '%'.$value.'%') binds the value and is safe. If you genuinely need a raw expression, bind it:

$request->whereRaw('LOWER(email) LIKE ?', ['%'.strtolower($value).'%']);

Never interpolate $value into whereRaw(). A naive copy of an example is a direct injection path from an authenticated admin surface — the highest-severity failure mode in the whole evaluation, and it lives entirely in your class.

3. One parameterised base class beats twelve copies

Once you have three of these, the duplication is the cost, not the dependency. The package gives you the component; you supply the domain shape:

abstract class ContainsFilter extends InputFilter
{
    abstract public function column(): string;
    abstract public function label(): string;

    public function name(): string { return $this->label(); }

    public function apply(Request $request, $value)
    {
        if (blank($value)) {
            return $request;
        }

        return $request->where($this->column(), 'like', '%'.$value.'%');
    }
}

class EmailFilter extends ContainsFilter
{
    public function column(): string { return 'email'; }
    public function label(): string  { return 'Email contains'; }
}

If the column is ever influenced by request input rather than hard-coded, resolve it through an allow-list array — never pass user data into the column position.

4. Relationship filtering works, but meter it

You own the query, so whereHas is available:

return $request->whereHas('customer', function ($query) use ($value) {
    $query->where('name', 'like', '%'.$value.'%');
});

That becomes a correlated subquery on every index load — every pagination click, every sort, every filter change. Fine on a narrow FK; painful on a wide relation.

5. Distinct key() per filter

Nova composes filters by key. If you derive several filters from a shared base class, give each a distinct key() — otherwise chips collide and one filter silently clobbers another's value in the URL. Symptom is maddening: the wrong filter appears active, or two appear applied when only one is.

6. Pair with a narrowing filter

The package doesn't scale; the query does. The practical mitigation is authoring, not configuration: put a date range, status, or tenant filter on the same resource so the LIKE only runs over a pre-narrowed set. That's a design decision inside your filters() method, and it's the difference between a usable admin index and a slow one.

7. Test apply() as plain PHP

The filter class is one method. You can instantiate it in a feature test and assert on the constructed query without booting the Nova frontend:

$filter = new EmailFilter;
$query  = User::query();
// assert SQL / bindings produced by $filter->apply($request, 'foo')

Two tests are mandatory: the populated case (assert bindings, not string interpolation) and the empty-value case — that's the behaviour users will misread.

8. Integrate the way Nova expects

Register filters in the resource's filters() method as normal. name() and key() come from Nova's filter contract; override them for legible chips and stable URL parameters. Beyond that there is nothing to integrate — it composes with built-in filters because Nova composes filters by key, not by type.

Gotchas and Tips

Empty input has no defined meaning until you define it. '' may arrive instead of null. Decide between "no-op" and "zero rows" and encode it with a blank($value) guard at the top of every apply(). Ambiguity here erodes trust in the panel faster than a missing feature — users cannot distinguish "filter applied, no matches" from "filter didn't apply," and they will ask you which one it was.

No documented debounce guarantee. Verify against the compiled component whether each keystroke hits Nova's index endpoint or only submission does. On a large table, per-keystroke requests against a leading-wildcard LIKE will feel broken even while technically working.

Leading wildcards kill B-tree indexes. '%'.$value.'%' cannot use an index — it's a sequential scan. Past low-hundreds-of-thousands of rows, either use prefix matching, add a trigram/fulltext index (Postgres pg_trgm + LIKE/ILIKE), or pair the filter with a narrowing constraint. Run EXPLAIN against production-like row counts during the spike, not after rollout.

Case sensitivity is a database quirk, not a package quirk. MySQL's default collation makes LIKE case-insensitive; Postgres makes it case-sensitive. The identical filter behaves differently across environments. Use ILIKE on Postgres or normalise both sides — and test on the engine you deploy on, not your local SQLite.

orWhere grouping — repeating because it bites. Any orWhere in apply() must live inside a where(function ($q) { ... }) closure. Without it, sibling filters from other classes are bypassed and you return rows no one asked for.

Nova major upgrades are the breakage vector. The package ships a compiled Vue asset against Nova's frontend API. A stale component typically fails silently — blank panel, input never mounts, no exception. Add "open every input filter on staging" to your Nova upgrade checklist. It cannot be reasoned about from source alone; it needs a real build and a real click.

Pin the Composer constraint. 24 stars, 0 public dependents, single-maintainer agency, last release January 2025. None of that disqualifies a small admin-layer primitive, but it does mean you should pin ("outl1ne/nova-input-filter": "1.x" or an exact tag) and treat version bumps as deliberate events.

Agree the fork path before you need it. The entire package is a base class plus a component — small enough to copy into app/Nova/Filters and drop the dependency in an afternoon. Decide now who would do that and roughly how, so an abandoned upstream is a routine task rather than an incident. This is the price of adopting 24-star tooling, and it's cheap.

Extension points are thinner than you'd like. name(), key(), and apply() are yours. A placeholder, inputmode, or type="number" are not in the PHP config — you'd have to override or re-register the Vue component via Nova's asset pipeline, which drags you back into the frontend build you were avoiding. If numeric or date semantics genuinely matter, use Nova's built-in date/number filters instead of fighting this one.

Debugging recipe, in order. When a filter "does nothing": (1) confirm the class extends Outl1ne\InputFilter\InputFilter, not Nova's base; (2) dump $value inside apply() — if it's never called, you have a frontend/asset problem, not a PHP one; (3) DB::listen() the index request to inspect SQL and bindings; (4) inspect the URL query string to confirm filter state persists; (5) check for a duplicate key() shadowing another filter.

Reuse threshold. On one resource, an in-house filter class is cheaper than the dependency. Two or more, and it pays for itself immediately. Either way, the query you write is the part that needs review — budget your time there, not on the wiring.

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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource