armincms/nova-opening-hours
A Laravel Nova field for managing weekly opening hours in your admin panel. Define multiple time slots per day, add exceptions, and store schedules as JSON for easy display on your frontend. Simple to integrate into Nova resources and customize for business hours.
Install with composer require <vendor>/<package> and confirm the version constraint — this is a 0.x release, so pin it ("^0.3") rather than floating. Minor bumps in 0.x are allowed to break.
Read in this order: README.md → src/Rules/ (the rule classes, including the new Overlap rule) → tests/ (the tests are the real specification for edge-case behaviour) → config/ if the package ships one (php artisan vendor:publish --tag=<package>-config).
Check composer.json for the supported Laravel/PHP matrix before adopting. Rule classes are usually plain Illuminate\Contracts\Validation\ValidationRule (Laravel 10+) or Rule (older) implementations, so they need no service provider bootstrapping — but confirm whether the provider registers translations or a facade.
First use case to try: block double-booking. In a form request:
public function rules(): array
{
return [
'starts_at' => ['required', 'date'],
'ends_at' => ['required', 'date', 'after:starts_at'],
'room_id' => ['required', 'integer', new Overlap($this->starts_at, $this->ends_at)],
];
}
Run it against a fixture where two ranges share an endpoint, one is fully contained, and one is adjacent-but-not-intersecting. Those three cases define the rule's contract and are exactly where implementations differ.
Carbon, IDs) so the rule stays serialisable for queued/async validators.after:starts_at, date_format:Y-m-d H:i, and nullable belong on the same fields — the overlap rule should only answer "does this collide", not "is this a valid date".bookings.*.starts_at and pass the sibling values via $this->input('bookings.*.starts_at') or validate a normalised DTO first. Normalise before validating; string-vs-Carbon comparison is the most common source of false negatives.BookingRequest base class or a Rule::class constant rather than repeating constructor arguments.after() / withValidator() for cross-record checks when the overlap depends on database state (existing bookings). Keep the rule itself pure where possible so it stays unit-testable; do the query in a Rule::exists-style closure or a dedicated withValidator hook and pass the resulting ranges in.(new Overlap(...))->passes('field', $value) gives fast, table-driven tests for the half-open vs closed interval question — cover it once, explicitly, and encode the answer in test names.assertInvalid in a feature test rather than per-surface.[start, end) or [start, end] is used. A booking ending at 10:00 and one starting at 10:00 should usually not collide; if the rule reports a conflict there, either pass adjusted endpoints or wrap it. Write the assertion down in a test.null end date (ongoing booking) is the classic silent pass — confirm whether the rule treats null as "infinity" or skips validation. Handle it with sometimes/explicit defaults rather than assuming.Y-m-d, Carbon, timestamps, unix ints) are compared inconsistently. Cast to CarbonImmutable in UTC at the form-request layer.Carbon::parse() picks up app.timezone; stored UTC values will be off by hours. Compare in a single, explicit timezone.nullable/sometimes before the overlap rule; bail will stop at the first failure and can mask which side of a collision actually failed, making debugging harder.:message in the attribute array ('room_id.overlap' => '...') and check whether the package publishes a lang/ directory — a missing translation key surfaces as the raw key string in the response.where('starts_at', '<', $end)->where('ends_at', '>', $start)) and use the rule only for in-memory/request-scope checks.0.x upgrade discipline. Read the diff on every minor. New rules in this release line up with a rule-object pattern, so extension points are typically: implement ValidationRule yourself, or subclass/extend the package rule to change interval semantics instead of forking.bail/nullable may short-circuit before it.How can I help you explore Laravel packages today?