datomatic / db-opening-hours
Use spatie/opening-hours package and save it on database to query it
Fund package maintenance!
Requires
- php: ^8.2
- illuminate/contracts: ^10.0||^11.0||^12.0||^13.0
- spatie/laravel-package-tools: ^1.16.4
- spatie/opening-hours: ^4.0.0
Requires (Dev)
- larastan/larastan: ^2.9.8|^3.0
- laravel/pint: ^1.17.2
- nunomaduro/collision: ^8.4.0|^7.10.0
- orchestra/testbench: ^11.0|^10.0|^9.3.0|^8.22.0
- pestphp/pest: ^2.36|^3.5|^4.5
- pestphp/pest-plugin-laravel: ^2.4|^3.0|^4.0
- phpstan/extension-installer: ^1.4.1
- phpstan/phpstan-deprecation-rules: ^1.2|^2.0
- phpstan/phpstan-phpunit: ^1.4|^2.0
- rector/rector: ^1.2|^2.5
README
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/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.
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 (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.
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:
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".
Get a spatie/opening-hours object
When you need the full spatie/opening-hours API, build it from the stored schedule. Both the weekly days and the date exceptions are fed in, so the object's date-aware methods honour exceptions exactly like the openAt/openBetween query scopes:
$hours = $openingHour->openingHours(); // Spatie\OpeningHours\OpeningHours $hours->isOpenAt(new DateTime('2024-01-01 11:00')); // true / false $hours->nextOpen(new DateTime()); $hours->forDay('monday'); // weekday ranges $hours->forDate(new DateTime('2024-12-25')); // ranges for that date, exceptions applied
A date exception replaces the weekday ranges for its date; an exception with no ranges makes that date closed.
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.