Zap is a comprehensive calendar and scheduling system for Laravel. Manage availabilities, appointments, blocked times, and custom schedules for any resourceβdoctors, meeting rooms, employees, and more.
Perfect for:
- π Appointment booking systems
- π₯ Healthcare resource management
- π Employee shift scheduling
- π’ Shared office space bookings
Requirements: PHP β€8.5 β’ Laravel β€12.0
You can install the package via composer:
composer require laraveljutsu/zapYou should publish the migration and the config/zap.php config file with:
php artisan vendor:publish --provider="Zap\ZapServiceProvider"If you are USING UUIDs, see the Custom Model Support section of the docs on UUID steps, before you continue. It explains some changes you may want to make to the migrations and config file before continuing. It also mentions important considerations after extending this package's models for UUID capability.
If so, run the migration command:
php artisan migrateThis package expects the primary key of your models to be an auto-incrementing int. If it is not, you may need to modify the create_schedules_table and create_schedule_periods_table migration and/or modify the default configuration. See Custom Model Support for more information.
Add the HasSchedules trait to any Eloquent model you want to make schedulable:
use Zap\Models\Concerns\HasSchedules;
class Doctor extends Model
{
use HasSchedules;
}Zap uses four schedule types to model different scenarios:
| Type | Purpose | Overlap Behavior |
|---|---|---|
| Availability | Define when resources can be booked | β Allows overlaps |
| Appointment | Actual bookings or scheduled events | β Prevents overlaps |
| Blocked | Periods where booking is forbidden | β Prevents overlaps |
| Custom | Neutral schedules with explicit rules | βοΈ You define the rules |
Here's a complete example of setting up a doctor's schedule:
use Zap\Facades\Zap;
// 1οΈβ£ Define 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 break
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 slots, 15 min buffer)
$slots = $doctor->getBookableSlots('2025-01-15', 60, 15);
// Returns: [['start_time' => '09:00', 'end_time' => '10:00', 'is_available' => true, ...], ...]
// 5οΈβ£ Find next available slot
$nextSlot = $doctor->getNextBookableSlot('2025-01-15', 60, 15);π‘ Tip: You can also use the
zap()helper function instead of the facade:zap()->for($doctor)->...(no import needed)
Zap supports various recurrence patterns for flexible scheduling:
// Daily
$schedule->daily()->from('2025-01-01')->to('2025-12-31');
// Weekly (specific days)
$schedule->weekly(['monday', 'wednesday', 'friday'])->forYear(2025);
// Weekly with time period (convenience method)
$schedule->weekDays(['monday', 'wednesday', 'friday'], '09:00', '17:00')->forYear(2025);
// Weekly odd (runs only on odd-numbered weeks)
$schedule->weeklyOdd(['monday', 'wednesday', 'friday'])->forYear(2025);
// Weekly odd with time period (convenience method)
$schedule->weekOddDays(['monday', 'wednesday', 'friday'], '09:00', '17:00')->forYear(2025);
// Weekly even (runs only on even-numbered weeks)
$schedule->weeklyEven(['monday', 'wednesday', 'friday'])->forYear(2025);
// Weekly even with time period (convenience method)
$schedule->weekEvenDays(['monday', 'wednesday', 'friday'], '09:00', '17:00')->forYear(2025);
// Bi-weekly (week of the start date by default, optional anchor)
$schedule->biweekly(['tuesday', 'thursday'])->from('2025-01-07')->to('2025-03-31');
// Monthly (supports multiple days)
$schedule->monthly(['days_of_month' => [1, 15]])->forYear(2025);
// Bi-monthly (multiple days, optional start_month anchor)
$schedule->bimonthly(['days_of_month' => [5, 20], 'start_month' => 2])
->from('2025-01-05')->to('2025-06-30');
// Quarterly (multiple days, optional start_month anchor)
$schedule->quarterly(['days_of_month' => [7, 21], 'start_month' => 2])
->from('2025-02-15')->to('2025-11-15');
// Semi-annually (multiple days, optional start_month anchor)
$schedule->semiannually(['days_of_month' => [10], 'start_month' => 3])
->from('2025-03-10')->to('2025-12-10');
// Annually (multiple days, optional start_month anchor)
$schedule->annually(['days_of_month' => [1, 15], 'start_month' => 4])
->from('2025-04-01')->to('2026-04-01');Specify when schedules are active:
$schedule->from('2025-01-15'); // Single date
$schedule->on('2025-01-15'); // Alias for from()
$schedule->from('2025-01-01')->to('2025-12-31'); // Date range
$schedule->between('2025-01-01', '2025-12-31'); // Alternative syntax
$schedule->forYear(2025); // Entire year shortcutDefine working hours and time slots:
// Single period
$schedule->addPeriod('09:00', '17:00');
// Multiple periods (split shifts)
$schedule->addPeriod('09:00', '12:00');
$schedule->addPeriod('14:00', '17:00');Check availability and query schedules:
// Check if there is at least one bookable slot on the day
$isBookable = $doctor->isBookableAt('2025-01-15', 60);
// Check if a specific time range is bookable
$isBookable = $doctor->isBookableAtTime('2025-01-15', '9:00', '9:30');
// Get bookable slots
$slots = $doctor->getBookableSlots('2025-01-15', 60, 15);
// Find conflicts
$conflicts = Zap::findConflicts($schedule);
$hasConflicts = Zap::hasConflicts($schedule);
// Query schedules
$doctor->schedulesForDate('2025-01-15')->get();
$doctor->schedulesForDateRange('2025-01-01', '2025-01-31')->get();
// Filter by type
$doctor->appointmentSchedules()->get();
$doctor->availabilitySchedules()->get();
$doctor->blockedSchedules()->get();
// Check schedule type
$schedule->isAvailability();
$schedule->isAppointment();
$schedule->isBlocked();
β οΈ Note:isAvailableAt()is deprecated in favor ofisBookableAt(),isBookableAtTime(), andgetBookableSlots(). Use the bookable APIs for all new code.
// Office hours
Zap::for($doctor)
->named('Office Hours')
->availability()
->forYear(2025)
->weekly(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'])
->addPeriod('09:00', '12:00')
->addPeriod('14:00', '17:00')
->save();
// Lunch break
Zap::for($doctor)
->named('Lunch Break')
->blocked()
->forYear(2025)
->weekly(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'])
->addPeriod('12:00', '13:00')
->save();
// Book appointment
Zap::for($doctor)
->named('Patient A - Checkup')
->appointment()
->from('2025-01-15')
->addPeriod('10:00', '11:00')
->withMetadata(['patient_id' => 1])
->save();
// Get available slots
$slots = $doctor->getBookableSlots('2025-01-15', 60, 15);// Room availability (using weekDays convenience method)
Zap::for($room)
->named('Conference Room A')
->availability()
->weekDays(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], '08:00', '18:00')
->forYear(2025)
->save();
// Book meeting
Zap::for($room)
->named('Board Meeting')
->appointment()
->from('2025-03-15')
->addPeriod('09:00', '11:00')
->withMetadata(['organizer' => '[email protected]'])
->save();// Regular schedule (using weekDays convenience method)
Zap::for($employee)
->named('Regular Shift')
->availability()
->weekDays(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], '09:00', '17:00')
->forYear(2025)
->save();
// Vacation
Zap::for($employee)
->named('Vacation Leave')
->blocked()
->between('2025-06-01', '2025-06-15')
->addPeriod('00:00', '23:59')
->save();Publish the migration:
php artisan vendor:publish --tag=zap-migrationsPublish and customize the configuration:
php artisan vendor:publish --tag=zap-configKey settings in config/zap.php:
'time_slots' => [
'buffer_minutes' => 0, // Default buffer between slots
],
'default_rules' => [
'no_overlap' => [
'enabled' => true,
'applies_to' => ['appointment', 'blocked'],
],
],Create custom schedules with explicit overlap rules:
Zap::for($user)
->named('Custom Event')
->custom()
->from('2025-01-15')
->addPeriod('15:00', '16:00')
->noOverlap() // Explicitly prevent overlaps
->save();Attach custom metadata to schedules:
->withMetadata([
'patient_id' => 1,
'type' => 'consultation',
'notes' => 'Follow-up required'
])If you're using UUIDs (ULID, GUID, etc) for your User models or Schedule / SchedulePeriod models there are a few considerations to note.
Since each UUID implementation approach is different, some of these may or may not benefit you. As always, your implementation may vary.
We use "uuid" in the examples below. Adapt for ULID or GUID as needed.
If you want all the schedule objects to have a UUID instead of an integer, you will need to extend the default Zap\Models\Schedule and Zap\Models\SchedulePeriod models into your own namespace in order to set some specific properties.
Create new models, which extend the Zap\Models\Schedule and Zap\Models\SchedulePeriod models of this package, and add Laravel's HasUuids trait (available since Laravel 9):
php artisan make:model Schedule
php artisan make:model SchedulePeriod<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Zap\Models\Schedule as Model;
class Schedule extends Model
{
use HasUuids;
}<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Zap\Models\SchedulePeriod as Model;
class SchedulePeriod extends Model
{
use HasUuids;
}Add HasUuids trait to schedulable Eloquent model:
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Zap\Models\Concerns\HasSchedules;
class Doctor extends Model
{
use HasSchedules, HasUuids;
}Update config/zap.php:
// config/zap.php
'models' => [
- 'schedule' => \Zap\Models\Schedule::class,
+ 'schedule' => \App\Models\Schedule::class,
- 'schedule_period' => \Zap\Models\SchedulePeriod::class,
+ 'schedule_period' => \App\Models\SchedulePeriod::class,
],You will need to update the create_schedules_table and create_schedule_periods_table migration after creating it with php artisan vendor:publish. After making your edits, be sure to run the migration.
// database/migrations/**_create_schedules_table.php
- $table->id();
+ $table->uuid('id')->primary();
- $table->morphs('schedulable');
+ $table->uuidMorphs('schedulable');
// database/migrations/**_create_schedule_periods_table.php
- $table->id();
+ $table->uuid('id')->primary();
- $table->foreignId('schedule_id')->constrained()->cascadeOnDelete();
+ $table->foreignUuid('schedule_id')->constrained()->cascadeOnDelete();We welcome contributions! Follow PSR-12 coding standards and include tests.
git clone https://github.com/ludoguenet/laravel-zap.git
cd laravel-zap
composer install
composer pestOpen-source software licensed under the MIT License.
Report vulnerabilities to [email protected] (please don't use the issue tracker).
Made with π by Ludovic GuΓ©net for the Laravel community
