---
title: "Business days and hours — Opening Hours for Laravel | Roundly"
description: "addOpenDays() and addOpenTime() count only open time — “2 business days”, “4 business hours” — skipping nights, weekends and holidays."
url: https://roundly-consulting.com/open-source/docs/opening-hours-for-laravel/business-time
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)

# Business days and hours

Deadlines and SLAs count in open time, not calendar time. addOpenDays() returns the same local time on the n-th open date after the start’s date (“2 business days”: Fri 15:00 → Tue 15:00); addOpenTime() returns the instant a CarbonInterval of open time later (“4 business hours”). Both skip closed weekdays, holidays, exceptions and dynamic exceptions, and both return a CarbonImmutable in the calendar’s output timezone:

```php
use Carbon\CarbonImmutable;
use Carbon\CarbonInterval;
use RoundlyConsulting\OpeningHours\Facades\OpeningHours;

OpeningHours::sync($office, [
    'timezone' => 'Europe/Bratislava',
    'week' => array_fill_keys(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], ['09:00-17:00']),
    'exceptions' => [['date' => '2026-11-09']],                                 // a holiday Monday
]);
OpeningHours::sync($office, ['timezone' => 'Europe/Bratislava', 'week' => ['saturday' => ['10:00-14:00']]], 'weekend');

$friday = CarbonImmutable::parse('2026-11-06 10:00', 'Europe/Bratislava');

// "N business days": the same local time on the n-th open date after the start's date
OpeningHours::addOpenDays($office, $friday, 2);                                // Wed 2026-11-11 10:00 — the holiday skipped
OpeningHours::addOpenDays($office, $friday, 1, 'weekend');                     // Sat 2026-11-07 10:00

// "N business hours": the instant that much open time later
OpeningHours::addOpenTime($office, $friday->setTime(16, 0), CarbonInterval::hours(2)); // Tue 2026-11-10 10:00
OpeningHours::addOpenTime($office, $friday, CarbonInterval::hours(5), 'weekend');      // Sat 2026-11-14 11:00
```

## Every entry point

The facade and the injected manager take the owner and an optional calendar key; the query object, the owner trait and the actions run the same code — a host rebind of AddOpenDaysAction or AddOpenTimeAction is honoured behind all of them:

```php
use Carbon\CarbonInterval;
use RoundlyConsulting\OpeningHours\Actions\AddOpenTimeAction;
use RoundlyConsulting\OpeningHours\Facades\OpeningHours;
use RoundlyConsulting\OpeningHours\OpeningHoursManager;

// The query object and the owner trait — shorthand over the same manager:
OpeningHours::for($office)->addOpenDays($friday, 2);                           // Wed 10:00
$office->addOpenDays($friday, 2);                                              // Wed 10:00
$office->addOpenTime($friday, CarbonInterval::hours(8));                       // Tue 10:00
$office->addOpenTime($friday, CarbonInterval::hour(), 'weekend');              // Sat 11:00

// The injected manager and the raw action:
app(OpeningHoursManager::class)->addOpenDays($office, $friday, 2);             // Wed 10:00
app(AddOpenTimeAction::class)->execute(OpeningHours::for($office), $friday, CarbonInterval::hours(8)); // Tue 10:00
```

## How they count

- An open date is one any range starts on — the same rule as isOpenOnDate() — with exceptions and dynamic exceptions applied, so a holiday is skipped and an exception that opens a closed day counts.
- addOpenDays() counts open dates strictly after the start’s local date and keeps the start’s wall-clock time, even when the office is closed at that time.
- addOpenTime() consumes only open time. Periods are half-open, so a start exactly at a close counts as closed (Mon 17:00 + 30 min → Tue 09:30), and an overnight run is consumed across midnight.
- addOpenTime() counts whole seconds of the interval — a fraction of a second is dropped.
- The start may be any DateTimeInterface and is read in the calendar’s timezone: Fri 23:30 in New York is already Saturday in Bratislava.
- Zero or a negative amount returns the start unchanged, in the output timezone.

```php
use Carbon\CarbonImmutable;
use Carbon\CarbonInterval;
use RoundlyConsulting\OpeningHours\OpeningHours;

$hours = OpeningHours::make([
    'week' => array_fill_keys(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], ['09:00-17:00']),
], 'UTC');

$hours->addOpenTime(CarbonImmutable::parse('2026-11-06 16:00', 'UTC'), CarbonInterval::hours(2));    // Mon 10:00
$hours->addOpenTime(CarbonImmutable::parse('2026-11-09 17:00', 'UTC'), CarbonInterval::minutes(30)); // Tue 09:30 — 17:00 is already closed
$hours->addOpenDays(CarbonImmutable::parse('2026-11-07 10:00', 'UTC'), 2);                           // Tue 10:00 — Saturday's time kept
$hours->addOpenDays(CarbonImmutable::parse('2026-11-07 10:00', 'UTC'), 0);                           // Sat 10:00 — zero or less returns the start
```

## Clock changes

addOpenDays() keeps the start’s wall-clock time on the target date. A time that occurs twice takes its first occurrence, as everywhere in the engine, but a time inside a spring-forward gap moves forward by the gap (02:30 → 03:30, as PHP resolves it) rather than to the transition instant — a deadline keeps its distance from the start. addOpenTime() counts real elapsed open time, so a 25-hour fall-back day holds 25 open hours:

```php
use Carbon\CarbonImmutable;
use Carbon\CarbonInterval;
use RoundlyConsulting\OpeningHours\OpeningHours;

// Europe/Bratislava springs forward on Sunday 2026-03-29 and falls back on Sunday 2026-10-25
$sundays = OpeningHours::make(['week' => ['sunday' => ['00:00-24:00']]], 'Europe/Bratislava');

$sundays->addOpenDays(CarbonImmutable::parse('2026-03-22 02:30', 'Europe/Bratislava'), 1);
// 2026-03-29 03:30 — 02:30 does not exist that day, so it moves forward by the gap

$sundays->addOpenDays(CarbonImmutable::parse('2026-10-18 02:30', 'Europe/Bratislava'), 1);
// 2026-10-25 02:30 +02:00 — 02:30 happens twice; the first occurrence

$sundays->addOpenTime(CarbonImmutable::parse('2026-10-25 00:00', 'Europe/Bratislava'), CarbonInterval::hours(24));
// 2026-10-25 23:00 — the day holds 25 real open hours
```

## The search window

Neither method searches forever. The answer must fall within opening-hours.search\_days local days of the start’s date — the whole span, not the gap between open days. A calendar that is never open, an owner without a calendar, or a span longer than the window throws OpenTimeNotFoundException, carrying searchDays:

```php
use RoundlyConsulting\OpeningHours\Exceptions\OpenTimeNotFoundException;
use RoundlyConsulting\OpeningHours\Facades\OpeningHours;

try {
    $due = OpeningHours::addOpenDays($office, now(), 2);
} catch (OpenTimeNotFoundException $e) {
    // The calendar has not enough open time within 366 days of the start (opening-hours.search_days).
    // $e->searchDays === 366
}
```

Under OpeningHours::fake() both methods still answer from the database and record nothing — they are reads.

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