zarbinco/laravel-workdays
zarbinco/laravel-workdays is a Laravel package for working-day calculations. Add or subtract business days, check whether a date is a workday, and account for weekends and holidays—useful for deadlines, SLAs, and scheduling.
This package gives a Laravel app two things that usually get bolted on badly: a Jalali/Persian (Iran) calendar layer, and a business-time engine for working hours, holidays, SLA deadlines and reverse scheduling. v1.0.0 is a ground-up consolidation of both — read it as "same capabilities, new core," not "new toy."
Minimum steps:
composer require <vendor>/<package> # check the release for the constraint
php artisan vendor:publish --tag=<package>-config
php artisan vendor:publish --tag=<package>-migrations # only if using the Database source
php artisan migrate
Where to look first, in this order:
Config (definitions live in PHP config) or Database (definitions live in tables, revisioned). They are exclusive. That single decision drives your whole integration.First use case to get working end-to-end: a support calendar with working hours and a couple of Iranian public holidays, then compute an SLA deadline.
// Names are illustrative — confirm against the config/docs for v1.0.0.
$calendar = Calendar::source('config')->calendar('support');
$deadline = $calendar->addWorkingTime(now(), 'PT8H'); // 8 working hours from now
$remaining = $calendar->workingTimeBetween(now(), $deadline);
Once that works, the natural next step is reverse scheduling (when must we start to finish by X?) and snapshot validation.
Config source for code-owned calendars. If your calendar is part of the app's contract — an ops team's fixed shift pattern, a compliance-defined holiday list — keep it in the config source and commit it. It is versioned by git, reviewed in PRs, and needs no migration step. Treat the config file as production code, not as a place for per-environment tweaks.
Database source for admin-editable calendars. For multi-tenant or admin-managed calendars, use the database source. v1.0.0 introduces revision-based storage with atomic publication, which gives you a clean draft/publish workflow:
$revision = Calendar::source('database')->draft('support');
$revision->addHoliday('2024-03-20', 'Nowruz');
$revision->validate(); // snapshot validation
$revision->publish(); // atomic swap — readers see old or new, never half
The atomic publication guarantee is the important part: in-flight SLA calculations running against the old revision complete correctly while the new one goes live. Design your admin UI around this — edits go to a draft, a human publishes, nothing mutates live definitions in place.
Wrap request/job work in an operation context. Where you previously scattered config() lookups or injected repositories, resolve the source once at the boundary (a middleware, a queued job's handle(), an artisan command) and pass the context down. This makes the exclusive-source rule trivial to honour and makes tests deterministic — the test suite sets the context to a fixture source instead of mocking singletons.
Import/export as the ops interface. The import/export round-trip (including restore of imported Iran calendar data) is the pragmatic way to move calendars between staging and production, seed test fixtures from real operational data, and let non-developers author holidays in a spreadsheet. Export from production periodically as a backup; the file format is stable in a way the schema is not.
SLA and deadline calculation. All working-time arithmetic now runs at microsecond precision and has standardised boundary behaviour. Use the package's arithmetic rather than computing working hours yourself — the DST, holiday and boundary edge cases are exactly what you are paying for. Store deadlines as timestamps computed at creation time, and recompute them when a revision is published if the business rule is "SLA follows the calendar."
Testing. Freeze the clock (Carbon::setTestNow) and assert against the calendar's snapshot rather than against hand-computed expectations. v1.0.0 ships regression, database and integration suites you can crib from; the snapshot validation feature means you can assert the definition set itself is what you expect before asserting on arithmetic.
This is a breaking release. Budget a migration, not an upgrade. Three changes will bite:
Chain and Hybrid storage modes are removed. If you were layering config with database overrides, you must now pick one source. The usual migration is: move the merged result into the database and make that authoritative, or move it into config and make it code-owned. Decide deliberately; there is no middle ground anymore.Microsecond precision changes equality. Comparing timestamps with === or an exact DB datetime column will now fail where it previously passed. Use equalTo() with a tolerance, and check your column precision — datetime without fractional seconds silently truncates, so a round-tripped value will not equal the computed one.
Exclusive sources fail loudly or quietly, depending on the path. Don't write conditional code that reaches for both. A single operation context with one source is the contract.
Stale caches after publish. Publication is atomic at the storage layer, but if you cache resolved calendars (config cache, php artisan cache:clear, a repository decorator) you must invalidate on publish. Symptom: the admin UI shows the new revision, calculations still use the old one.
Timezone discipline. Keep the app timezone and the calendar timezone in mind separately. DST handling is much improved, but the test worth writing is: a working-time span that crosses a transition, asserted against the snapshot, with Carbon::setTestNow pinning the date. Also verify behaviour for the Iranian calendar year boundary, where "same time next year" is genuinely not a fixed number of seconds.
Snapshot validation is your debugging entry point. When arithmetic looks wrong, validate the snapshot first — the package will tell you the definition set is internally inconsistent (overlapping shifts, holiday outside range, empty working week) rather than letting you chase a wrong number. Add a validate step to your tests and to your publish flow.
Reverse scheduling and boundaries. The new standardised boundary behaviour means "start of working day" and "end of working day" are now well-defined. If you previously added a fudge factor of one second to make ranges inclusive, remove it — it is now double-counting.
RTL/Persian output. The docs fix is cosmetic, but the same class of bug hits your app: if you render Persian calendar output in an RTL layout, force dir="ltr" on code samples, logs and timestamps in admin views. Mixed-direction dates are the most common UI complaint.
Extension points worth knowing. Custom holiday providers, a custom definition source, and a custom operation context are the three seams for app-specific behaviour. Reach for the operation context before you subclass anything in the arithmetic layer — that layer was just rewritten and is the least stable surface to build on.
Migration checklist. New config published and diffed → source chosen (config or database, no chain) → stored deadlines recomputed → cache invalidation wired to publish → tests moved to frozen-clock + snapshot assertions → docs read in English or Persian, not the old ones.
How can I help you explore Laravel packages today?