Fluent builder and concurrency
OpeningHours::edit($owner) — or the owner’s editOpeningHours() — returns a CalendarBuilder seeded with the current definition. Chain your changes and call save() — it validates and writes optimistically:
use RoundlyConsulting\OpeningHours\Builders\ScheduleBuilder;
use RoundlyConsulting\OpeningHours\Enums\Weekday;
use RoundlyConsulting\OpeningHours\Facades\OpeningHours;
OpeningHours::edit($clinic) // or $clinic->editOpeningHours()
->timezone('Europe/Bratislava')
->baseSchedule(fn (ScheduleBuilder $week) => $week
->weekdays('08:00-12:00', '13:00-17:00')
->saturday('09:00-12:00')
->closedOn(Weekday::Sunday))
->schedule(fn (ScheduleBuilder $week) => $week
->label('Summer')->yearly('07-01', '08-31')->priority(10)
->weekdays('07:00-14:00'))
->closed('2026-12-24', '2026-12-26', label: 'Christmas')
->closedYearly('01-01', label: 'New Year')
->exception('2026-10-17', ranges: ['10:00-12:00'], label: 'Short day')
->save();Schedule builder
- monday() … sunday(), weekdays(), weekend(), everyDay() and days([...], ...) set ranges; each day setter replaces that day’s ranges.
- closedOn(Weekday ...) and open24Hours(Weekday ...) mark closed or round-the-clock days.
- yearly('07-01', '08-31') sets a yearly window; between(), from() and until() set one-off windows; withoutWindow() turns it back into a base schedule.
- label(), priority() and meta() describe the schedule.
Calendar builder
- timezone(), label() and meta() set the calendar header.
- baseSchedule(fn …) edits the current base schedule (or creates one): the closure’s builder starts from its days, label, priority and meta, a day setter replaces that day and closedOn() clears it — pass a ScheduleData to replace the base schedule whole. schedule(fn …) always adds a new schedule; removeSchedule($id) and withoutSchedules() remove them.
- exception($from, $until, ranges:, label:, yearly:, meta:), closed() and closedYearly() add exceptions; removeException($id) and withoutExceptions() remove them.
- mergeOverlapping() merges overlapping and touching ranges instead of rejecting them — a range that overlaps nothing is kept exactly as given, and a merged range keeps a label, capacity or meta entry only when every range it absorbed agrees on it.
- violations() and toData() let you inspect before saving; build() returns an in-memory OpeningHours; save() persists.
Optimistic concurrency
A builder remembers the revision it loaded. If someone else saved in between, save() throws StaleOpeningHoursException (with the expected and actual revisions) and rolls back, instead of silently discarding their change:
use RoundlyConsulting\OpeningHours\Exceptions\StaleOpeningHoursException;
use RoundlyConsulting\OpeningHours\Facades\OpeningHours;
try {
OpeningHours::edit($clinic)
->closed('2026-12-24', '2026-12-26', label: 'Christmas')
->save();
} catch (StaleOpeningHoursException $e) {
// someone saved in between: $e->expected, $e->actual — reload and retry
}
OpeningHours::edit($clinic)
->expectRevision($request->integer('revision')) // a revision the client echoed back
->closed('2026-11-17', label: 'Public holiday')
->save();
OpeningHours::edit($clinic)
->ignoreConcurrentChanges() // last writer wins
->closedYearly('01-01', label: 'New Year')
->save();
OpeningHours::sync($clinic, $data, expectedRevision: 0); // "must not exist yet"Without a database
Build or check hours in memory — for an admin preview, an import or a test — before anything is written:
use RoundlyConsulting\OpeningHours\Builders\CalendarBuilder;
use RoundlyConsulting\OpeningHours\Builders\ScheduleBuilder;
use RoundlyConsulting\OpeningHours\OpeningHours;
$preview = OpeningHours::make(['week' => ['saturday' => ['22:00-03:00']]], 'Europe/Bratislava');
$builder = CalendarBuilder::make()
->timezone('Europe/Bratislava')
->baseSchedule(fn (ScheduleBuilder $week) => $week->weekdays('09:00-17:00'));
$builder->violations(); // ViolationList — empty when the definition is valid
$builder->toData(); // CalendarData
$builder->build(); // OpeningHours, in memoryShow your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.