spatie/opening-hours
spatie/opening-hours lets you define business opening hours as a weekly schedule plus exceptions, then query whether you’re open or closed on a day or specific date and present times per day. It also works with Carbon via cmixin/business-time.
Architecture fit: spatie/opening-hours is a framework-agnostic, dependency-light PHP library, not a Laravel package in the traditional sense — there is no service provider, facade, config file, or migration. That is a feature, not a gap: the OpeningHours aggregate (with Time, TimeRange, and OpeningHoursForDay value objects) is a clean domain primitive that drops into a Laravel service layer without imposing persistence, routing, or container conventions. For a TPM, this means it can be adopted as a bounded piece of business logic — a "schedule resolver" — and wrapped with whatever Laravel idioms the application already uses (a repository, a singleton binding, a cast, or a facade of your own).
Integration feasibility: High. Installation is a single Composer require. The schedule is a plain array, which maps naturally to a JSON column on an existing model (store, location, branch, provider) or to a separate schedules table. Hydration is OpeningHours::create($array, $timezone). Query methods (isOpenAt, isOpenOn, nextOpen, nextClose, currentOpenRange, forWeek, forDate) cover the overwhelmingly common product requirements — "Open now" badges, next-open messaging, weekly display, holiday exceptions — without custom date math. Input/output timezone separation is a genuinely useful capability for multi-region tenants, and the optional Carbon integration via cmixin/business-time lets existing Carbon-centric codebases stay idiomatic.
Technical risk: Moderate but contained, concentrated in a few areas:
DateTime objects match the schedule timezone unless you configure conversion. DST transitions around overnight ranges (overflow, 24:00) are the classic source of subtle bugs and need explicit test cases.mergeOverlappingRanges, createAndMergeOverlappingRanges) before construction.Key questions:
nextOpen/nextClose searches bounded when a schedule is permanently closed, or can they iterate indefinitely?Stack fit: Works with any Laravel version on a supported PHP runtime; no framework coupling to fight. The natural placement is app/Domain/Schedule/ with a thin ScheduleResolver service that accepts a model, builds the OpeningHours object, and exposes application-shaped methods (isOpenNow(), nextOpeningLabel()). Eloquent should never be aware of the library beyond a custom cast or accessor that serializes/deserializes the JSON schedule.
Migration path: None required for the package itself. The real work is our own schema: a JSON schedule column plus timezone on the owning entity, and an exceptions structure (either embedded in the JSON or a related table if exceptions need independent editing, approval, or audit). A three-step path is recommended: (1) add the column and hydrate existing manual hours into the array format, (2) read exclusively through the resolver while keeping the legacy display path as a fallback, (3) retire the legacy path once parity is confirmed across a full week plus a holiday.
Compatibility: Confirm Carbon 2 vs. 3 alignment early, since Carbon is the most likely transitive-version collision in a large Laravel app. If the app already uses cmixin/business-time, validate that the added Carbon macros don't conflict. Output timezone conversion means API responses should be explicit about which timezone they return — pick one convention and enforce it in resources.
Sequencing:
forWeekCombined/forWeekConsecutiveDays).Maintenance: Low surface area and an MIT license make this cheap to own. The library has been stable for years with a small, well-documented API. Principal maintenance burden is not the package but the schedule data model we build around it. Pin the version, review the changelog on upgrade, and keep the resolver as the only call site so a future swap (or fork) is a one-file change.
Support: Community-only via GitHub issues; no SLA, no commercial backing. Given the small API and mature test suite this is acceptable for a non-critical feature, but it should be classified accordingly — this package is not appropriate as the sole authority for something like billing eligibility or regulatory compliance without our own test coverage and fallback logic.
Scaling: Computation is pure CPU and cheap per call, but the cost accumulates per entity per request. Two mitigations matter: memoize the OpeningHours object within a request (construction parses the full array each time), and cache "open now" / "next open" results for high-traffic listing pages, invalidating on schedule write and at time-bucket boundaries. Avoid filters entirely in hot paths; if dynamic exceptions are needed, precompute them into concrete exception arrays. Querying thousands of locations for "open now" in a single page should be modeled as a batch resolve with caching, not N uncached constructions.
Failure modes: The realistic ones are (a) exceptions thrown on overlapping or malformed ranges during user input or data import — catch at the boundary and surface as validation errors; (b) incorrect results around DST and overnight overflow ranges — cover with fixed-date tests; (c) timezone mismatch between stored schedule and request context producing confidently wrong "Open now" badges — enforce a single timezone convention and log mismatches; (d) unbounded or slow behavior from filters — disallow by convention and code review; (e) null returns from nextOpen/previousClose when a schedule is entirely closed — callers must handle nulls rather than assume a DateTime. A dashboard counter for "open now" queries returning null is a cheap early-warning signal.
Ramp-up: Very low. One aggregate class, three value objects, and a README that covers nearly every method with examples. A mid-level engineer can be productive in half a day; the real learning curve is the schedule domain (exception precedence, overflow, timezone conversion), which is best captured as a short internal ADR plus a fixture-based test suite rather than left to the upstream docs.
How can I help you explore Laravel packages today?