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

Owners and calendars

Implement the OpeningHoursOwner contract with the HasOpeningHours trait on any model — a clinic, a shop, a branch, a person, a meeting room. Override openingHoursTimezone() to evaluate calendars without their own timezone in the owner’s:

use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\OpeningHours\Concerns\HasOpeningHours;
use RoundlyConsulting\OpeningHours\Contracts\OpeningHoursOwner;

final class Clinic extends Model implements OpeningHoursOwner
{
    use HasOpeningHours;

    // Optional: evaluate calendars without their own timezone in the clinic's.
    public function openingHoursTimezone(): ?string
    {
        return $this->timezone;
    }
}

openingHours() is a method that returns the query object, not a relation — reading $clinic->openingHours as a property throws. The relation is openingHoursCalendars(); use it for eager loading and whenLoaded() in resources.

Named calendars

An owner can have several independent calendars — default, pickup, reception… Every method takes an optional calendar key; without one it uses opening-hours.default_calendar:

use RoundlyConsulting\OpeningHours\Facades\OpeningHours;

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

OpeningHours::for($clinic, 'pickup')->isOpenDuring($from, $to);
OpeningHours::has($clinic, 'pickup');        // bool
OpeningHours::calendar($clinic, 'pickup');   // ?Calendar — the live header

// The owner trait is shorthand for the same calls:
$clinic->openingHours('pickup');
$clinic->hasOpeningHours('pickup');
$clinic->openingHoursCalendar('pickup');
$clinic->openingHoursCalendars;              // every calendar of the owner (MorphMany)

Owner API

MethodPurpose
openingHoursCalendars()MorphMany relation over every calendar header of the owner — eager load it or use it in whenLoaded().
openingHoursCalendar(?$calendar)The live ?Calendar header for a key; a loaded relation is used without a query.
openingHours(?$calendar)The OpeningHours query object; an empty (closed) calendar when the key doesn’t exist yet.
hasOpeningHours(?$calendar)Whether the calendar exists.
setOpeningHours($data, ?$calendar, ?$expectedRevision)Full replace from an array or CalendarData; returns the new OpeningHours.
editOpeningHours(?$calendar)A CalendarBuilder seeded with the current definition; optimistic save().
openingHoursTimezone()Hook — the owner’s timezone; default null.
openingHoursDynamicExceptions()Hook — a list of DynamicExceptionProvider; default [].
withOpeningHours(definitions: false)Scope — eager-load calendar headers (plus full definitions with definitions: true).
whereOpenAt() / whereOpenThroughout()Scopes over materialized intervals (opt-in).

Deleting owners

With delete_with_owner on (the default), permanently deleting an owner — a model without SoftDeletes, or forceDelete() — removes its calendars too. Soft-deleting the owner keeps them. Mass deletes such as Clinic::query()->delete() fire no model events and leave the calendars behind, because polymorphic owners cannot carry a foreign key.

Using the trait on a model that doesn’t implement OpeningHoursOwner throws InvalidOwnerException.

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.