NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Opening Hours for Laravel

Defining opening hours

OpeningHours::sync() — or the owner’s setOpeningHours() — takes an array in the same shape the validation rule accepts. A top-level week is sugar for one base schedule:

use RoundlyConsulting\OpeningHours\Facades\OpeningHours;

OpeningHours::sync($clinic, [
    'timezone' => 'Europe/Bratislava',
    'week' => [
        'monday' => ['08:00-12:00', '13:00-17:00'],
        'friday' => ['08:00-15:00'],
        'saturday' => [['from' => '09:00', 'to' => '12:00', 'label' => 'Short day', 'capacity' => 2]],
    ],
    'exceptions' => [['date' => '12-25', 'label' => 'Christmas']],
]);

OpeningHours::sync($clinic, ['week' => ['monday' => ['14:00-16:00']]], 'pickup'); // a second calendar

// The owner trait is shorthand for the same call:
$clinic->setOpeningHours(['week' => ['monday' => ['14:00-16:00']]], 'pickup');

Every write is validated, runs in a transaction, replaces the calendar’s definition and bumps its revision exactly once.

The canonical shape

The full shape supports several schedules — one base schedule without a window plus seasonal ones with a window and a priority — and exceptions for single dates, spans and yearly dates:

[
    'timezone' => 'Europe/Bratislava',                  // optional, IANA only
    'label' => 'Reception',                              // optional
    'schedules' => [
        ['label' => 'Regular', 'week' => [
            'monday' => ['08:00-12:00', '13:00-17:00'],
            'friday' => ['22:00-03:00'],                 // overnight: ends 03:00 on Saturday
            'sunday' => ['00:00-24:00'],                 // 24 hours
        ]],
        ['label' => 'Summer', 'priority' => 10,
         'window' => ['from' => '07-01', 'until' => '08-31'],        // m-d = every year
         'week' => ['monday' => ['07:00-14:00']]],
        ['label' => 'New hours',
         'window' => ['from' => '2026-11-01'],                       // Y-m-d = one-off, open-ended
         'week' => ['monday' => ['09:00-18:00']]],
    ],
    'exceptions' => [
        ['date' => '12-25', 'label' => 'Christmas'],                           // yearly, closed
        ['from' => '12-24', 'until' => '01-02', 'label' => 'Holidays'],        // yearly, wraps the year
        ['from' => '2026-08-03', 'until' => '2026-08-14', 'label' => 'Break'], // one-off span, closed
        ['date' => '2026-10-17', 'ranges' => ['10:00-12:00'], 'label' => 'Short day'],
    ],
]

Format rules

  • Weekday keys accept monday, Monday, mon or 1–7 (ISO; 0 is rejected).
  • Ranges are HH:MM-HH:MM strings or {from, to, label?, capacity?, meta?} arrays; capacity runs from 1 to 1000.
  • An end of 00:00 means midnight (24:00). An end at or before the start makes the range overnight — it finishes the next day. 12:00-12:00 is an empty range and is rejected.
  • m-d dates repeat every year; Y-m-d dates are one-off. An explicit recurrence must agree with the format.
  • An exception without ranges is closed; with ranges it replaces that day’s hours. For exceptions, until defaults to from.
  • Yearly windows may wrap the year end (12-24 → 01-02). Schedule priority runs from −1000 to 1000.
  • Unknown keys are ignored; key, revision and updated_at (echoed by CalendarResource) are accepted, so a definition can be resubmitted as-is.

Schedules and leap days

A schedule without a window (null, {} or all-null) is the base schedule — there can be at most one. Seasonal schedules at the same priority must not overlap. A yearly 02-29 single date applies only in leap years; as a window start in a non-leap year it begins on 03-01, and as a window end it ends on 02-28.

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

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