rpj/daterangepicker
Laravel package that adds a ready-made date range picker field to Blade forms, wrapping the popular daterangepicker widget with sensible defaults and server-side parsing so you can collect, validate, and read start/end dates cleanly in PHP.
1. Settle the identity question before you write a line of code. The repo description ("generic Laravel Blade date range component — configurable formats, preset ranges, locale, publishable assets") and the release notes ("Date Range filter for Laravel Nova 4") describe two different products with two different integration points. Ten minutes in vendor/ saves a day:
composer require rpj/daterangepicker:0.0.11 # exact pin, no caret — see Gotchas
ls -R vendor/rpj/daterangepicker
cat vendor/rpj/daterangepicker/composer.json
grep -R "extends" vendor/rpj/daterangepicker/src
What you're looking for: a class extending Laravel\Nova\Filters\Filter, a Laravel\Nova\Fields\Field, or a plain Illuminate\View\Component. That single fact decides whether your entry point is NovaServiceProvider::filters(), a resource's fields(), or a Blade include. Do not take the README's word for it.
2. Install against a Nova 5 staging instance.
php artisan vendor:publish --provider="Rpj\DateRangePicker\DateRangePickerServiceProvider" --tag=config
php artisan vendor:publish --provider="Rpj\DateRangePicker\DateRangePickerServiceProvider" --tag=assets
php artisan nova:publish
php artisan optimize:clear && php artisan view:clear
Then hard-reload the browser (Cmd/Ctrl+Shift+R). Stale compiled Nova JS is the single most common cause of "the filter renders but does nothing."
3. Where to look first, in order of usefulness:
vendor/rpj/daterangepicker/src/ — the provider, the filter class, and its apply() / options() methods. The apply() signature tells you exactly what value shape you'll receive.vendor/rpj/daterangepicker/config/ — date format, locale, display behaviour.vendor/rpj/daterangepicker/composer.json — its illuminate/* and laravel/nova constraints. Cross-check with composer show laravel/nova.resources/js/ inside the package, if present — this is the Vue component Nova registers; knowing its name helps when debugging via browser console.4. First use case: a Nova resource index filter on a reporting-ish resource. Orders, audit logs, webhook deliveries, an activity feed. It's read-only, admin-scoped, and directly valuable:
// app/Nova/Order.php
public function filters(Request $request): array
{
return [
new \Rpj\DateRangePicker\DateRangeFilter,
];
}
Prove one screen end-to-end — select a range, watch the network tab, confirm the query it produces — before you roll it out to a second resource. If the artefact turns out to be a Blade component rather than a Nova filter, the equivalent first target is one admin report page with ?from=&to= query params and a form request doing the validation.
Register once, reuse everywhere. Multiple resources needing the same filter is the normal case within a week. Use a trait, not copy-paste:
trait HasDateRangeFilter
{
public function filters(Request $request): array
{
return array_merge(parent::filters($request), [
new \Rpj\DateRangePicker\DateRangeFilter,
]);
}
}
Normalise the submitted value exactly once. Nova hands apply() an opaque value whose shape depends on the package's Vue component. Parse it in one method and reuse that method — never parse inline at call sites:
public function apply(Request $request, $query, $value)
{
[$from, $to] = $this->parseRange($value);
if (! $from || ! $to) {
return $query; // fail open — an empty filter must not empty the table
}
return $query->whereBetween('created_at', [$from, $to]);
}
protected function parseRange(mixed $value): array
{
// Handle the shapes you will actually see:
// "2025-01-01,2025-01-31" | ['from' => ..., 'to' => ...] | null | ''
if (blank($value)) {
return [null, null];
}
// ...normalise to Carbon pairs...
return [
Carbon::parse($from, config('app.timezone'))->startOfDay()->utc(),
Carbon::parse($to, config('app.timezone'))->endOfDay()->utc(),
];
}
Log the raw $value on the first run of a new Nova minor. Its shape is the thing most likely to silently change.
Extract the range out of Nova so exports and APIs share the semantics. This is the pattern that actually pays off day-to-day:
// app/Support/DateRange.php
final class DateRange
{
public function __construct(
public readonly Carbon $from,
public readonly Carbon $to,
) {}
public function applyTo(Builder $query, string $column = 'created_at'): Builder
{
return $query->whereBetween($column, [$this->from, $this->to]);
}
}
Resolve it from the request in a service provider, inject it into the Nova filter and the CSV export controller and the API endpoint. You've now eliminated the "the screen says January but the CSV has February" bug class permanently.
Pass resolved ranges into queued jobs, never re-derive them. A job that calls Carbon::now() or re-parses a picker string will drift between click time and worker time. Serialise as ISO-8601:
ExportOrders::dispatch($range->from->toIso8601String(), $range->to->toIso8601String());
Compose filters without mutating shared state. Return a modified query and let Nova chain. If you're ever unsure of ordering, assert on the SQL:
expect($query->toSql())->toContain('between');
Testing pattern. You can't render the Vue component in CI, but the contract is testable:
it('applies an inclusive day range', function () {
$query = Order::query();
(new DateRangeFilter)->apply(request(), $query, '2025-01-01,2025-01-31');
expect($query->getBindings())
->toContain('2025-01-01 00:00:00')
->toContain('2025-01-31 23:59:59');
});
it('registers on the resource', function () {
expect(collect((new Order)->filters(request())))
->toContain(fn ($f) => $f instanceof DateRangeFilter);
});
The second test is a one-liner that turns a Nova minor bump into a red CI job instead of a support ticket.
Blade path (if that's what shipped). Keep the picker dumb: submit from and to as separate inputs, validate with date|before_or_equal:to, and derive the range in a form request. Never trust a combined string like "2025-01-01 - 2025-01-31" without re-parsing it server-side.
Pin the exact version. At 0.0.11, both ^ and ~ permit breaking changes on a patch bump. Use "rpj/daterangepicker": "0.0.11" with no operator, and disable Dependabot auto-merge for it specifically.
GPL-3.0 is a gating item, not a footnote. Everything else in your Nova stack is almost certainly MIT. GPL-3.0 alongside commercially-licensed Nova is fine for internal admin-only use in most organisations, but becomes a real problem the moment you redistribute, ship an on-prem appliance, or hand source to a client. Route it to legal before you build anything on top — that answer flips the whole build-vs-buy decision.
"Works with Nova 5 without any change" is a claim, not evidence. 0.0.11 bumped a constraint string; there's no CI matrix, no test suite, no dependents. Verify by rendering the filter on a Nova 5 screen, exercising a selection, and inspecting the resulting query in Telescope or the query log. Field serialisation and Vue component registration are the two likeliest silent breakages.
Silent failure is the dominant failure mode. A blank dropdown, a missing filter, or a filter that renders but sends null all throw nothing. Debug order: (1) is it registered in filters()? (2) php artisan nova:publish, (3) hard-reload, (4) check public/vendor/ for the package's assets, (5) browser console for a failed component registration. Nine times out of ten it's step 3 or 4.
Decide timezone and boundary semantics once, in writing. Two questions: is $to inclusive of the whole end day (endOfDay()) or exclusive (< $to + 1 day)? And do you parse in app.timezone then convert to UTC, or store local? Put the answer in parseRange() and a comment above it. Per-call-site decisions here are the number one source of "one day is missing" reports.
Check for constraint conflicts before committing. composer why-not rpj/daterangepicker 0.0.11 and composer why-not laravel/nova ^5. A transitive illuminate/* pin can block your entire Nova upgrade, not just this feature. Sequence adoption after the Nova 4→5 migration as an isolated change — bundling them means a broken admin panel with two possible causes.
Nova's private Composer repo must exist in every CI job and every Docker build. NOVA_USERNAME/NOVA_PASSWORD or an auth.json. When it's missing you get "package not found" errors that look like the daterangepicker's fault and aren't.
whereBetween('created_at', ...) on an unindexed column full-scans. Confirm the index exists before someone selects a three-year range on a table with millions of rows, and consider capping server-side ($to->diffInDays($from) <= 366) rather than trusting the UI.
Customise via extension points, never by editing vendor/. Clean seams: subclass the shipped filter and override apply() or options(); publish and edit the config for format and locale; override the Vue component only as a last resort. The moment you patch files in vendor/, you own a package you didn't want to own.
Have a fork fallback ready. Five stars, zero dependents, single maintainer, no stated support policy. Before production, add a repositories entry pointing at a VCS fork you control, so "maintainer disappears" is a composer require change rather than an incident.
Assign a named owner and put it on the Nova upgrade checklist now — while the context is fresh. Someone has to read the diff on every major Nova release, and that someone should be a person, not a team.
How can I help you explore Laravel packages today?