---
title: "Busy-period providers — Opening Hours for Laravel | Roundly"
description: "Feed availability from Eloquent queries, arrays, closures or your own provider — with no coupling to any booking package."
url: https://roundly-consulting.com/open-source/docs/opening-hours-for-laravel/busy-periods
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)

# Busy-period providers

Busy time comes from a BusyPeriodProvider, an iterable of BusyPeriod::make($start, $end, weight: 1), or a closure. Five providers ship: ArrayBusyPeriodProvider, ClosureBusyPeriodProvider, CompositeBusyPeriodProvider, NullBusyPeriodProvider and EloquentBusyPeriodProvider.

## From an Eloquent query

EloquentBusyPeriodProvider adapts any query to busy periods — map the columns, handle rows without an end, and say which timezone the raw values are stored in:

```php
use RoundlyConsulting\OpeningHours\Availability\Providers\EloquentBusyPeriodProvider;
use RoundlyConsulting\OpeningHours\Facades\OpeningHours;

$busy = EloquentBusyPeriodProvider::for(
        Appointment::query()->where('vet_id', $vet->id)->whereNotIn('status', ['cancelled']),
    )
    ->columns(start: 'starts_at', end: 'ends_at')
    ->durationColumn('duration_minutes')   // rows with a NULL end last this many minutes
    ->defaultDuration(minutes: 30)         // … or this, when the duration is NULL too
    ->maxNullEndMinutes(480)               // optional: the longest possible duration
    ->weightColumn(null)                   // or a column with capacity units
    ->storedIn('UTC');                     // timezone of the raw column values

OpeningHours::for($clinic)->availability()->withBusyPeriods($busy)->isAvailable($start, $end);
```

- columns(start:, end:) — the start and end columns.
- durationColumn() — rows with a NULL end last this many minutes; defaultDuration() when the duration is NULL too (or there is no duration column).
- weightColumn() — a column holding capacity units per row (null = weight 1).
- maxNullEndMinutes() — optional: the longest possible duration. A NULL-end row can only overlap a window if it started at most its duration before it; without this bound the provider finds it with one extra MAX(duration) query per read (or uses defaultDuration() when there is no duration column). Set it to skip that query — never lower than your longest duration, or longer bookings are missed and show as free.
- storedIn() — the timezone of the raw column values (default app.timezone). Storing booking times in UTC is recommended.

Column names are allow-list validated and values are bound. The provider selects only the named columns, caps the read at limits.busy\_periods, and resolves wall-clock values around DST changes with the same boundary rule as the engine.

## Combining sources

```php
use RoundlyConsulting\OpeningHours\Availability\BusyPeriod;
use RoundlyConsulting\OpeningHours\Availability\Providers\ArrayBusyPeriodProvider;
use RoundlyConsulting\OpeningHours\Availability\Providers\CompositeBusyPeriodProvider;
use RoundlyConsulting\OpeningHours\Facades\OpeningHours;

$maintenance = new ArrayBusyPeriodProvider([
    BusyPeriod::make($start, $end, weight: 2, reference: 'maintenance'),
]);

OpeningHours::for($clinic)->availability()
    ->withBusyPeriods(new CompositeBusyPeriodProvider($busy, $maintenance))   // the union of both
    ->isAvailable($from, $to);
```

## Your own provider

Implement BusyPeriodProvider::busyPeriodsBetween() and return the periods overlapping the window. Returning extra periods is fine — the engine clips:

```php
use Carbon\CarbonImmutable;
use RoundlyConsulting\OpeningHours\Availability\BusyPeriod;
use RoundlyConsulting\OpeningHours\Contracts\BusyPeriodProvider;

final class MaintenanceWindows implements BusyPeriodProvider
{
    public function busyPeriodsBetween(CarbonImmutable $start, CarbonImmutable $end): iterable
    {
        return MaintenanceWindow::query()
            ->where('starts_at', '<', $end)
            ->where('ends_at', '>', $start)
            ->get()
            ->map(fn (MaintenanceWindow $window) => BusyPeriod::make($window->starts_at, $window->ends_at));
    }
}
```

A BusyPeriod must end after it starts and weigh at least 1 (InvalidBusyPeriodException); more than limits.busy\_periods per evaluation throws TooManyBusyPeriodsException.

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