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

The config is merged, so you override only what you need — and most knobs are env-backed. The published config/opening-hours.php:

use RoundlyConsulting\OpeningHours\Models\Calendar;
use RoundlyConsulting\OpeningHours\Models\ExceptionRule;
use RoundlyConsulting\OpeningHours\Models\Schedule;

return [
    'key_type' => env('OPENING_HOURS_KEY_TYPE', 'bigint'),
    'models' => [
        'calendar' => Calendar::class,
        'schedule' => Schedule::class,
        'exception_rule' => ExceptionRule::class,
    ],
    'default_calendar' => env('OPENING_HOURS_DEFAULT_CALENDAR', 'default'),
    'timezone' => env('OPENING_HOURS_TIMEZONE'),
    'first_day_of_week' => env('OPENING_HOURS_FIRST_DAY_OF_WEEK', 'monday'),
    'search_days' => env('OPENING_HOURS_SEARCH_DAYS', 366),
    'max_query_days' => env('OPENING_HOURS_MAX_QUERY_DAYS', 366),
    'delete_with_owner' => env('OPENING_HOURS_DELETE_WITH_OWNER', true),
    'limits' => [
        'calendars' => 16, 'schedules' => 20, 'ranges_per_day' => 12, 'exceptions' => 1000,
        'label_length' => 191, 'meta_bytes' => 4096, 'busy_periods' => 10000, 'slots' => 2000,
    ],
    'cache' => [
        'enabled' => env('OPENING_HOURS_CACHE_ENABLED', true),
        'store' => env('OPENING_HOURS_CACHE_STORE'),
        'ttl' => env('OPENING_HOURS_CACHE_TTL', 86400),
        'prefix' => env('OPENING_HOURS_CACHE_PREFIX', 'opening-hours'),
    ],
    'api' => ['expose_meta' => false, 'week_mode' => 'upcoming', 'upcoming_exceptions_days' => 60],
    'prune' => [
        'exceptions_after_days' => env('OPENING_HOURS_PRUNE_EXCEPTIONS_AFTER_DAYS'),
        'trashed_after_days' => env('OPENING_HOURS_PRUNE_TRASHED_AFTER_DAYS'),
    ],
    'materialize' => [
        'enabled' => env('OPENING_HOURS_MATERIALIZE', false),
        'days_ahead' => env('OPENING_HOURS_MATERIALIZE_DAYS_AHEAD', 60),
        'days_behind' => env('OPENING_HOURS_MATERIALIZE_DAYS_BEHIND', 1),
        'connection' => env('OPENING_HOURS_QUEUE_CONNECTION'),
        'queue' => env('OPENING_HOURS_QUEUE'),
    ],
    'facade_alias' => 'OpeningHours',
];

Every key

