---
title: "Defining opening hours — Opening Hours for Laravel | Roundly"
description: "Save hours from an array — weekly ranges, seasonal schedules and exceptions — in the same canonical shape the validation rule accepts."
url: https://roundly-consulting.com/open-source/docs/opening-hours-for-laravel/defining-hours
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)

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

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

```php
[
    '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 and must fall in the years 1900–2200 (invalid\_date otherwise, also for a LocalDate built in code). 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 window clashes with a dated one only when that window contains a Feb 29. 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](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)
