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 • resource scheduling • shift management • calendar applications
Requirements: PHP ≤8.5 • Laravel ≤12.0
composer require laraveljutsu/zap
php artisan vendor:publish --tag=zap-migrations
php artisan migrateAdd the trait to your schedulable models:
use Zap\Models\Concerns\HasSchedules;
class Doctor extends Model
{
use HasSchedules;
}| 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 |
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);// Daily
$schedule->daily()->from('2025-01-01')->to('2025-12-31');
// Weekly (specific days)
$schedule->weekly(['monday', 'wednesday', 'friday'])->forYear(2025);
// Monthly
$schedule->monthly(['day_of_month' => 1])->forYear(2025);$schedule->from('2025-01-15'); // Single date
$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 shortcut// 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 if there is at least one bookable slot on the day
$isBookable = $doctor->isBookableAt('2025-01-15', 60);
// 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();
isAvailableAt()is deprecated in favor ofisBookableAt()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
Zap::for($room)->named('Conference Room A')->availability()
->forYear(2025)->weekly(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'])
->addPeriod('08:00', '18:00')->save();
// Book meeting
Zap::for($room)->named('Board Meeting')->appointment()
->from('2025-03-15')->addPeriod('09:00', '11:00')
->withMetadata(['organizer' => 'john@company.com'])->save();// Regular schedule
Zap::for($employee)->named('Regular Shift')->availability()
->forYear(2025)->weekly(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'])
->addPeriod('09:00', '17:00')->save();
// Vacation
Zap::for($employee)->named('Vacation Leave')->blocked()
->between('2025-06-01', '2025-06-15')
->addPeriod('00:00', '23:59')->save();Publish and customize:
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'],
],
],Zap::for($user)->named('Custom Event')->custom()
->from('2025-01-15')->addPeriod('15:00', '16:00')
->noOverlap() // Explicitly prevent overlaps
->save();->withMetadata([
'patient_id' => 1,
'type' => 'consultation',
'notes' => 'Follow-up required'
])We welcome contributions! Follow PSR-12 coding standards and include tests.
git clone https://github.com/laraveljutsu/zap.git
cd zap
composer install
vendor/bin/pestOpen-source software licensed under the MIT License.
Report vulnerabilities to ludo@epekta.com (please don't use the issue tracker).
Made with 💛 by Ludovic Guénet for the Laravel community