KeyDefaultEnvPurpose
key_typebigintOPENING_HOURS_KEY_TYPEOwner primary-key type (bigint, uuid, ulid); sets the owner_id column type in the migrations. Any other value throws.
models.calendarCalendar::class—Calendar model; swap in a subclass (anything else throws).
models.scheduleSchedule::class—Schedule model; swap in a subclass (anything else throws).
models.exception_ruleExceptionRule::class—Exception model; swap in a subclass (anything else throws).
default_calendardefaultOPENING_HOURS_DEFAULT_CALENDARCalendar key used when none is given.
timezonenullOPENING_HOURS_TIMEZONEFallback timezone before app.timezone (IANA names only; not set — null or blank — = app.timezone).
first_day_of_weekmondayOPENING_HOURS_FIRST_DAY_OF_WEEKOrdering of forWeek(), forWeekOf() and the calendar-week API mode.
search_days366OPENING_HOURS_SEARCH_DAYSHow many local days after/before the given day next*/previous* and nextAvailableSlot() look (1–3660); beyond it they return null.
max_query_days366OPENING_HOURS_MAX_QUERY_DAYSLargest span a span query or slot search may cover (1–3660); nextAvailableSlot() and the status resource stay within it on their own.
delete_with_ownertrueOPENING_HOURS_DELETE_WITH_OWNERRemove calendars when their owner is deleted permanently.
limits.calendars16—Named calendars per owner.
limits.schedules20—Schedules per calendar.
limits.ranges_per_day12—Ranges per weekday / per exception.
limits.exceptions1000—Exceptions per calendar.
limits.label_length191—Maximum label length.
limits.meta_bytes4096—Maximum JSON size of a meta payload.
limits.busy_periods10000—Busy periods read per availability evaluation.
limits.slots2000—Maximum slots per slot query.
cache.enabledtrueOPENING_HOURS_CACHE_ENABLEDCache compiled definitions.
cache.storenullOPENING_HOURS_CACHE_STORECache store (not set — null or blank — = default store; a set value must be a string).
cache.ttl86400OPENING_HOURS_CACHE_TTLSeconds; null = forever (a blank env value is not set, so 86400).
cache.prefixopening-hoursOPENING_HOURS_CACHE_PREFIXCache key prefix (a string; blank = not set = opening-hours).
api.expose_metafalse—Include meta in API resources.
api.week_modeupcoming—Status resource week: upcoming (next 7 dates) or calendar_week.
api.upcoming_exceptions_days60—Look-ahead of the status resource (capped at max_query_days − 1) and structured data.
prune.exceptions_after_daysnullOPENING_HOURS_PRUNE_EXCEPTIONS_AFTER_DAYSPrune one-off exceptions that ended N days ago (not set — null or blank — = off).
prune.trashed_after_daysnullOPENING_HOURS_PRUNE_TRASHED_AFTER_DAYSPurge soft-deleted rows older than N days (not set — null or blank — = off).
materialize.enabledfalseOPENING_HOURS_MATERIALIZEMaintain the opening_hours_intervals table for SQL scopes.
materialize.days_ahead60OPENING_HOURS_MATERIALIZE_DAYS_AHEADMaterialized horizon forward (days).
materialize.days_behind1OPENING_HOURS_MATERIALIZE_DAYS_BEHINDMaterialized horizon backward (days).
materialize.connectionnullOPENING_HOURS_QUEUE_CONNECTIONQueue connection of the materialization job (not set — null or blank — = default; else a string).
materialize.queuenullOPENING_HOURS_QUEUEQueue of the materialization job (not set — null or blank — = default; else a string).
facade_aliasOpeningHours—Global facade alias; null or a false spelling (false, 0, off, no) disables it; blank is not set and keeps OpeningHours.

Env values arrive as strings and are validated when read: the switches (delete_with_owner, cache.enabled, materialize.enabled, api.expose_meta) take true/1/on/yes or false/0/off/no, the numeric keys take whole numbers within their range, and key_type, first_day_of_week and api.week_mode take one of their listed values. A blank value ('' or whitespace, as a bare OPENING_HOURS_SEARCH_DAYS= line gives) is not set and takes the default in the table. Anything else — a typo such as OPENING_HOURS_CACHE_ENABLED=disabled or OPENING_HOURS_SEARCH_DAYS=abc, a non-string cache store, prefix, queue or connection, or a timezone that isn’t a string — throws the toolkit’s InvalidConfigurationException instead of quietly becoming a default.

null keeps its documented meaning: the default store, connection or queue, app.timezone for timezone, forever for cache.ttl, off for prune.*, and no alias for facade_alias. A blank value means the same as null, except for cache.ttl, where it keeps 86400, and facade_alias, where it keeps OpeningHours.

Environment

Every env-backed key at a glance:

OPENING_HOURS_KEY_TYPE=bigint
OPENING_HOURS_DEFAULT_CALENDAR=default
OPENING_HOURS_TIMEZONE=Europe/Bratislava
OPENING_HOURS_FIRST_DAY_OF_WEEK=monday
OPENING_HOURS_SEARCH_DAYS=366
OPENING_HOURS_MAX_QUERY_DAYS=366
OPENING_HOURS_DELETE_WITH_OWNER=true
OPENING_HOURS_CACHE_ENABLED=true
OPENING_HOURS_CACHE_STORE=redis
OPENING_HOURS_CACHE_TTL=86400
OPENING_HOURS_CACHE_PREFIX=opening-hours
OPENING_HOURS_PRUNE_EXCEPTIONS_AFTER_DAYS=30
OPENING_HOURS_PRUNE_TRASHED_AFTER_DAYS=90
OPENING_HOURS_MATERIALIZE=true
OPENING_HOURS_MATERIALIZE_DAYS_AHEAD=60
OPENING_HOURS_MATERIALIZE_DAYS_BEHIND=1
OPENING_HOURS_QUEUE_CONNECTION=redis
OPENING_HOURS_QUEUE=opening-hours

php artisan about

The package adds an Opening-hours section to php artisan about: the three model classes, key type, default calendar and timezone, search days, cache on/off, the cache store (shown only as default or custom), materialization on/off and the facade alias. A malformed setting other than a model class shows as INVALID, so the command still renders on a misconfigured host.

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.