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

Timezones and DST

A calendar is evaluated in its own timezone → the owner’s openingHoursTimezone() → opening-hours.timezone → app.timezone. Only IANA names are accepted; fixed offsets such as +02:00 are rejected with InvalidTimezoneException. withOutputTimezone() changes only how instants are reported, never the answer:

$hours = OpeningHours::for($clinic);   // or $clinic->openingHours()

$hours->timezone();                                // the zone the calendar is evaluated in
$hours->withOutputTimezone('UTC')->nextClose();    // same instant, reported in UTC

$visitor = $hours->withOutputTimezone('America/New_York');
$visitor->nextOpen();                              // reported in New York time
$visitor->outputTimezone();                        // DateTimeZone('America/New_York')

The boundary rule

Every wall-clock boundary follows one rule: it resolves to the first instant at which the local clock reads that time or later.

  • Spring forward (02:30 does not exist): a boundary at 02:30 is the moment the clock jumps (03:00). A 01:00-02:30 range lasts one real hour that day.
  • Fall back (02:30 happens twice): the first occurrence — for starts and ends, in every zone.
  • A 00:00-24:00 day is 23 or 25 hours long on transition days; durations are real time.
  • Dates given as instants are converted to the calendar timezone first; the weekday is the local one.
use Carbon\CarbonImmutable;
use RoundlyConsulting\OpeningHours\OpeningHours;

// Europe/Bratislava falls back on Sunday 2026-10-25 (03:00 → 02:00)
$sundays = OpeningHours::make(['week' => ['sunday' => ['00:00-24:00']]], 'Europe/Bratislava');
$sunday = CarbonImmutable::parse('2026-10-25 00:00', 'Europe/Bratislava');
$sundays->openSecondsBetween($sunday, $sunday->addDay());   // 90000 — 25 real hours

// … and springs forward on Sunday 2026-03-29 (02:00 → 03:00)
$gap = OpeningHours::make(['week' => ['sunday' => ['01:00-02:30']]], 'Europe/Bratislava');
$period = $gap->currentPeriod(CarbonImmutable::parse('2026-03-29 01:30', 'Europe/Bratislava'));
$period?->end->format('c');                                 // 2026-03-29T03:00:00+02:00 — one real hour

PHP itself resolves ambiguous times to different occurrences in different zones, so the engine computes boundaries from each zone’s transition table instead. It is checked against an independent brute-force oracle across real transitions — including a 30-minute shift (Australia/Lord_Howe), a +05:45 offset (Asia/Kathmandu), a midnight transition (America/Santiago) and a skipped day (Pacific/Apia).

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.