marshmallow/nova-date-range-filter
A Laravel Nova filter that lets users narrow resource listings by a date range using native from/to pickers. Drop it into any Nova resource to filter records on date or datetime columns, with a clean UI and simple setup.
Architecture fit. The package occupies the narrowest possible seam in a Laravel application: the Nova admin presentation layer. It ships a filter class that plugs into a resource's filters() method, a Vue component for the picker, and (as of v2.3.0) lang/ files. There are no migrations, no config file, no published assets requiring service-provider work beyond Composer package discovery, and no touchpoints with domain models, APIs, or queue/event infrastructure. For a TPM this is the ideal shape of a dependency: the blast radius of failure is "an admin screen's filter misbehaves," and the blast radius of removal is "delete one line and composer remove." It is not a framework, not a query layer, and not something other code will grow to depend on — properties that make the buy-vs-build call easy and the reversal cost near zero.
Integration feasibility. Integration is a composer require, a single DateRangeFilter::make($label, $column) line per resource, and a browser check. A Nova-fluent developer is done in under an hour for the first resource; the remaining cost is only the number of resources you standardize. Two integration details carry disproportionate weight and must be read from source rather than assumed: the make() argument order (label-first vs. column-first) and the exact apply(Request $request, $query, $value) implementation. That method is the package as far as data correctness is concerned — it determines end-date inclusivity, whether it uses whereBetween on raw columns (sargable) versus whereDate() (index-defeating), and whether timezones are handled at all.
Technical risk. The dominant risk is not availability or licensing — it is silent misreporting. A range filter that excludes records after 00:00:00 on the end date produces numbers that look plausible and are wrong; an operator cannot detect it from the screen. Secondary risks are coupling and ownership: the real forced-upgrade trigger is a Nova major version bump, not a release of this package, and the project is bus-factor-1 (13 stars, 0 dependents, no commercial support).
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| End-date exclusivity skews month-end reports | Medium | High (silent) | Read apply(); subclass and normalize; test a 23:59:59 record |
| Timezone boundary shifts "Today" for non-UTC operators | Medium | High (silent) | Explicit startOfDay()->utc() / endOfDay()->utc() in an override |
| Nova major upgrade breaks the Vue component | Medium (over 12–18 mo) | Medium | Add a line to the Nova upgrade runbook; re-verify post-bump |
| Unindexed date column → slow index pages | Medium | Medium | Verify against the highest-volume resources; add composite indexes |
Locale gap ("UK" = uk vs en_GB) |
High (ambiguous) | Low | Verify against lang/ before promising localized UI |
| Maintainer abandonment | Low–Medium | Low | Pre-agree fork/vendor as the fallback, not an incident-time discovery |
Key questions to close before commit.
composer.json declare support for our pinned Nova major? (Hard gate — do not proceed if absent.)apply() actually do — inclusive or exclusive end date, whereBetween on raw columns, timezone-aware or naive?$value when it reaches apply(), and is it validated for partial/malformed input (user picks start only)?vendor:publish tag for lang/? If not, our locale additions live in vendor/ and get wiped by composer update.uk) or United Kingdom (en_GB)?->default() and ->canSee() on filters (needed for defaulted ranges and role-scoped visibility)?Recommendation: Conditional proceed. Accept the dependency for internal admin tooling; treat end-date semantics and timezone handling as a correctness gate rather than an implementation detail.
Stack fit. Laravel + Laravel Nova is a hard prerequisite. On a non-Nova Laravel app the package delivers nothing — a general query-scope approach is the right answer there. Within a Nova app, fit is essentially frictionless: Composer autoloading, package discovery, and a filters() registration match the framework's own extension idiom.
Migration path. There is no data migration and no schema change. The path is additive and reversible in both directions:
composer require marshmallow-packages/nova-date-range-filter → add the filter to one resource's filters() → verify in the browser.filters() line(s) → composer remove. No orphaned tables, columns, or config. This makes the decision low-commitment, which in turn means it can be made at tech-lead/PM level without an architecture review.Compatibility. Two axes matter. First, the Nova major version constraint — verify it explicitly against composer.json before installation, because Nova's front-end asset pipeline is the actual coupling point; a Nova bump is when this filter breaks, not a package release. Second, locale files: v2.3.0 is purely additive over 2.2.2 (EN/UK/NL translations), so upgrading within v2.x is a Composer bump with no code change on our side. Pin the version and let Renovate/Dependabot surface bumps rather than floating.
Sequencing.
src/DateRangeFilter.php and apply() line by line, decode the base64 filters query parameter in devtools to see what the front end actually sends. Confirm the Nova version gate and the uk/en_GB question here.filters.apply() that normalizes start to startOfDay()->utc() and end to endOfDay()->utc(), and unit-test apply() directly against ->toSql() and bindings — including a record at 23:59:59 on the end date and a DST-transition window.HasDateRangeFilters trait so label/column definitions cannot drift, and roll out across the resource set.lang/ if a tag exists and commit to lang/vendor/...; otherwise contribute the locale upstream rather than patching vendor/.Maintenance. The maintenance surface is small but unowned-by-default, which is the thing to fix. With zero dependents and a single maintainer, upstream responsiveness is not a plan we can build on. Pin the version, route update PRs through a named owner, keep our registration footprint to a handful of lines plus one subclass, and pre-agree that the fallback is vendoring or forking — a sub-hour operation precisely because nothing in the domain model or API depends on this package. Never edit lang/ files in vendor/.
Support. There is no support contract and no SLA. Internal support is therefore: (a) a runbook entry for "filter shows no/obviously wrong results" that starts by decoding the base64 filters parameter — half of all "the filter does nothing" reports end there; (b) a known-answer check for the end-date boundary; (c) the fork fallback for anything requiring upstream changes. If an SLA on admin tooling were ever a hard requirement, this package would be disqualified — it is not, for internal screens.
Scaling. Package scaling is a non-issue; query scaling is ours. Range predicates on raw datetime columns stay sargable and will use a B-tree index, so the prerequisite is that the filtered column is indexed. On high-volume resources (orders, activity logs), check the composite case as well: a date range ANDed with a status filter becomes where status = ? and created_at between ? and ?, which wants a composite index (status, created_at), not two single-column indexes. Any override we write must avoid whereDate(), which wraps the column in a function and quietly kills index usage. Cap the number of filters registered per index; each one adds a predicate and a UI control, and operator legibility degrades before the database does.
Failure modes.
apply() and boundary tests.whereDate() in an override makes index pages slow or time out under operator load.$value shapes reaching apply() — validate/normalize rather than trusting the UI.Ramp-up. Developer ramp-up is under a day for anyone Nova-fluent, dominated by reading apply() rather than by the install. Operator ramp-up is minutes — presets plus a custom range are self-explanatory — but one thing needs explicit communication: what the end date means. If the team ships the package's default semantics rather than the hardened subclass, ops must be told the end date is effectively exclusive, or month-end exports will be quietly wrong. Standardizing one date-filter pattern across resources is the product payoff here: operators learn the behavior once instead of relearning a bespoke filter per screen, and the recurring "can we filter the admin by date?" request stream closes without custom-build capacity.
How can I help you explore Laravel packages today?