Booking systems are among the most requested features for service businesses. From medical appointments to salon bookings, Laravel provides the perfect foundation. At ZIRA Software, we've built booking platforms handling thousands of daily appointments.
Core Data Model
// database/migrations/create_booking_tables.php
Schema::create('services', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->integer('duration'); // minutes
$table->decimal('price', 8, 2);
$table->boolean('active')->default(true);
$table->timestamps();
});
Schema::create('staff', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->unsigned();
$table->string('name');
$table->text('bio')->nullable();
$table->timestamps();
});
Schema::create('availability', function (Blueprint $table) {
$table->increments('id');
$table->integer('staff_id')->unsigned();
$table->tinyInteger('day_of_week'); // 0-6
$table->time('start_time');
$table->time('end_time');
$table->timestamps();
});
Schema::create('bookings', function (Blueprint $table) {
$table->increments('id');
$table->integer('customer_id')->unsigned();
$table->integer('staff_id')->unsigned();
$table->integer('service_id')->unsigned();
$table->dateTime('starts_at');
$table->dateTime('ends_at');
$table->enum('status', ['pending', 'confirmed', 'cancelled', 'completed']);
$table->text('notes')->nullable();
$table->timestamps();
});
Availability Service
// app/Services/AvailabilityService.php
class AvailabilityService
{
public function getAvailableSlots(Staff $staff, Service $service, Carbon $date)
{
$dayOfWeek = $date->dayOfWeek;
// Get staff schedule for this day
$schedule = $staff->availability()
->where('day_of_week', $dayOfWeek)
->first();
if (!$schedule) {
return collect();
}
// Get existing bookings
$bookings = $staff->bookings()
->whereDate('starts_at', $date)
->where('status', '!=', 'cancelled')
->get();
// Generate time slots
$slots = collect();
$current = $date->copy()->setTimeFromTimeString($schedule->start_time);
$end = $date->copy()->setTimeFromTimeString($schedule->end_time);
while ($current->copy()->addMinutes($service->duration) <= $end) {
$slotEnd = $current->copy()->addMinutes($service->duration);
// Check if slot conflicts with existing bookings
$conflict = $bookings->first(function ($booking) use ($current, $slotEnd) {
return $current < $booking->ends_at && $slotEnd > $booking->starts_at;
});
if (!$conflict) {
$slots->push([
'start' => $current->copy(),
'end' => $slotEnd,
]);
}
$current->addMinutes(15); // 15-minute intervals
}
return $slots;
}
}
Booking Controller
// app/Http/Controllers/BookingController.php
class BookingController extends Controller
{
public function __construct(
protected AvailabilityService $availability
) {}
public function availableSlots(Request $request)
{
$request->validate([
'staff_id' => 'required|exists:staff,id',
'service_id' => 'required|exists:services,id',
'date' => 'required|date|after:today',
]);
$staff = Staff::findOrFail($request->staff_id);
$service = Service::findOrFail($request->service_id);
$date = Carbon::parse($request->date);
$slots = $this->availability->getAvailableSlots($staff, $service, $date);
return response()->json($slots);
}
public function store(BookingRequest $request)
{
$service = Service::findOrFail($request->service_id);
$booking = DB::transaction(function () use ($request, $service) {
// Double-check availability
$exists = Booking::where('staff_id', $request->staff_id)
->where('starts_at', '<', $request->ends_at)
->where('ends_at', '>', $request->starts_at)
->where('status', '!=', 'cancelled')
->exists();
if ($exists) {
throw new SlotNotAvailableException();
}
return Booking::create([
'customer_id' => auth()->id(),
'staff_id' => $request->staff_id,
'service_id' => $service->id,
'starts_at' => $request->starts_at,
'ends_at' => Carbon::parse($request->starts_at)
->addMinutes($service->duration),
'status' => 'pending',
]);
});
event(new BookingCreated($booking));
return response()->json($booking, 201);
}
}
Calendar View
// Generate month calendar data
public function calendar(Request $request)
{
$month = Carbon::parse($request->month ?? now());
$staff = Staff::findOrFail($request->staff_id);
$bookings = $staff->bookings()
->whereBetween('starts_at', [
$month->copy()->startOfMonth(),
$month->copy()->endOfMonth(),
])
->get()
->groupBy(fn($b) => $b->starts_at->format('Y-m-d'));
$days = collect();
$date = $month->copy()->startOfMonth();
while ($date->month === $month->month) {
$days->push([
'date' => $date->format('Y-m-d'),
'bookings' => $bookings->get($date->format('Y-m-d'), collect()),
'available' => $this->hasAvailability($staff, $date),
]);
$date->addDay();
}
return response()->json($days);
}
Conclusion
Building booking systems requires careful handling of time slots, availability, and conflict prevention. Laravel's Eloquent and transaction support make implementing reliable booking platforms straightforward.
Need a booking system? Contact ZIRA Software for custom development.