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();- 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/oldestOpeningHourshelpers. - Multiple time ranges per day — e.g. a lunch break:
09:00-13:00and14: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 scopes —
openAt,closeAt,openBetween,closedBetweenrun as SQL, so you can filter and paginate large tables without hydrating models. - Bridge to
spatie/opening-hours— turn a stored schedule into a fullOpeningHoursobject on demand. - Localised weekday labels — English, Italian and Dutch translations included, backed by a
Dayenum. - Swappable models — point any of the internal models at your own subclass (tenant scoping, extra columns) via config.
Install the package via composer:
composer require datomatic/db-opening-hoursRun the migrations (four tables: opening_hours, opening_hours_days, opening_hours_exceptions, opening_hours_time_ranges):
php artisan migrateOptionally 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"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,
],
];| Table | Model | Role |
|---|---|---|
opening_hours |
OpeningHour |
One schedule, polymorphically linked to your model (openable). |
opening_hours_days |
Day |
A weekday (monday…sunday) inside a schedule. |
opening_hours_exceptions |
Exception |
A date override inside a schedule. |
opening_hours_time_ranges |
TimeRange |
A start–end window, belonging (polymorphically) to a Day or an Exception. |
Weekdays are represented by the Datomatic\DatabaseOpeningHours\Enums\Day enum.
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 scheduleCreate 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 → SundayOverride 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']);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:
openBetweenuses containment: a single range must span the window end to end. A10:00-11:30request is not satisfied by a09:00-11:00range.closeAt/closedBetweendeliberately exclude models without any schedule — no schedule means "no restriction", not "always closed".
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');composer testPlease see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Please review our security policy on how to report security vulnerabilities.
The MIT License (MIT). Please see License File for more information.