Skip to content

datomatic/db-opening-hours

Repository files navigation

Store and query opening hours in the database

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

spatie/opening-hours is great for describing a weekly schedule in code, but it keeps everything in memory. This package persists that schedule in your database and attaches it to any Eloquent model, so you can store opening hours per record, edit them at runtime, and query them straight from SQL — "which shops are open right now?", "is this venue open between 10:00 and 12:00 next Monday?" — without loading every model into PHP.

You still get a real Spatie\OpeningHours\OpeningHours object whenever you need its full API.

$shop->openingHours()->create(['name' => 'default'])->syncWeeklySchedule([
    'monday' => [['start' => '09:00', 'end' => '13:00'], ['start' => '14:00', 'end' => '18:00']],
    'tuesday' => [['start' => '09:00', 'end' => '18:00']],
]);

// Query from the database
Shop::openAt(now())->get();                          // shops open right now
Shop::openBetween('2024-01-01', '10:00', '12:00')->get();

What it offers

  • Weekly schedule per model — attach opening hours to any Eloquent model through a single trait (a shop, a restaurant, an office…). A model can hold more than one schedule (e.g. seasonal), with latestOpeningHours / oldestOpeningHours helpers.
  • Multiple time ranges per day — e.g. a lunch break: 09:00-13:00 and 14:00-18:00.
  • Date exceptions — override a specific date (holidays, special events). On a date with an exception, only the exception's ranges count for that day.
  • Database-level query scopesopenAt, closeAt, openBetween, closedBetween run as SQL, so you can filter and paginate large tables without hydrating models.
  • Bridge to spatie/opening-hours — turn a stored schedule into a full OpeningHours object on demand.
  • Localised weekday labels — English, Italian and Dutch translations included, backed by a Day enum.
  • Swappable models — point any of the internal models at your own subclass (tenant scoping, extra columns) via config.

Installation

Install the package via composer:

composer require datomatic/db-opening-hours

Run the migrations (four tables: opening_hours, opening_hours_days, opening_hours_exceptions, opening_hours_time_ranges):

php artisan migrate

Optionally publish the config file:

php artisan vendor:publish --tag="db-opening-hours-config"

Optionally publish the translations:

php artisan vendor:publish --tag="db-opening-hours-translations"

Config

use Datomatic\DatabaseOpeningHours\Models\Day;
use Datomatic\DatabaseOpeningHours\Models\Exception;
use Datomatic\DatabaseOpeningHours\Models\OpeningHour;
use Datomatic\DatabaseOpeningHours\Models\TimeRange;

return [
    /*
     * Every internal relation resolves its model through this map, so you can
     * point any role at a subclass (for example to add tenant scoping or extra
     * columns) without forking the package. A subclass must extend the package
     * model it replaces and keep its table name.
     */
    'models' => [
        'opening_hour' => OpeningHour::class,
        'day'          => Day::class,
        'exception'    => Exception::class,
        'time_range'   => TimeRange::class,
    ],
];

Data model

Table Model Role
opening_hours OpeningHour One schedule, polymorphically linked to your model (openable).
opening_hours_days Day A weekday (mondaysunday) inside a schedule.
opening_hours_exceptions Exception A date override inside a schedule.
opening_hours_time_ranges TimeRange A startend window, belonging (polymorphically) to a Day or an Exception.

Weekdays are represented by the Datomatic\DatabaseOpeningHours\Enums\Day enum.

Usage

Attach opening hours to a model

Add the HasOpeningHours trait to any model:

use Datomatic\DatabaseOpeningHours\Traits\HasOpeningHours;
use Illuminate\Database\Eloquent\Model;

class Shop extends Model
{
    use HasOpeningHours;
}

This gives the model these relations:

$shop->openingHours;        // MorphMany – every schedule attached to the model
$shop->latestOpeningHours;  // MorphOne  – the most recently created schedule
$shop->oldestOpeningHours;  // MorphOne  – the first created schedule

Define a weekly schedule

Create a schedule and set its weekly hours in one call. syncWeeklySchedule() replaces the whole schedule: weekdays missing from the payload (or with empty ranges) are removed, so the argument is always the complete new state.

$openingHour = $shop->openingHours()->create(['name' => 'default']);

$openingHour->syncWeeklySchedule([
    'monday'    => [['start' => '09:00', 'end' => '13:00'], ['start' => '14:00', 'end' => '18:00']],
    'tuesday'   => [['start' => '09:00', 'end' => '18:00']],
    'wednesday' => [['start' => '09:00', 'end' => '18:00']],
]);

Times accept 9:00, 09:00 or 09:00:00; they are normalised before being stored.

Read the schedule back in the same shape:

$openingHour->weeklySchedule();
// [
//     'monday'  => [['start' => '09:00', 'end' => '13:00'], ['start' => '14:00', 'end' => '18:00']],
//     'tuesday' => [['start' => '09:00', 'end' => '18:00']],
//     ...
// ]

You also get a relation per weekday (returns a Day model or null):

$openingHour->monday;   // Day|null
$openingHour->sunday;   // Day|null
$openingHour->days;     // all Day models, ordered Monday → Sunday

Date exceptions

Override a specific date. On that date only the exception's ranges apply — the regular weekday hours are ignored.

$exception = $openingHour->exceptions()->create([
    'date'        => '2024-12-25',
    'description' => 'Christmas – short hours',
]);

$exception->timeRanges()->create(['start' => '10:00', 'end' => '12:00']);

Query the database

The HasOpeningHours trait adds SQL query scopes to your model. They accept a date-time string or any DateTimeInterface.

// Records open at a given instant (weekday hours, or a matching date exception)
Shop::openAt(now())->get();
Shop::openAt('2024-12-25 11:00')->get();

// Records that have opening hours but are closed at that instant
Shop::closeAt(now())->get();

// Records whose hours fully cover a whole window on a date
Shop::openBetween('2024-01-01', '10:00', '12:00')->get();

// Records that have hours but do not cover that window
Shop::closedBetween('2024-01-01', '10:00', '12:00')->get();

Notes:

  • openBetween uses containment: a single range must span the window end to end. A 10:00-11:30 request is not satisfied by a 09:00-11:00 range.
  • closeAt / closedBetween deliberately exclude models without any schedule — no schedule means "no restriction", not "always closed".

Get a spatie/opening-hours object

When you need the full spatie/opening-hours API, build it from the stored weekly schedule:

$hours = $openingHour->openingHours(); // Spatie\OpeningHours\OpeningHours

$hours->isOpenAt(new DateTime('2024-01-01 11:00')); // true / false
$hours->nextOpen(new DateTime());
$hours->forDay('monday');

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages