spatie/opening-hours
Define and query business opening hours with weekly schedules and exceptions. Check if a date/time is open or closed, get next open/close times, and format hours per day. Integrates with Carbon via cmixin/business-time for date-based queries.
Installation:
composer require spatie/opening-hours
No additional configuration is required—just autoload the package.
First Use Case:
Define opening hours for a store (e.g., Store.php model):
use Spatie\OpeningHours\OpeningHours;
$openingHours = OpeningHours::create()
->addClosure('Monday', '09:00', '17:00')
->addClosure('Tuesday', '09:00', '17:00')
->addClosure('Wednesday', '09:00', '17:00')
->addClosure('Thursday', '09:00', '17:00')
->addClosure('Friday', '09:00', '17:00')
->addClosure('Saturday', '10:00', '14:00')
->addClosure('Sunday', fn() => false); // Closed
Check if Open:
$isOpen = $openingHours->isOpen(now());
// Returns `true` if the current time falls within any open interval.
Query Next/Previous Open/Close:
$nextOpen = $openingHours->nextOpen()->format('Y-m-d H:i');
$previousClose = $openingHours->previousClose()->format('Y-m-d H:i');
Model Integration:
Store opening hours as a JSON column in your database (e.g., opening_hours on stores table) and hydrate them:
use Spatie\OpeningHours\OpeningHours;
$store = Store::find(1);
$openingHours = OpeningHours::createFromJson($store->opening_hours);
Dynamic Rules: Use closures for conditional logic (e.g., holidays, seasonal changes):
$openingHours->addClosure('Monday', fn($date) => $date->isBetween('2023-12-25', '2024-01-01') ? false : '09:00-17:00');
API Responses: Serialize opening hours for frontend consumption:
return response()->json([
'is_open' => $openingHours->isOpen(now()),
'next_open' => $openingHours->nextOpen()->toIso8601String(),
'formatted_hours' => $openingHours->getFormattedOpeningHours(),
]);
Validation: Validate opening hours during model creation/update:
use Spatie\OpeningHours\OpeningHours;
$openingHours = OpeningHours::createFromJson($request->opening_hours);
$openingHours->validate(); // Throws \Spatie\OpeningHours\Exceptions\InvalidOpeningHours if invalid.
Recurring Exceptions: Handle recurring closures (e.g., every 1st of the month):
$openingHours->addClosure('Monday', fn($date) =>
$date->day === 1 ? false : '09:00-17:00'
);
Timezone Awareness:
Pass a DateTimeZone to respect global/local times:
$openingHours->isOpen(now(), new DateTimeZone('America/New_York'));
Caching:
Cache computed results (e.g., nextOpen) for performance:
$cacheKey = "store:{$store->id}:next_open";
$nextOpen = Cache::remember($cacheKey, now()->addHour(), fn() =>
$openingHours->nextOpen()
);
Testing: Mock opening hours for unit tests:
$mockHours = OpeningHours::create()
->addClosure('*', '10:00', '16:00'); // Open 10 AM - 4 PM every day
$this->assertTrue($mockHours->isOpen(now()));
Time Zone Mismatches:
DateTimeZone to methods like isOpen() or use the default timezone explicitly:
$openingHours->isOpen(now(), new DateTimeZone('Europe/Amsterdam'));
Invalid JSON:
json_decode($store->opening_hours, true) ?: throw new \InvalidArgumentException('Invalid JSON');
Closure Edge Cases:
false or strings like '09:00-17:00' must be consistent. Mixing types can cause unexpected behavior.// Avoid:
$openingHours->addClosure('Monday', fn() => false); // Boolean
// Prefer:
$openingHours->addClosure('Monday', fn() => '09:00-17:00'); // String
Daylight Saving Time (DST):
DateTimeImmutable for precision.Database Storage:
// Migration:
Schema::create('store_opening_hours', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->string('day'); // e.g., 'Monday', '*'
$table->string('hours')->nullable(); // '09:00-17:00'
$table->text('closure')->nullable(); // JSON-encoded closure logic
});
Inspect Rules:
Use getRules() to debug the current configuration:
dd($openingHours->getRules());
// Outputs an array of rules like:
// [
// 'Monday' => ['09:00', '17:00'],
// '*' => fn($date) => false,
// ]
Log Closure Evaluations: Override the closure logic temporarily for debugging:
$openingHours->addClosure('Monday', fn($date) => {
\Log::info("Evaluating Monday rule for {$date->format('Y-m-d H:i')}");
return '09:00-17:00';
});
Validate Manually:
Use the validate() method to catch issues early:
try {
$openingHours->validate();
} catch (\Spatie\OpeningHours\Exceptions\InvalidOpeningHours $e) {
\Log::error($e->getMessage());
}
Custom Validators: Extend the validator for business-specific rules:
use Spatie\OpeningHours\OpeningHours;
use Spatie\OpeningHours\OpeningHoursValidator;
class CustomOpeningHoursValidator extends OpeningHoursValidator {
protected function validateRules(array $rules): void {
parent::validateRules($rules);
// Add custom logic, e.g., max 2 hours between open/close times.
}
}
$openingHours = OpeningHours::create()->setValidator(new CustomOpeningHoursValidator());
Additional Methods:
Add helper methods to the OpeningHours class via traits or decorators:
trait CustomOpeningHoursMethods {
public function isOpenToday(): bool {
return $this->isOpen(now());
}
public function getOpeningDays(): array {
return array_keys($this->getRules());
}
}
// Usage:
$openingHours->isOpenToday();
Localization:
Localize day names or time formats by overriding the OpeningHours class:
class LocalizedOpeningHours extends \Spatie\OpeningHours\OpeningHours {
protected function getDayName(int $day): string {
$days = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'];
return $days[$day - 1] ?? '*';
}
}
Event Triggers: Dispatch events when opening hours change (e.g., for notifications):
use
How can I help you explore Laravel packages today?