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 Range Filter Laravel Package

rosamarsky/nova-range-filter

A Laravel Nova filter that adds range-based filtering to your resources, letting users narrow lists by numeric or date intervals instead of single values. Drop it into a Nova resource's filters array for quick between-style queries.

View on GitHub
Deep Wiki
Context7

Getting Started

Read the source before you composer require. This is a 5-star, 0-dependent MIT micro-package with a dormant upstream. Treat it as reference code you adopt and own, not infrastructure you consume. In this order:

  1. composer.json — check the laravel/nova constraint against your project. Nova major-version compatibility is the single most likely reason it fails, and it takes ten seconds to verify.
  2. src/ — the filter class: what it extends, its namespace, whether it ships a concrete filter or an abstract base you subclass, and how column() / name() are exposed.
  3. resources/js/ and dist/ — if dist/ is committed and registered via Nova::script() in a service provider, there is no Node build step for you. If not, you own a Vue build against Nova's toolchain, which changes the cost estimate from hours to days.
composer require rosamarsky/nova-range-filter

Confirm service-provider auto-discovery via extra.laravel.providers, or register it manually. Then subclass for your column:

// app/Nova/Filters/OrderTotalRange.php
use Rosamarsky\NovaRangeFilter\RangeFilter; // verify actual namespace/class in src/

class OrderTotalRange extends RangeFilter
{
    public function name() { return 'Order Total'; }
    public function column() { return 'total'; } // only if the base exposes this
}

Wire it into the resource:

public function filters(Request $request)
{
    return [new OrderTotalRange];
}

First day-to-day use case: a numeric range on an index screen — order totals, stock counts, ages, prices. Reach for it the first time an ops user asks "can I filter between 100 and 500?" and a SelectFilter with bucketed options feels wrong. Roll it out on one low-risk resource first, with production-like data volume, and inspect the generated SQL before shipping.

Implementation Patterns

The apply() contract is where all the work happens. Nova decodes the filter value into $this->value; the package's real job is the UI. Write (or override) apply() defensively — this three-branch shape is the pattern you will reuse for every range filter you build on top of the package:

public function apply(Request $request, $builder)
{
    $value = $this->value ?? [];
    $min = is_numeric($value['min'] ?? null) ? $value['min'] : null;
    $max = is_numeric($value['max'] ?? null) ? $value['max'] : null;

    if ($min === null && $max === null) {
        return $builder; // no constraint at all
    }

    if ($min !== null && $max !== null) {
        return $builder->whereBetween($this->column, [$min, $max]);
    }

    return $min !== null
        ? $builder->where($this->column, '>=', $min)
        : $builder->where($this->column, '<=', $max);
}

Single-sided ranges are the case the README is least likely to document and users hit most often.

One subclass per range, not one class parameterized. Nova filter state is keyed by class name. Two instances of the same filter class on one resource collide and share a value — results that look like a query bug but are a state bug. Create PriceRangeFilter, WeightRangeFilter, StockRangeFilter.

Reuse it for dates — with caveats. The component is just two inputs, so nothing stops you feeding whereBetween($col, [Carbon::parse($min), Carbon::parse($max)]). You lose the date picker, so users type 2024-01-01. Acceptable for internal admin panels, poor for anything user-facing — and for date-only columns prefer whereDate/startOfDay/endOfDay (see Gotchas).

Lenses come along for free. Filters apply to lenses as well as the index, so one range filter gives consistent behavior everywhere the model is queried through Nova. Audit any lens that overrides filters() — it will silently drop your range filter if it returns its own array.

Test apply() without a browser. This is pure query-builder manipulation and the cheapest way to pin down what the package actually sends you:

$filter = new OrderTotalRange;
$filter->value = ['min' => 10, 'max' => 20];
$query = $filter->apply(request(), Order::query());
$this->assertStringContainsString('between', $query->toSql());

Assert on ->toSql() to lock down query shape, and on results against SQLite to lock down behavior. Do this early — the value shape (['min' => ..., 'max' => ...] vs a delimited string) is the one thing docs may not spell out, and it is what your override depends on. Cover both-bounds, min-only, and max-empty cases.

Composing with other filters. Range filters narrow; a SelectFilter on the same column will AND with it. Nova does not deduplicate, so pick a convention (e.g. never pair a range filter with an option filter on the same column) rather than debugging contradictory WHERE clauses later.

Gotchas and Tips

Empty strings are the #1 bug. Nova's filter UI sends whatever the inputs hold; an untouched range input typically arrives as '', not null. whereBetween('total', ['', '']) silently compares against 0 on MySQL and returns nothing. Always coerce with is_numeric() (as above) or filled(). Symptom: "no results until I type both values."

whereBetween is inclusive on both ends. If your UI implies exclusive bounds, users will report off-by-one results on the exact boundary. Switch to >/< in apply() when it matters.

Qualify the column when joins are involved. The package has no opinion about your query shape. On a resource with a BelongsTo join or a lens with a join, an unqualified total becomes an ambiguous-column SQL error. Return 'orders.total' from column() and confirm the alias matches.

Dates and datetimes are a truncation trap. whereBetween('created_at', ['2024-01-01', '2024-01-31']) becomes BETWEEN '2024-01-01 00:00:00' AND '2024-01-31 00:00:00' — it silently excludes all of the 31st. Use whereDate, or parse to startOfDay()/endOfDay() explicitly. Timezones are on you too: Nova displays in the user's timezone, the DB stores UTC, and this filter does nothing to reconcile them.

NULLs are invisible. A row with total = NULL never satisfies BETWEEN, >=, or <=. Users reading "4 of 12 orders shown" will assume a bug. Decide whether to orWhereNull and document it — there is no config flag for this.

Decimals and loose typing. PHP-side is_numeric accepts '1e5' and leading whitespace; MySQL happily coerces 'abc' to 0 in a numeric comparison. If precision matters (money), validate with filter_var(..., FILTER_VALIDATE_FLOAT) and cast before binding rather than relying on the driver.

Nova version drift is the dominant failure mode. There are no community signals to warn you when a Nova upgrade breaks the filter — it just stops rendering or stops applying. Pin the version in composer.json. If you're on a Nova major the package wasn't built against, fork the class into app/Nova/Filters instead of debugging abandoned assets. The MIT license makes that a five-minute decision.

Own it before you ship it. Because the logic is small and upstream is dormant: vendor or pin it, add the apply() smoke tests above, write one line in your README naming the internal owner of the fork, and check the index on the filtered column. A range filter on an unindexed 5M-row table is a full scan every time a user touches the input.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
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
cgv/localized-resource