---
title: "Fluent builder and concurrency — Opening Hours for Laravel | Roundly"
description: "Edit hours with CalendarBuilder and ScheduleBuilder, guard concurrent edits with revisions, and build calendars in memory."
url: https://roundly-consulting.com/open-source/docs/opening-hours-for-laravel/fluent-builder
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Opening Hours for Laravel](https://roundly-consulting.com/open-source/docs/opening-hours-for-laravel.md)

# 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:

```php
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 — on ScheduleBuilder::fromData() of a dated schedule, from() or until() changes one bound and keeps the other; 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:

```php
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:

```php
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 memory
```

## Show 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 crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/opening-hours-for-laravel.md)
