Conflict detection
Turn on double-booking protection for one call with preventConflicts(). When a participant already has an overlapping booking, SchedulingConflictException is thrown before anything is written, and it carries the conflicting appointments:
use RoundlyConsulting\Appointments\Exceptions\SchedulingConflictException;
use RoundlyConsulting\Appointments\Facades\Appointments;
try {
Appointments::schedule('Follow-up')
->startingAt('2026-07-01 09:30')
->lasting(60)
->withParticipant($doctor)
->preventConflicts()
->create();
} catch (SchedulingConflictException $e) {
$e->conflicts; // Collection<int, Appointment> — the bookings this slot overlaps
$e->getMessage(); // The requested time conflicts with 1 existing appointment(s).
}Or guard every create, reschedule and participant add in the app:
// config/appointments.php — guard every create, reschedule and participant add
'prevent_conflicts' => true,What counts as a conflict
- An existing appointment conflicts when it starts before the new end and ends after the new start. Touching ranges — one ends exactly when the next begins — don’t conflict.
- An existing appointment without an ends_at is treated as open-ended.
- Cancelled and declined appointments are ignored; pending, confirmed, completed and no-show ones count.
- On create, every participant passed to the builder or DTO is checked. On reschedule, the appointment’s current participants are checked and the appointment never clashes with itself. On participants()->add(), the added model is checked against the appointment’s window.
- The per-call flag only switches the guard on — with prevent_conflicts set to true, every create, reschedule and participant add is guarded.
- In a recurring series every occurrence is checked, and one conflict rolls back the whole series.
Safe under concurrency
The check holds when two requests book the same person at once. It runs in the transaction that writes, after locking each participant’s row — and, for a reschedule or an added participant, the appointment’s row — so two simultaneous bookings of one person can’t both pass it. Locks are always taken in the same order, so bookings that share participants don’t deadlock. SQLite has no row locks; it serializes writers instead.
Checking availability
Ask the facade directly — for example to grey out busy slots before someone picks one. Both methods read only and apply the same rules. $start and $end may be Carbon instances in any timezone; they are compared as the instants they are:
use RoundlyConsulting\Appointments\Facades\Appointments;
Appointments::isAvailable($doctor, $start, $end); // bool
Appointments::conflicts($doctor, $start, $end); // Collection<int, Appointment>
Appointments::conflicts($doctor, $start, $end, ignore: $appointment); // leave one booking outShow your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.