laboiteacode/filament-business-hours
Filament field for managing business opening hours on a drag-and-drop weekly 24-hour grid, with exceptional closures, special hours, vacation periods, and timezones. Stores data natively in spatie/opening-hours format.
Architecture fit
The package is a thin, well-behaved adapter between a Filament panel and spatie/opening-hours, and that shape is what makes it defensible. It owns exactly one concern — the editing surface — and delegates the domain model entirely to a mature, widely deployed Spatie library. Concretely:
business_hours with key, timezone, overflow, hours (JSON), exceptions (JSON). Nothing else touches your schema. No polymorphic relations, no side tables, no migrations colliding with application models.OpeningHours::create() can be built from a raw DB::table() query, a seeder, an import job, or a queue worker without ever loading the Filament page or the package model. This is the single most important architectural property: the UI is replaceable, the data is not.canAccessUsing() closure or config gate, per-panel ->record() key so multiple panels can manage distinct hour sets, and a fluent API that overrides a publishable config file. It behaves like a first-class Filament citizen rather than bolting a custom page on.Spatie\OpeningHours\OpeningHours before persisting, so a stored record is queryable by construction. Combined with range normalization (sort, merge overlaps/touching), duplicate-coverage rejection across exceptions and vacation periods, and hard payload caps (100 ranges/day, 500 exception entries, 1100 days per vacation, 255-char labels) to defend against crafted Livewire state, the correctness story is stronger than the 8-star count suggests.The main architectural limitation: there is no multi-tenant scoping, no audit trail, and no API. Multi-location means one plugin instance per panel per record key — workable for a handful of sites, awkward at dozens. Any headless/storefront exposure is your code, not theirs.
Integration feasibility
On the right stack, this is a sub-day integration:
composer require laboiteacode/filament-business-hours->plugin(FilamentBusinessHoursPlugin::make()) in the panel providerBusinessHours::openingHours($key) or ->toOpeningHoursArray()That is genuinely it. The friction is entirely in the compatibility gate.
| Constraint | Supported |
|---|---|
| PHP | 8.2 – 8.5 |
| Laravel | 12, 13 |
| Filament | 4, 5 |
Those are the only supported versions. Laravel 13 and Filament 5 are leading-edge targets. Any estate still on Laravel 10/11 or Filament 3 — which is most of the installed base in 2025 — is hard-blocked, and nothing in the package can be backported cheaply because the page is built on Filament 4 form/schema primitives.
Technical risk
The risk is concentrated in maintenance and ecosystem, not in code quality.
m-d to m-d recurring periods and structured data payloads survive UI edits verbatim. Good for data integrity, dangerous for operators: the admin sees a saved state that the grid cannot fully express, so "the page looks right" is not proof the record is right.overflow vs. two-range modelling. This needs an explicit decision, documented, not left to the editor.Key questions to raise with the vendor/maintainer (or answer ourselves)
spatie/opening-hours major is pinned, and how are its majors tracked?Stack fit
Good fit if and only if the target panel is already on Filament 4/5 with Laravel 12/13 and PHP 8.2+. This is an additive settings page, not a platform dependency — it does not touch authentication, routing, queueing, or storage beyond one table. It slots in beside existing Filament resources with no architectural negotiation.
For a Filament 3 / Laravel 10–11 estate: do not attempt. Either defer until the panel upgrade lands, or budget the 2–4 days to lift the pattern (the grid UX is the work; the Spatie persistence contract is 30 lines).
Migration path
There is no incumbent to displace — the package is net-new. The migration work is data, not code:
opens_at, closes_at, per-day booleans) or a bespoke table, write a one-off artisan command or seeder that assembles the Spatie definition array and writes rows via BusinessHours::create(['key' => ..., 'timezone' => ..., 'hours' => [...], 'exceptions' => [...]]). The page picks them up on next load.OpeningHours::create() keeps working. Zero data lock-in. This is the strongest argument for adopting.Compatibility
composer.json; treat Filament major bumps as a scheduled evaluation, not a routine composer update.spatie/opening-hours constraint resolves against whatever version the rest of the estate already uses — a duplicate major is a nuisance but not a blocker (the definition array is stable across Spatie majors, the query API less so).Sequencing
default record key, drag a few ranges, save, and round-trip through OpeningHours::create() in tinker. Validate the overflow-mode decision explicitly.20:00-24:00 Fri + 00:00-02:00 Sat) vs. overflow mode. Write it down; it determines how every future editor uses the grid.OpeningHoursService::for(string $key): ?OpeningHours wrapping BusinessHours::resolve() with a cache layer. All application code depends on this, never on the package model.BusinessHours busting the business-hours:{key} cache key on save. Pair with a short TTL (1 h) as a backstop.manageBusinessHours gate (or ->canAccessUsing()), not the default "anyone with panel access."Maintenance
The maintenance surface is small and well-defined: one migration, one config file, one plugin registration line, one page. The codebase is not something you maintain — the compatibility envelope is what you maintain.
Support
MIT, single-maintainer agency, no commercial support tier, no dependents to piggyback on. Assume self-support with upstream GitHub issues as best-effort. Mitigations already present in the repo (pinned CI actions, SECURITY.md with private disclosure and response timelines, Dependabot with cooldown) are a positive signal on maintainer discipline — but they are process hygiene, not a support commitment. Plan for the maintainer to be unreachable.
Operationally this means: whoever on the team owns Filament owns this package. Name them.
Scaling
OpeningHours::create() parses the full definition on every call. On a storefront header rendered per request, that is a measurable per-request cost. Cache the definition array (the README's own guidance) with a 1-hour TTL plus observer invalidation. After that, the read path is a cache hit and the table is touched almost never.Failure modes
| Failure | Impact | Detection | Mitigation |
|---|---|---|---|
| Filament major upgrade breaks the page | Editors cannot change hours; existing data unaffected | Staging smoke test on panel upgrade | Pin version; half-day verification per Filament major |
| Cache not invalidated after save | Storefront shows stale hours ("closed" while open) | Operator report; hours look right in panel but not on site | Observer on save + short TTL backstop |
| Overnight/DST edge handled wrong | Storefront reports wrong open state around DST or at 1 a.m. | Scheduled assertion tests against the definition | Explicit overflow-vs-two-range decision, documented; DST tests in CI |
| Two editors save concurrently | One editor's changes silently lost | Editor report ("my hours reverted") | Operational: single owner of hours; or add updated_at check before save in a custom wrapper |
| Hand-written exception survives but is unrepresentable in UI | Record is semantically richer/different than the grid implies | Hard to detect visually | Read the raw JSON in reviews; restrict who writes rows directly |
| Package abandoned pre-Filament-6 | Frozen on current Filament major | — | Fork contingency (2–4 days); low trigger threshold given exit cost is near zero |
| Malformed/large Livewire payload | Page render degrades or caps silently truncate | Validation message on save | Caps are in place (100 ranges/day, 500 exceptions); document them for heavy users |
Ramp-up
spatie/opening-hours' query API (isOpenAt, nextOpen, forDate, exception precedence, overflow) — budget half a day there, because that is what application code will actually call.TPM recommendation: Adopt in a Filament 4/5 panel where hours are a peripheral settings concern, with three non-negotiables — a read-side service abstraction so the app never touches the package model, a cached definition with observer invalidation, and a named internal owner plus a documented fork trigger. Do not adopt as a critical-path multi-location or multi-tenant scheduling system, and do not adopt on a Filament 3 estate. The vendor risk is real but hedged by the Spatie-native storage: the cost of being wrong is measured in days, not quarters.
How can I help you explore Laravel packages today?