laraveljutsu / zap
A flexible, performant, and developer-friendly schedule management system for Laravel
Installs: 52 722
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1 398
Watchers: 16
Forks: 99
Open Issues: 2
pkg:composer/laraveljutsu/zap
Requires
- php: >=8.2 <8.6
- ext-intl: *
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.20
- orchestra/testbench: ^10.0
- pestphp/pest: ^4.0
README
Flexible schedule management for modern Laravel applications
Website β’ Documentation β’ Support
Table of contents
- What is Zap?
- Installation
- Core concepts
- Quick start
- Schedule patterns
- Query & availability
- Real-world examples
- Configuration
- Advanced
- AI agent support
- Contributing
π― 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
| 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 |
π 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
| 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() |
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
| Need | Method |
|---|---|
| Any bookable slot today? | $model->isBookableAt('2025-01-15', 60) |
| Time range bookable? | $model->isBookableAtTime('2025-01-15', '09:00', '09:30') |
| 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().
πΌ 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' => 'john@company.com'])->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:
- Models β Extend
Zap\Models\ScheduleandZap\Models\SchedulePeriod, add LaravelβsHasUuidstrait. AddHasUuidsto your schedulable model (e.g.Doctor) as well. - Config β In
config/zap.php, setmodels.scheduleandmodels.schedule_periodto your extended classes. - Migrations β After publishing, change
id()touuid('id')->primary(),morphs('schedulable')touuidMorphs('schedulable'), andforeignId('schedule_id')toforeignUuid('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.
| 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.
π€ 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
π Security
Report issues to ludo@epekta.com (not the public issue tracker).
Made with π by Ludovic GuΓ©net for the Laravel community