laraveljutsu/zap
Zap is a Laravel scheduling package to manage availabilities, appointments, blocked times, and custom schedules for any resource (doctors, rooms, employees). Query availability, prevent overlaps, and build booking, shift, or shared space workflows.
Flexible schedule management for modern Laravel applications
Website β’ Documentation β’ Support
Zap is a calendar and scheduling package for Laravel. Define availabilities, appointments, blocked times, and custom schedules for any resource (doctors, rooms, employees, etc.).
Use cases: appointment booking, healthcare resources, employee shifts, shared space booking.
Requirements: PHP β₯8.5 β’ Laravel β₯13.0
composer require laraveljutsu/zap
php artisan vendor:publish --provider="Zap\ZapServiceProvider"
UUIDs/ULIDs: If your app uses non-integer primary keys, read Custom model support before migrating. You may need to change migrations and config.
php artisan migrate
Make a model schedulable: add the HasSchedules trait.
use Zap\Models\Concerns\HasSchedules;
class Doctor extends Model
{
use HasSchedules;
}
| Type | Purpose | Overlaps |
|---|---|---|
| Availability | When a resource can be booked | Allowed |
| Appointment | Bookings / scheduled events | Exclusive |
| Blocked | When booking is forbidden | Exclusive |
| Custom | Your rules (overlap, etc.) | You define |
use Zap\Facades\Zap;
// 1. Working hours
Zap::for($doctor)
->named('Office Hours')
->availability()
->forYear(2025)
->addPeriod('09:00', '12:00')
->addPeriod('14:00', '17:00')
->weekly(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'])
->save();
// 2. Block lunch
Zap::for($doctor)
->named('Lunch Break')
->blocked()
->forYear(2025)
->addPeriod('12:00', '13:00')
->weekly(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'])
->save();
// 3. Create an appointment
Zap::for($doctor)
->named('Patient A - Consultation')
->appointment()
->from('2025-01-15')
->addPeriod('10:00', '11:00')
->withMetadata(['patient_id' => 1, 'type' => 'consultation'])
->save();
// 4. Get bookable slots (60 min, 15 min buffer)
$slots = $doctor->getBookableSlots('2025-01-15', 60, 15);
// 5. Next available slot
$nextSlot = $doctor->getNextBookableSlot('2025-01-15', 60, 15);
π‘ Use the
zap()helper instead of the facade when you prefer:zap()->for($doctor)->...
| Pattern | Method / example |
|---|---|
| Daily | daily() |
| Weekly (days) | weekly(['monday', 'friday']) |
| Weekly + period | weekDays(['monday', 'friday'], '09:00', '17:00') |
| Odd/even weeks | weeklyOdd(), weeklyEven() (+ weekOddDays / weekEvenDays) |
| Bi-weekly | biweekly(['tuesday'], $startsOn?) |
| Monthly (dates) | monthly(['days_of_month' => [1, 15]]) |
| Bi-monthly / quarter / semi / annual | bimonthly(), quarterly(), semiannually(), annually() + config |
| Ordinal weekday | firstWednesdayOfMonth(), secondFridayOfMonth(), lastMondayOfMonth() |
| Every N weeks | everyThreeWeeks(), β¦ everyFiftyTwoWeeks() |
| Every N months | everyFourMonths(), β¦ everyElevenMonths() |
Daily & weekly
$schedule->daily()->from('2025-01-01')->to('2025-12-31');
$schedule->weekly(['monday', 'wednesday', 'friday'])->forYear(2025);
$schedule->weekDays(['monday', 'wednesday', 'friday'], '09:00', '17:00')->forYear(2025);
$schedule->weeklyOdd(['monday', 'wednesday', 'friday'])->forYear(2025);
$schedule->weeklyEven(['monday', 'wednesday', 'friday'])->forYear(2025);
$schedule->biweekly(['tuesday', 'thursday'], '2025-01-07')->from('2025-01-07')->to('2025-03-31');
Monthly (by day of month)
$schedule->monthly(['days_of_month' => [1, 15]])->forYear(2025);
$schedule->bimonthly(['days_of_month' => [5, 20], 'start_month' => 2])->from('2025-01-05')->to('2025-06-30');
$schedule->quarterly(['days_of_month' => [7, 21], 'start_month' => 2])->from('2025-02-15')->to('2025-11-15');
$schedule->semiannually(['days_of_month' => [10], 'start_month' => 3])->from('2025-03-10')->to('2025-12-10');
$schedule->annually(['days_of_month' => [1, 15], 'start_month' => 4])->from('2025-04-01')->to('2026-04-01');
Monthly ordinal weekday (1st, 2nd, 3rd, 4th, or last weekday of the month)
$schedule->firstWednesdayOfMonth()->forYear(2025); // Every 1st Wednesday
$schedule->secondFridayOfMonth()->forYear(2025); // Every 2nd Friday
$schedule->lastMondayOfMonth()->forYear(2025); // Every last Monday
// Also: thirdTuesdayOfMonth(), fourthSaturdayOfMonth(), lastSundayOfMonth(), etc.
Dynamic intervals
$schedule->everyThreeWeeks(['monday', 'friday'])->from('2025-01-06')->to('2025-12-31');
$schedule->everyFourWeeks(['tuesday'], '2025-01-06')->from('2025-01-13');
$schedule->everyFourMonths(['day_of_month' => 15])->forYear(2025);
$schedule->everyFiveMonths(['days_of_month' => [1, 15], 'start_month' => 2])->forYear(2025);
$schedule->from('2025-01-15'); // Start
$schedule->on('2025-01-15'); // Alias for from()
$schedule->from('2025-01-01')->to('2025-12-31'); // Range
$schedule->between('2025-01-01', '2025-12-31'); // Same
$schedule->forYear(2025); // Full year
$schedule->addPeriod('09:00', '17:00');
$schedule->addPeriod('09:00', '12:00');
$schedule->addPeriod('14:00', '17:00');
| Need | Method |
|---|---|
| Any bookable slot today? | $model->isBookableAt('2025-01-15', 60) |
| Time range bookable? | $model->isBookableAtTime('2025-01-15', '09:00', '09:30') |
| Time range bookable (custom slot)? | $model->isBookableAtTime('2025-01-15', '09:30', '10:00', null, 30) |
| Time range bookable (custom slot + buffer)? | $model->isBookableAtTime('2025-01-15', '09:45', '10:15', null, 30, 15) |
| List bookable slots | $model->getBookableSlots('2025-01-15', 60, 15) |
| Next bookable slot | $model->getNextBookableSlot('2025-01-15', 60, 15) |
| Conflicts for a schedule | Zap::findConflicts($schedule) / Zap::hasConflicts($schedule) |
| Schedules on a date | $model->schedulesForDate('2025-01-15')->get() |
| Schedules in range | $model->schedulesForDateRange('2025-01-01', '2025-01-31')->get() |
| By type | $model->appointmentSchedules(), availabilitySchedules(), blockedSchedules() |
| Schedule type checks | $schedule->isAvailability(), isAppointment(), isBlocked() |
β οΈ
isAvailableAt()is deprecated. PreferisBookableAt(),isBookableAtTime(), andgetBookableSlots().
Zap::for($doctor)->named('Office Hours')->availability()->forYear(2025)
->addPeriod('09:00', '12:00')->addPeriod('14:00', '17:00')
->weekly(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'])->save();
Zap::for($doctor)->named('Lunch Break')->blocked()->forYear(2025)
->addPeriod('12:00', '13:00')
->weekly(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'])->save();
Zap::for($doctor)->named('Patient A - Checkup')->appointment()
->from('2025-01-15')->addPeriod('10:00', '11:00')->withMetadata(['patient_id' => 1])->save();
$slots = $doctor->getBookableSlots('2025-01-15', 60, 15);
Zap::for($room)->named('Conference Room A')->availability()
->weekDays(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], '08:00', '18:00')
->forYear(2025)->save();
Zap::for($room)->named('Board Meeting')->appointment()
->from('2025-03-15')->addPeriod('09:00', '11:00')
->withMetadata(['organizer' => 'john@company.com'])->save();
Zap::for($employee)->named('Regular Shift')->availability()
->weekDays(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], '09:00', '17:00')
->forYear(2025)->save();
Zap::for($employee)->named('Vacation Leave')->blocked()
->between('2025-06-01', '2025-06-15')->addPeriod('00:00', '23:59')->save();
Publish assets:
php artisan vendor:publish --tag=zap-migrations
php artisan vendor:publish --tag=zap-config
Important keys in config/zap.php: time_slots.buffer_minutes, default_rules.no_overlap, conflict_detection, validation.
Custom schedules & rules
Zap::for($user)->named('Custom Event')->custom()
->from('2025-01-15')->addPeriod('15:00', '16:00')->noOverlap()->save();
Metadata
->withMetadata(['patient_id' => 1, 'type' => 'consultation', 'notes' => 'Follow-up'])
Validation rules: noOverlap(), allowOverlap(), workingHoursOnly('09:00', '17:00'), maxDuration(120), noWeekends().
If your app uses UUIDs/ULIDs for primary keys:
Zap\Models\Schedule and Zap\Models\SchedulePeriod, add Laravelβs HasUuids trait. Add HasUuids to your schedulable model (e.g. Doctor) as well.config/zap.php, set models.schedule and models.schedule_period to your extended classes.id() to uuid('id')->primary(), morphs('schedulable') to uuidMorphs('schedulable'), and foreignId('schedule_id') to foreignUuid('schedule_id') in the schedules and schedule_periods tables.Do this before running migrations.
Zap provides Laravel Boost 2.0 skills. With Boost installed, agents get accurate knowledge of the API.
| Skill | Contents |
|---|---|
zap-schedules |
Types, builder API, validation, conflict detection |
zap-availability |
Bookable slots, availability checks, querying |
zap-recurrence |
All recurrence patterns (daily, weekly, odd/even, monthly, ordinal weekday, dynamic) |
No extra configuration.
Contributions are welcome. Use PSR-12 and add tests.
git clone https://github.com/ludoguenet/laravel-zap.git
cd laravel-zap
composer install
composer pest
Report issues to ludo@epekta.com (not the public issue tracker).
Made with π by Ludovic GuΓ©net for the Laravel community
How can I help you explore Laravel packages today?