laraveljutsu/zap
Zap is a calendar and scheduling package for Laravel. Define availabilities, appointments, blocked times, and custom schedules for any resource like doctors, rooms, or employees. Handles overlaps, recurrence patterns, and availability queries. PHP 8.5+ and Laravel 13+.
Architecture fit
Zap is a domain model + query engine for temporal availability, not a booking UI. It slots in as an infrastructure concern behind whatever booking flow already exists.
HasSchedules is a trait applied to any Eloquent model, backed by a schedulable morph. One package serves doctors, rooms, employees, and equipment without schema-per-resource. This is the right shape for multi-tenant or multi-resource scheduling products.09:00–12:00, 14:00–17:00), which avoids row explosion for split shifts and is the correct normalization.start_month offsets, ordinal weekday (lastMondayOfMonth), and dynamic N-week / N-month intervals. Teams typically either hand-roll this (bug farm) or integrate a generic RRULE library with awkward Laravel ergonomics. Covering ordinal-weekday and N-week anchors natively is disproportionately valuable for HR/shift and clinic use cases.getBookableSlots($date, $duration, $buffer) plus getNextBookableSlot, isBookableAt, isBookableAtTime, and findConflicts/hasConflicts. That is the surface we would actually expose to a UI; the builder API is authoring-only. This separation is good — it means slot computation can be swapped or cached independently of schedule storage.Integration feasibility
Low code footprint, but gated by version constraints.
composer require → publish provider → publish migrations/config → migrate, then one trait per schedulable model. That is a sub-day integration for a greenfield resource type.composer.json. The first action item is reading the actual constraint block, because it determines whether this is installable on our current Laravel 11/12 + PHP 8.3 estate or requires a runtime upgrade first. If the README badge is accurate, adoption is effectively blocked until we are on Laravel 13 — a multi-quarter dependency, not a sprint task.Zap\Models\Schedule and SchedulePeriod, adding HasUuids, repointing models.schedule/models.schedule_period in config, and rewriting id() → uuid('id'), morphs → uuidMorphs, foreignId('schedule_id') → foreignUuid — before running migrations. Any team that migrates first and reads the docs second ships a broken schema. Most modern Laravel codebases use UUIDs, so this is the likely default path, not the edge case.Technical risk
isAvailableAt() is already deprecated in favor of isBookableAt*. Expect renames through the 1.x line.noOverlap(), conflict_detection) appears to be application-level. If there is no transactional lock, unique constraint, or SELECT ... FOR UPDATE around check-and-write, two simultaneous booking requests can both pass validation and double-book. This is the single highest-severity exposure for any booking product and must be verified in source, not assumed.'09:00', '17:00') across date ranges spanning a year. Nothing in the README states what timezone periods are interpreted in, whether the schedulable model carries a timezone, or how a recurring 09:00 behaves across a DST boundary. For a global product this is a blocking unknown; for a single-timezone internal tool it is tolerable.Key questions
composer.json constraints for php and illuminate/*, and do they match the README badges or the CI matrix?forYear(2025) weekly schedule across 200 resources), and is expansion done in PHP or SQL?Stack fit
Conditional. Zap is a strong fit for Laravel applications that are (a) already on, or imminently moving to, Laravel 13 / PHP 8.5, and (b) have a genuine multi-resource or complex-recurrence requirement. It is a poor fit as a drop-in on a mature Laravel 10/11/12 estate, where the constraint block alone likely prevents composer require. It is also over-engineered for a single-resource, simple-hours booking flow where a starts_at/ends_at table and a unique index would suffice.
Migration path
Treat Zap as a slot-computation service, not a booking system of record, for the first phase.
composer.json; stand up a throwaway PHP 8.5 / Laravel 13 container; run composer require and the package's own test suite. Output: go/no-go on install, and a definitive supported-version matrix.getBookableSlots() read-only and diff its output against the current engine over a 90-day window. No writes, no user impact.For a UUID codebase, step 2 must be preceded by the model-extension and migration-editing work from the README, with the config repoint committed before php artisan migrate ever runs.
Compatibility
zap-schedules, zap-availability, zap-recurrence) ship with the package and require no configuration. If the team uses AI-assisted development, this measurably lowers the cost of getting recurrence semantics right and reduces hallucinated method names — worth enabling deliberately rather than incidentally.Sequencing
| Phase | Work | Gate |
|---|---|---|
| 0 | Constraint spike on PHP 8.5 / Laravel 13 | composer require succeeds, package tests pass |
| 1 | Schema review; UUID path if applicable | Migrations apply cleanly on a clone of prod |
| 2 | Single resource, shadow-mode slot computation | Parity diff clean over 90 days |
| 3 | Concurrency source review + load test | Double-booking provably impossible or mitigated |
| 4 | Dual-write | No divergence under production traffic |
| 5 | Read cutover, then write cutover | Rollback rehearsed |
| 6 | Legacy retirement | — |
Do not compress phases 2–4. The package's core value is correctness in edge cases, and edge cases are exactly what shadow-mode diffing is for.
Maintenance
Ongoing cost is low in code and moderate in risk. The package is MIT, self-contained, two tables plus a config file, and does not patch Laravel internals. Upgrades track Laravel majors, which ship annually — so expect an annual compatibility hop, and expect it to be gated on the maintainer publishing first, since a single-maintainer project cannot ship a fix on our timeline. The realistic mitigation is vendoring: fork the package into our VCS at a pinned commit so we can patch a constraint or a bug without waiting for upstream. With 0 dependents and one maintainer, the fork is not optional insurance — it is the plan.
Support
No commercial support tier. Channels are GitHub issues and a docs site (laravel-zap.com); security is a personal mailbox rather than a disclosure policy. For an internal scheduling tool this is acceptable. For a customer-facing booking product, we need an internal owner who can read the source, because there is no escalation path and no SLA.
Scaling
The scaling profile is recurrence expansion, not request volume. getBookableSlots() must materialize candidate periods for a date before subtracting appointments and blocks. A weekly forYear() availability across a date-range query, multiplied by N resources in a listing view, is a plausible hot path. Mitigations: cache computed slot sets per (resource, date), keep queries to single-date granularity rather than wide ranges, and confirm the schedules table is indexed on (schedulable_type, schedulable_id) plus the date bounds — the morphs() helper covers the first two but not the date columns. v1.3.0's query optimization PR (#26) and v1.5.0's buffer-time support indicate the maintainer is watching this, but we should benchmark our own resource cardinality rather than assume.
Failure modes
| Failure | Impact | Detection | Mitigation |
|---|---|---|---|
| Concurrent double-booking | Customer-facing, high | Load test in phase 3 | Transactional lock / unique constraint outside the package if absent |
| Timezone / DST drift | Silent wrong slots, seasonal | Golden-file tests across DST boundary | Confirm tz model; normalize at the boundary |
| Recurrence anchor errors (biweekly, every-N-weeks) | Wrong days offered | Parity diff over 90+ days | Fixture tests pinning anchor dates |
| UUID misconfiguration before migrate | Schema broken at deploy | Staging clone run | Enforce documented ordering in the runbook |
| Laravel major upgrade blocked on upstream | Security patching stalls | Annual dependency review | Maintained fork |
| Silent empty availability (bad anchor / typo'd period) | Users see "no slots", no error | Slot-count alerts per resource | Instrument: alert when a resource with availability returns zero slots for 3+ days |
| Overlap-validation bypass via custom type | Double booking on custom rules | Code review | Restrict custom() to reviewed use only |
The most dangerous failure mode is the quiet ones — DST drift and anchor errors return plausible-looking slots rather than errors, so they are found by customers, not by monitoring. Instrument slot-count anomalies.
Ramp-up
Authoring API reads well and ramps fast: Zap::for($doctor)->named('Office Hours')->availability()->forYear(2025)->addPeriod('09:00','12:00')->weekly([...])->save() is close to self-documenting for a mid-level Laravel developer. Expect under a day to productive for simple availability, blocks, and appointments.
Recurrence is the real learning curve. Odd/even weeks, weekOddDays/weekEvenDays, biweekly with an anchor date, and everyFourWeeks with a separate anchor argument have subtle, easy-to-invert semantics, and a mistake produces wrong data rather than a crash. Budget for: a curated internal cheat-sheet restricted to the 5 patterns we actually use, a fixture test suite for each of those patterns, and a lint/review rule that new recurrence configurations require a second pair of eyes. The shipped Boost skills help here if the team uses AI-assisted development — point agents at zap-recurrence rather than letting them infer the API.
Recommendation: pursue only if the constraint check in phase 0 passes and the target stack is Laravel 13 / PHP 8.5. Adopt as a slot-computation engine behind a shadow-mode parity gate, fork for supply-chain safety, and verify concurrency handling in source before any customer-facing write path is enabled. Score 6.9 is a fair read: high conceptual value in recurrence, low maturity as a dependency.
How can I help you explore Laravel packages today?