Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Opening Hours Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation:

    composer require spatie/opening-hours
    

    No additional configuration is required—just autoload the package.

  2. 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
    
  3. Check if Open:

    $isOpen = $openingHours->isOpen(now());
    // Returns `true` if the current time falls within any open interval.
    
  4. Query Next/Previous Open/Close:

    $nextOpen = $openingHours->nextOpen()->format('Y-m-d H:i');
    $previousClose = $openingHours->previousClose()->format('Y-m-d H:i');
    

Implementation Patterns

Core Workflows

  1. 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);
    
  2. 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');
    
  3. 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(),
    ]);
    
  4. 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.
    

Advanced Patterns

  1. 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'
    );
    
  2. Timezone Awareness: Pass a DateTimeZone to respect global/local times:

    $openingHours->isOpen(now(), new DateTimeZone('America/New_York'));
    
  3. Caching: Cache computed results (e.g., nextOpen) for performance:

    $cacheKey = "store:{$store->id}:next_open";
    $nextOpen = Cache::remember($cacheKey, now()->addHour(), fn() =>
        $openingHours->nextOpen()
    );
    
  4. 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()));
    

Gotchas and Tips

Common Pitfalls

  1. Time Zone Mismatches:

    • Issue: Forgetting to specify a timezone can lead to incorrect results if the server timezone differs from the business's timezone.
    • Fix: Always pass a DateTimeZone to methods like isOpen() or use the default timezone explicitly:
      $openingHours->isOpen(now(), new DateTimeZone('Europe/Amsterdam'));
      
  2. Invalid JSON:

    • Issue: Serializing/deserializing malformed JSON throws cryptic errors.
    • Fix: Validate JSON before parsing:
      json_decode($store->opening_hours, true) ?: throw new \InvalidArgumentException('Invalid JSON');
      
  3. Closure Edge Cases:

    • Issue: Closures returning false or strings like '09:00-17:00' must be consistent. Mixing types can cause unexpected behavior.
    • Fix: Standardize closure returns (e.g., always use strings for intervals):
      // Avoid:
      $openingHours->addClosure('Monday', fn() => false); // Boolean
      // Prefer:
      $openingHours->addClosure('Monday', fn() => '09:00-17:00'); // String
      
  4. Daylight Saving Time (DST):

    • Issue: DST transitions may cause 1-hour gaps or overlaps in opening hours.
    • Fix: Test thoroughly around DST boundaries and consider using DateTimeImmutable for precision.
  5. Database Storage:

    • Issue: Storing opening hours as JSON can bloat the database or complicate queries.
    • Fix: Use a dedicated table for complex rules or normalize if querying frequently:
      // 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
      });
      

Debugging Tips

  1. 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,
    // ]
    
  2. 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';
    });
    
  3. Validate Manually: Use the validate() method to catch issues early:

    try {
        $openingHours->validate();
    } catch (\Spatie\OpeningHours\Exceptions\InvalidOpeningHours $e) {
        \Log::error($e->getMessage());
    }
    

Extension Points

  1. 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());
    
  2. 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();
    
  3. 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] ?? '*';
        }
    }
    
  4. Event Triggers: Dispatch events when opening hours change (e.g., for notifications):

    use
    
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
davejamesmiller/laravel-breadcrumbs
artisanry/parsedown
christhompsontldr/phpsdk
enqueue/dsn
bunny/bunny
enqueue/test
enqueue/null
enqueue/amqp-tools
milesj/emojibase
bower-asset/punycode
bower-asset/inputmask
bower-asset/jquery
bower-asset/yii2-pjax
laravel/nova
spatie/laravel-mailcoach
spatie/laravel-superseeder
laravel/liferaft
nst/json-test-suite
danielmiessler/sec-lists
jackalope/jackalope-transport