# Opening Hours Library ## Introduction The `spatie/opening-hours` library is a PHP package designed to manage and query business opening hours schedules. It provides a comprehensive solution for defining regular weekly schedules, handling exceptions for holidays or special dates, and determining whether a business is open or closed at any given moment. The library supports complex scenarios including overnight operations, timezone conversions, and recurring date patterns. This package enables developers to easily implement features like displaying current opening status, calculating when a business will next open or close, and formatting schedules for display purposes. It includes support for schema.org structured data formats, making it ideal for SEO-optimized business websites. The library can handle edge cases such as time ranges that overflow into the next day (like nightclubs open until 3am) and provides robust timezone handling for businesses operating across different time zones. ## API Reference and Code Examples ### Creating an Opening Hours Schedule Create an OpeningHours instance with weekly schedule and exceptions ```php use Spatie\OpeningHours\OpeningHours; $openingHours = OpeningHours::create([ 'monday' => ['09:00-12:00', '13:00-18:00'], 'tuesday' => ['09:00-12:00', '13:00-18:00'], 'wednesday' => ['09:00-12:00'], 'thursday' => ['09:00-12:00', '13:00-18:00'], 'friday' => ['09:00-12:00', '13:00-20:00'], 'saturday' => ['09:00-12:00', '13:00-16:00'], 'sunday' => [], 'exceptions' => [ '2016-11-11' => ['09:00-12:00'], '2016-12-25' => [], '01-01' => [], '12-25' => ['09:00-12:00'], ], ]); ``` ### Checking if Business is Open or Closed Query opening status for specific days or date/time combinations ```php // Check if open on a specific day of the week if ($openingHours->isOpenOn('monday')) { echo "We're open on Mondays!"; } // Check if closed on a specific day if ($openingHours->isClosedOn('sunday')) { echo "Closed on Sundays"; } // Check if open at a specific date and time $isOpen = $openingHours->isOpenAt(new DateTime('2016-09-26 19:00:00')); if (!$isOpen) { echo "Sorry, we're closed at that time"; } // Check if open right now if ($openingHours->isOpen()) { echo "We're currently open!"; } else { echo "We're currently closed"; } // Check exception dates $isOpenChristmas = $openingHours->isOpenOn('2016-12-25'); ``` ### Finding Next Open and Close Times Calculate when the business will next open or close from a given moment ```php // Find the next open time $nextOpen = $openingHours->nextOpen(new DateTime('2016-12-25 10:00:00')); echo "Next open: " . $nextOpen->format('Y-m-d H:i'); // 2016-12-26 09:00 // Find next open after lunch break $nextOpen = $openingHours->nextOpen(new DateTime('2016-12-24 11:00:00')); echo "Opens at: " . $nextOpen->format('H:i'); // 13:00 // Find the next close time $nextClose = $openingHours->nextClose(new DateTime('2016-12-24 10:00:00')); echo "Closes at: " . $nextClose->format('H:i'); // 12:00 // Find previous open/close times $previousOpen = $openingHours->previousOpen(new DateTime('2016-12-24 11:00:00')); $previousClose = $openingHours->previousClose(new DateTime('2016-12-24 15:00:00')); // With search limits and caps try { $nextOpen = $openingHours->nextOpen( new DateTime('2016-12-24 10:00:00'), searchUntil: new DateTime('2016-12-31 23:59:59'), cap: new DateTime('2017-01-01 00:00:00') ); } catch (\Spatie\OpeningHours\Exceptions\SearchLimitReached $e) { echo "No opening time found within the search period"; } ``` ### Getting Current Open Range Retrieve the current time range if the business is open ```php $now = new DateTime('2016-12-24 11:00:00'); $range = $openingHours->currentOpenRange($now); if ($range) { echo "Open since: " . $range->start() . "\n"; // 09:00 echo "Closes at: " . $range->end() . "\n"; // 12:00 } else { echo "Currently closed"; } // Get specific start and end times as DateTime objects $rangeStart = $openingHours->currentOpenRangeStart($now); $rangeEnd = $openingHours->currentOpenRangeEnd($now); if ($rangeStart && $rangeEnd) { echo "Open from " . $rangeStart->format('H:i') . " to " . $rangeEnd->format('H:i'); } ``` ### Handling Overflow Times (Night Hours) Configure opening hours that extend past midnight ```php // For businesses open past midnight (nightclubs, 24-hour diners, etc.) $nightclub = OpeningHours::create([ 'overflow' => true, 'friday' => ['20:00-03:00'], // Open 8pm Friday to 3am Saturday 'saturday' => ['20:00-03:00'], // Open 8pm Saturday to 3am Sunday ]); // Check if open at 1am on Saturday (part of Friday's hours) $isOpen = $nightclub->isOpenAt(new DateTime('Saturday 01:00:00')); echo $isOpen ? "Open" : "Closed"; // Open ``` ### Working with Date Ranges Define multiple consecutive days or date ranges at once ```php $schedule = OpeningHours::create([ 'monday to friday' => ['09:00-19:00'], 'saturday to sunday' => [], 'exceptions' => [ // Recurring annual holiday period '12-24 to 12-26' => [ 'hours' => [], 'data' => 'Christmas Holidays', ], // Specific year maintenance period '2024-06-25 to 2024-07-01' => [ 'hours' => [], 'data' => 'Closed for renovations', ], ], ]); ``` ### Adding Custom Data to Time Ranges Attach metadata to specific time ranges or days ```php $restaurant = OpeningHours::create([ 'monday' => [ 'data' => 'Regular Monday service', '09:00-12:00', '13:00-18:00', ], 'tuesday' => [ '09:00-12:00', '13:00-18:00', [ '19:00-21:00', 'data' => 'Live music evening', ], ], 'exceptions' => [ '2016-12-25' => [ 'hours' => [], 'data' => 'Closed for Christmas', ], ], ]); // Retrieve the custom data echo $restaurant->forDay('monday')->data; // "Regular Monday service" echo $restaurant->forDay('tuesday')[2]->data; // "Live music evening" echo $restaurant->forDate(new DateTime('2016-12-25'))->data; // "Closed for Christmas" // Alternative format with explicit hours key $altFormat = OpeningHours::create([ 'wednesday' => [ 'hours' => ['09:00-17:00'], 'data' => ['special' => true, 'discount' => '20%'], ], ]); ``` ### Using Dynamic Filters Apply callable filters for computed exceptions like Easter Monday ```php $schedule = OpeningHours::create([ 'monday' => ['09:00-12:00', '13:00-17:00'], 'tuesday' => ['09:00-12:00', '13:00-17:00'], 'wednesday' => ['09:00-12:00', '13:00-17:00'], 'thursday' => ['09:00-12:00', '13:00-17:00'], 'friday' => ['09:00-12:00', '13:00-17:00'], 'filters' => [ function ($date) { // Calculate Easter Monday for the given date's year $year = intval($date->format('Y')); $easterMonday = new DateTimeImmutable( '2018-03-21 +' . (easter_days($year) + 1) . ' days' ); if ($date->format('m-d') === $easterMonday->format('m-d')) { return []; // Closed on Easter Monday } // Return null to allow other filters or regular schedule }, function ($date) { // Custom business logic: closed on first Monday of each month if ($date->format('D') === 'Mon' && $date->format('j') <= 7) { return ['data' => 'Monthly inventory day']; } }, ], ]); ``` ### Retrieving Week and Day Schedules Get opening hours for entire weeks or specific days ```php // Get all opening hours for the week $weekSchedule = $openingHours->forWeek(); foreach ($weekSchedule as $day => $hours) { echo "$day: $hours\n"; // e.g., "monday: 09:00-12:00,13:00-18:00" } // Get opening hours for a specific day $mondayHours = $openingHours->forDay('monday'); foreach ($mondayHours as $timeRange) { echo "Open: " . $timeRange->start() . " to " . $timeRange->end() . "\n"; } // Get opening hours for a specific date (considers exceptions) $christmasHours = $openingHours->forDate(new DateTime('2016-12-25')); if ($christmasHours->isEmpty()) { echo "Closed on Christmas"; } // Get combined days with same hours $combined = $openingHours->forWeekCombined(); foreach ($combined as $firstDay => $info) { $days = implode(', ', $info['days']); echo "$days: {$info['opening_hours']}\n"; } // Get consecutive days with same hours $consecutive = $openingHours->forWeekConsecutiveDays(); ``` ### Managing Timezone Conversions Handle timezone conversions for input and output ```php // Set timezone for the schedule $parisHours = OpeningHours::create([ 'monday' => ['09:00-18:00'], 'tuesday' => ['09:00-18:00'], 'timezone' => 'Europe/Paris', // Business operates in Paris time ]); // Pass DateTime in any timezone, it will be converted $newYorkTime = new DateTime('2016-12-24 15:00:00', new DateTimeZone('America/New_York')); $isOpen = $parisHours->isOpenAt($newYorkTime); // Automatically converts to Paris time // Different input and output timezones $multiTimezone = OpeningHours::create([ 'monday' => ['09:00-18:00'], 'timezone' => [ 'input' => 'America/New_York', // Accept times in NY timezone 'output' => 'Europe/Oslo', // Return times in Oslo timezone ], ]); // Or pass as separate arguments $schedule = OpeningHours::create( ['monday' => ['09:00-18:00']], timezone: 'America/New_York', outputTimezone: 'Europe/Oslo' ); ``` ### Handling Overlapping Time Ranges Merge or validate overlapping time ranges ```php // This will throw an exception due to overlapping ranges try { $invalid = OpeningHours::create([ 'monday' => ['08:00-11:00', '10:00-12:00'], // Overlaps! ]); } catch (\Spatie\OpeningHours\Exceptions\OverlappingTimeRanges $e) { echo "Time ranges overlap!"; } // Allow overlaps explicitly $withOverlaps = OpeningHours::create([ 'overflow' => true, 'monday' => ['08:00-11:00', '10:00-12:00'], ]); // Or merge overlapping ranges automatically $merged = OpeningHours::createAndMergeOverlappingRanges([ 'monday' => ['08:00-11:00', '10:00-12:00'], // Will become ['08:00-12:00'] ]); // Merge ranges manually $ranges = ['monday' => ['08:00-11:00', '10:00-12:00']]; $mergedRanges = OpeningHours::mergeOverlappingRanges($ranges); $schedule = OpeningHours::create($mergedRanges); ``` ### Calculating Time Differences Calculate open or closed duration between two dates ```php $start = new DateTime('2016-12-24 10:00:00'); $end = new DateTime('2016-12-24 16:30:00'); // Calculate hours the business was open $openHours = $openingHours->diffInOpenHours($start, $end); echo "Open for $openHours hours"; // e.g., "Open for 4.5 hours" // Calculate in different units $openMinutes = $openingHours->diffInOpenMinutes($start, $end); $openSeconds = $openingHours->diffInOpenSeconds($start, $end); // Calculate closed time $closedHours = $openingHours->diffInClosedHours($start, $end); $closedMinutes = $openingHours->diffInClosedMinutes($start, $end); $closedSeconds = $openingHours->diffInClosedSeconds($start, $end); echo "Closed for $closedHours hours (e.g., lunch break)"; ``` ### Working with Exceptions Retrieve and manage exception dates ```php // Get all exceptions $allExceptions = $openingHours->exceptions(); foreach ($allExceptions as $date => $hours) { echo "$date: $hours\n"; } // Get regular closing days $closedDays = $openingHours->regularClosingDays(); echo "Regularly closed on: " . implode(', ', $closedDays); // ["sunday"] // Get ISO day numbers for closed days (1=Monday, 7=Sunday) $isoDays = $openingHours->regularClosingDaysISO(); // Get exceptional closing dates as DateTime objects $exceptionalClosings = $openingHours->exceptionalClosingDates(); foreach ($exceptionalClosings as $date) { echo "Closed on " . $date->format('Y-m-d') . "\n"; } ``` ### Generating Schema.org Structured Data Export opening hours in schema.org format for SEO ```php // Generate structured data array $structuredData = $openingHours->asStructuredData(); echo json_encode($structuredData, JSON_PRETTY_PRINT); // Customize time format $customFormat = $openingHours->asStructuredData('h:i A'); // 12-hour format // Add timezone to output $withTimezone = $openingHours->asStructuredData('H:iP', '-05:00'); // Create from structured data $fromSchema = OpeningHours::createFromStructuredData([ [ "@type" => "OpeningHoursSpecification", "opens" => "08:00", "closes" => "12:00", "dayOfWeek" => [ "https://schema.org/Monday", "https://schema.org/Tuesday", ] ], [ "@type" => "OpeningHoursSpecification", "opens" => "14:00", "closes" => "18:00", "dayOfWeek" => ["Monday", "Tuesday"] ], ]); // Also accepts JSON string $fromJson = OpeningHours::createFromStructuredData(' [{"@type": "OpeningHoursSpecification", "opens": "09:00", "closes": "17:00", "dayOfWeek": ["Monday"]}] '); ``` ### Filtering and Mapping Schedule Data Apply functional operations to opening hours ```php // Filter days by condition $longDays = $openingHours->filter(function($day) { return count($day) > 1; // Days with multiple time ranges }); // Map over all days $formatted = $openingHours->map(function($hoursForDay, $dayName) { return ucfirst($dayName) . ': ' . (string)$hoursForDay; }); // Flat map for flattened results $allRanges = $openingHours->flatMap(function($hoursForDay) { return iterator_to_array($hoursForDay); }); // Check if all days match condition $alwaysHasLunch = $openingHours->every(function($day) { return count($day) >= 2; // Every day has lunch break }); // Filter exceptions $closedExceptions = $openingHours->filterExceptions(function($hours) { return $hours->isEmpty(); }); // Check all exceptions match condition $allExceptionsAreClosed = $openingHours->everyExceptions(function($hours) { return $hours->isEmpty(); }); ``` ### Validating and Checking Special States Check for always-open or always-closed states ```php // Check if schedule is valid if (!OpeningHours::isValid($scheduleArray)) { throw new Exception("Invalid opening hours configuration"); } // Check if always open (24/7) if ($openingHours->isAlwaysOpen()) { echo "This business is open 24/7"; } // Check if always closed (no hours set) if ($openingHours->isAlwaysClosed()) { throw new RuntimeException("Opening hours are not configured"); } // Configure search limit for finding next open/close $schedule = OpeningHours::create(['monday' => ['09:00-17:00']]); $schedule->setDayLimit(30); // Search up to 30 days ahead $limit = $schedule->getDayLimit(); // 24/7 schedule example $alwaysOpen = OpeningHours::create([ 'monday' => ['00:00-24:00'], 'tuesday' => ['00:00-24:00'], 'wednesday' => ['00:00-24:00'], 'thursday' => ['00:00-24:00'], 'friday' => ['00:00-24:00'], 'saturday' => ['00:00-24:00'], 'sunday' => ['00:00-24:00'], ]); ``` ### Working with Time and TimeRange Objects Directly manipulate time and time range value objects ```php // Create Time objects $time = \Spatie\OpeningHours\Time::fromString('14:30'); echo $time->hours(); // 14 echo $time->minutes(); // 30 echo $time->format(); // "14:30" echo $time->format('h:i A'); // "02:30 PM" // Compare times $earlier = \Spatie\OpeningHours\Time::fromString('09:00'); $later = \Spatie\OpeningHours\Time::fromString('17:00'); $later->isAfter($earlier); // true $earlier->isBefore($later); // true // Create TimeRange objects $range = \Spatie\OpeningHours\TimeRange::fromString('09:00-17:00'); echo $range->start(); // "09:00" echo $range->end(); // "17:00" echo $range; // "09:00-17:00" // Check if time is in range $checkTime = \Spatie\OpeningHours\Time::fromString('12:00'); if ($range->containsTime($checkTime)) { echo "Within business hours"; } // Check for overnight ranges $nightRange = \Spatie\OpeningHours\TimeRange::fromString('22:00-02:00'); $nightRange->overflowsNextDay(); // true // Time differences $diff = $later->diff($earlier); echo $diff->h . " hours"; // "8 hours" ``` ## Summary and Integration The `spatie/opening-hours` library provides a robust, production-ready solution for managing business hours in PHP applications. Common use cases include restaurant websites displaying "Open Now" badges, booking systems that prevent reservations outside business hours, customer service portals showing support availability, and store locators with real-time opening status. The library handles edge cases gracefully including holidays, overnight operations, and timezone complexities, making it suitable for both simple schedules and complex multi-location businesses. Integration patterns typically involve creating an OpeningHours instance at application startup or loading from a database, then querying it throughout the application lifecycle. The library works seamlessly with popular frameworks like Laravel, Symfony, and WordPress through its framework-agnostic design. For real-time status displays, combine the `isOpen()` method with JavaScript polling or WebSocket updates. The structured data export feature integrates directly with JSON-LD schema markup for enhanced SEO. The library also pairs well with the Carbon datetime library through the `cmixin/business-time` package, enabling business hour calculations directly on Carbon instances for a more fluent API experience.