spatie/holidays
Spatie's PHP package calculates public holidays for a country. Call Holidays::for('be')->get() to receive Holiday objects with localized names, CarbonImmutable dates, and types. Supports many countries via ISO 3166-1 alpha-2 codes or country instances. Requires PHP 8.4+.
Architecture fit. A framework-agnostic PHP utility library: a static entry point (Holidays::for()), immutable value objects (Holiday with CarbonImmutable dates, a HolidayType enum, and JsonSerializable), and a Country abstraction with per-country subclasses. There is no service provider, no config file, no persistence, and no HTTP surface. It belongs at the domain-utility layer — callable from controllers, queued jobs, Artisan commands, and API resources identically. The JsonSerializable contract means holiday data can be exposed directly in a HolidayResource without a mapping layer, and CarbonImmutable is already resident in any Laravel stack, so no competing date abstraction is introduced.
Extension model. The important architectural detail is CountryRegistry::MAP, a hardcoded constant mapping ISO codes to classes. Adding a new jurisdiction to the string-code path requires forking or an upstream PR. However, Holidays::for() also accepts a Country instance, so proprietary or unregistered calendars can subclass Country in application code and be passed in directly. Only Holidays::has() and ISO-code lookup are unavailable for those. This is a workable escape hatch but it argues for always resolving holidays through an app-owned adapter rather than calling the facade across the codebase — otherwise an upstream rename or later registration of the same country creates a collision.
Hard constraints. PHP 8.4+ is a genuine gating risk. Laravel 10 environments (PHP 8.1–8.3) cannot run current releases at all and would need to pin an older package major — which simultaneously pins older holiday data. That trade-off should be surfaced before scoping. The second constraint is supportedYearRange() on the lookup-table calendar traits (Islamic, Chinese, Indian, Nepali): dates are precomputed constants with an explicit horizon, and queries outside it throw InvalidYear. An explicit exception is the right failure mode, but it is a scheduled cliff that somebody must own.
Primary technical risk: correctness is a data problem, not a code problem. Holiday definitions change by decree — new statutory days, one-off national holidays, substitute-day rule changes. The package's correctness model is Pest snapshot tests per country, which lock current behaviour, not legal truth; a wrong date that is snapshotted stays wrong until someone challenges it. Data arrives via community PRs with no SLA. For any use with payroll, billing, or contractual SLA consequences, the package is an 80% accelerator and the final verification is yours.
Key questions.
HolidayType taxonomy (National/Regional/Religious/Observed/Banked) map onto our domain categories, or does it need reinterpretation?HasObservedHolidays helpers?InvalidYear?Stack fit. composer require spatie/holidays; nothing to publish or configure. Verify the Carbon constraint resolves against the application's locked nesbot/carbon before committing — Carbon major bumps are the most likely dependency conflict.
Migration path. There is no schema change and no data migration. If an in-house holiday table or hardcoded calendar already exists, run the package in shadow mode for one full calendar year: compute holidays from both sources in a non-blocking job, log deltas, and only cut over after the delta set is empty or explained. This is cheap and converts the largest unknown (data accuracy for our countries) into an observed fact rather than an assumption.
Adapter and caching. Wrap the facade in a single application service (e.g. HolidayCalendar) that exposes only the operations the product needs — forCountryYear(), isBusinessDay(), nextBusinessDay(), longWeekends() — and returns app-owned DTOs. Benefits: one place to add caching, one place to handle unsupported-country fallback, one place to swap the vendor if needed. Cache keyed on country + region + year + package version; a country-year calendar is immutable once law settles, so a Redis or DB-backed cache with a long TTL is safe and busted automatically by the version component on composer update.
Fallback for uncovered jurisdictions. Register an app-level map of Country subclasses for anything not in the registry, and make HolidayCalendar resolve instance-first rather than code-first. Never let an InvalidCountry/InvalidRegion exception escape into a user-facing path.
Sequencing.
Holidays::has().InvalidYear/InvalidRegion, and a release-watch on the upstream changelog.Locale handling. Translation files are JSON keyed by the original holiday name with silent fallback. Treat missing translations as a product defect, not a non-event — untranslated Dutch or German strings will surface in a French UI. Add a test that asserts no fallback occurs for our supported locales.
Maintenance. The package's release cadence effectively is our data refresh cadence. Upgrading composer is how a newly legislated holiday reaches production; there is no separate data feed. That means the package must be on the routine dependency-upgrade train with an owner, not parked at a pinned version. Budget a small recurring cost per upgrade to re-run the spot-check and diff the snapshot output for our countries. The changelog is release-oriented rather than data-oriented, so diffs of actual holiday output are worth generating ourselves.
Support. Spatie is a well-established commercial OSS shop with a strong Laravel track record and a documented security policy, so code-level support risk is low. Data-level support is community-driven: fixes arrive as PRs from contributors, with no SLA and no escalation path. The reported dependent count is effectively zero, which should be read as limited evidence of production use at our scale rather than a defect — but it does mean we will not have a body of public war stories to lean on when something looks wrong.
Scaling. Compute is trivial: dozens of date calculations per country per year. The only scaling concern is fan-out — per-employee, per-country, per-pay-period loops in reporting jobs. Precompute the country-year calendar once per cache key and iterate the cached array; that turns an O(n) recomputation into O(1) lookups. Do not call Holidays::for() inside an inner loop.
Failure modes to instrument.
InvalidYear on Islamic/Chinese/Indian/Nepali calendars. Loud failure is correct; ensure the exception is not swallowed by a broad catch. Alert before the horizon, not after.Observed entries; if our business rule differs (or we also apply our own roll-forward), we will count twice or miss the substitute day.isTodayHoliday() evaluated against now() in a different app timezone can be off by a day in distributed or multi-region deployments.Ramp-up. Low. A single developer can integrate and ship a read-only holiday display in under a day; the API surface is small, the README is complete with worked examples, and the snapshot tests double as a record of expected values. Realistically, 80% of the effort sits in jurisdiction inventory, source verification, and the adapter/cache layer — not in learning the package. That asymmetry is the core planning insight: this is a cheap integration and a moderately expensive validation project, and the validation work is what determines whether it is safe for money-touching or contract-touching flows.
How can I help you explore Laravel packages today?