ludoguenet/laravel-zap

The missing calendar management for Laravel

PHP

1,469

170 commits

updated Sep 30, 2026

See the code

README

Zap Logo

Flexible schedule management for modern Laravel applications

PHP Version Laravel Version License Total Downloads Why PHP

Website • Documentation • Support


Table of contents


🎯 What is Zap?

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.


📦 Installation

Requirements: PHP ≥8.5 • Laravel ≥12.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;
}

🧩 Core concepts

TypePurposeOverlaps
AvailabilityWhen a resource can be bookedAllowed
AppointmentBookings / scheduled eventsExclusive
BlockedWhen booking is forbiddenExclusive
CustomYour rules (overlap, etc.)You define

🚀 Quick start

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)->...


📅 Schedule patterns

Recurrence at a glance

PatternMethod / example
Dailydaily()
Weekly (days)weekly(['monday', 'friday'])
Weekly + periodweekDays(['monday', 'friday'], '09:00', '17:00')
Odd/even weeksweeklyOdd(), weeklyEven() (+ weekOddDays / weekEvenDays)
Bi-weeklybiweekly(['tuesday'], $startsOn?)
Monthly (dates)monthly(['days_of_month' => [1, 15]])
Bi-monthly / quarter / semi / annualbimonthly(), quarterly(), semiannually(), annually() + config
Ordinal weekdayfirstWednesdayOfMonth(), secondFridayOfMonth(), lastMondayOfMonth()
Every N weekseveryThreeWeeks(), … everyFiftyTwoWeeks()
Every N monthseveryFourMonths(), … everyElevenMonths()

Recurrence examples

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);

Date ranges

$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

Time periods

$schedule->addPeriod('09:00', '17:00');
$schedule->addPeriod('09:00', '12:00');
$schedule->addPeriod('14:00', '17:00');

🔍 Query & check availability

NeedMethod
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 scheduleZap::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. Prefer isBookableAt(), isBookableAtTime(), and getBookableSlots().


💼 Real-world examples

Doctor

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);

Meeting room

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' => '[email protected]'])->save();

Employee (with vacation)

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();

⚙️ Configuration

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.


🛡️ Advanced features

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().

Custom model support (UUIDs)

If your app uses UUIDs/ULIDs for primary keys:

  1. Models — Extend Zap\Models\Schedule and Zap\Models\SchedulePeriod, add Laravel’s HasUuids trait. Add HasUuids to your schedulable model (e.g. Doctor) as well.
  2. Config — In config/zap.php, set models.schedule and models.schedule_period to your extended classes.
  3. Migrations — After publishing, change 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.


🧠 AI agent support

Zap provides Laravel Boost 2.0 skills. With Boost installed, agents get accurate knowledge of the API.

SkillContents
zap-schedulesTypes, builder API, validation, conflict detection
zap-availabilityBookable slots, availability checks, querying
zap-recurrenceAll recurrence patterns (daily, weekly, odd/even, monthly, ordinal weekday, dynamic)

No extra configuration.


🤝 Contributing

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

📄 License

MIT License.

🔒 Security

Report issues to [email protected] (not the public issue tracker).


Made with 💛 by Ludovic Guénet for the Laravel community

calendar
dates
laravel
schedule

ludoguenet/laravel-zap

The missing calendar management for Laravel

PHP

1,469

170 commits

updated Sep 30, 2026

See the code

README

Zap Logo

Flexible schedule management for modern Laravel applications

PHP Version Laravel Version License Total Downloads Why PHP

Website • Documentation • Support


Table of contents


🎯 What is Zap?

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.


📦 Installation

Requirements: PHP ≥8.5 • Laravel ≥12.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;
}

🧩 Core concepts

TypePurposeOverlaps
AvailabilityWhen a resource can be bookedAllowed
AppointmentBookings / scheduled eventsExclusive
BlockedWhen booking is forbiddenExclusive
CustomYour rules (overlap, etc.)You define

🚀 Quick start

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)->...


📅 Schedule patterns

Recurrence at a glance

PatternMethod / example
Dailydaily()
Weekly (days)weekly(['monday', 'friday'])
Weekly + periodweekDays(['monday', 'friday'], '09:00', '17:00')
Odd/even weeksweeklyOdd(), weeklyEven() (+ weekOddDays / weekEvenDays)
Bi-weeklybiweekly(['tuesday'], $startsOn?)
Monthly (dates)monthly(['days_of_month' => [1, 15]])
Bi-monthly / quarter / semi / annualbimonthly(), quarterly(), semiannually(), annually() + config
Ordinal weekdayfirstWednesdayOfMonth(), secondFridayOfMonth(), lastMondayOfMonth()
Every N weekseveryThreeWeeks(), … everyFiftyTwoWeeks()
Every N monthseveryFourMonths(), … everyElevenMonths()

Recurrence examples

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);

Date ranges

$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

Time periods

$schedule->addPeriod('09:00', '17:00');
$schedule->addPeriod('09:00', '12:00');
$schedule->addPeriod('14:00', '17:00');

🔍 Query & check availability

NeedMethod
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 scheduleZap::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. Prefer isBookableAt(), isBookableAtTime(), and getBookableSlots().


💼 Real-world examples

Doctor

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);

Meeting room

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' => '[email protected]'])->save();

Employee (with vacation)

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();

⚙️ Configuration

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.


🛡️ Advanced features

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().

Custom model support (UUIDs)

If your app uses UUIDs/ULIDs for primary keys:

  1. Models — Extend Zap\Models\Schedule and Zap\Models\SchedulePeriod, add Laravel’s HasUuids trait. Add HasUuids to your schedulable model (e.g. Doctor) as well.
  2. Config — In config/zap.php, set models.schedule and models.schedule_period to your extended classes.
  3. Migrations — After publishing, change 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.


🧠 AI agent support

Zap provides Laravel Boost 2.0 skills. With Boost installed, agents get accurate knowledge of the API.

SkillContents
zap-schedulesTypes, builder API, validation, conflict detection
zap-availabilityBookable slots, availability checks, querying
zap-recurrenceAll recurrence patterns (daily, weekly, odd/even, monthly, ordinal weekday, dynamic)

No extra configuration.


🤝 Contributing

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

📄 License

MIT License.

🔒 Security

Report issues to [email protected] (not the public issue tracker).


Made with 💛 by Ludovic Guénet for the Laravel community

calendar
dates
laravel
schedule

Significant stargazers

nunomaduro

9,419 followers · starred Jun 2025

Niels Vanpachtenbeke

87 followers · starred Dec 2025

Kevin Woblick

364 followers · starred Jul 2025

Nico Devs

101 followers · starred Jun 2025