NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Appointments for Laravel

The Appointments facade

Everything goes through RoundlyConsulting\Appointments\Facades\Appointments. Its root is AppointmentManager, a container singleton you can also inject (see DI and actions). schedule(), create() and createRecurring() write new bookings, for($appointment) scopes every change to one booking, and the read methods answer availability and export questions without writing anything:

use Carbon\CarbonImmutable;
use RoundlyConsulting\Appointments\DataTransferObjects\RecurrenceData;
use RoundlyConsulting\Appointments\Enums\Frequency;
use RoundlyConsulting\Appointments\Enums\Status;
use RoundlyConsulting\Appointments\Facades\Appointments;

// Create
$appointment = Appointments::schedule('Consultation')->startingAt('2026-07-01 09:00')->lasting(45)->create();
$appointment = Appointments::create($appointmentData);             // from an AppointmentData
$series = Appointments::createRecurring($appointmentData, $rule);  // Collection<int, Appointment>

// One booking
$booking = Appointments::for($appointment);                        // AppointmentHandle
$booking->reschedule(CarbonImmutable::parse('2026-07-02 10:00'));  // keeps the duration
$booking->confirm();                                               // or cancel(), complete(), decline(), markNoShow()
$booking->transition(Status::Completed);
$booking->participants()->add($user);                              // AppointmentParticipants
$booking->ics();                                                   // one-event calendar

// Reads — nothing is written
Appointments::isAvailable($doctor, $start, $end);                  // bool
Appointments::conflicts($doctor, $start, $end);                    // Collection<int, Appointment>
Appointments::ics($user->appointments()->upcoming()->get());       // one VCALENDAR, many VEVENTs
Appointments::occurrences($start, new RecurrenceData(Frequency::Weekly, count: 4)); // list<CarbonImmutable>

// Tests
$fake = Appointments::fake();                                      // AppointmentsFake

Method reference

MethodReturns
schedule(string $name)AppointmentBuilder — chain the builder methods, then create() or createRecurring().
create(AppointmentData $data)Appointment.
createRecurring(AppointmentData $data, ?RecurrenceData $rule = null)Collection<int, Appointment> — the rule falls back to the DTO’s own recurrence; with none, a single-item collection.
for(Appointment $appointment)AppointmentHandle — reschedule(), transition(), confirm(), cancel(), complete(), decline(), markNoShow(), participants(), ics().
for($appointment)->participants()AppointmentParticipants — add(), remove(), has(), all().
conflicts(Model $participant, $start, $end, ?Appointment $ignore = null)Collection<int, Appointment> — the participant’s overlapping bookings.
isAvailable(Model $participant, $start, $end, ?Appointment $ignore = null)bool — free for the whole window.
ics(iterable $appointments)string — one VCALENDAR with a VEVENT per appointment.
occurrences(CarbonInterface $start, RecurrenceData $rule)list<CarbonImmutable> — a rule’s start times, nothing written.
fake()AppointmentsFake — see Testing.

The for() handle

Appointments::for($appointment) returns an AppointmentHandle bound to that booking. reschedule() keeps the duration unless you pass one and fires AppointmentRescheduled; transition() and the confirm()-style shortcuts run the guarded status machine; participants() returns an AppointmentParticipants scoped to the booking, with add(), remove(), has() and all(); ics() renders the booking as a one-event calendar.

Model shortcuts

The Appointment model’s confirm(), cancel(), complete(), decline(), markNoShow(), transitionTo() and toIcs() are sugar over the same handle — they call for($this) on the manager, so a host override and Appointments::fake() see them too.

  • Every write — facade, injected manager, builder, handle or model shortcut — funnels through one manager method that resolves its action from the container, so an action you rebind applies everywhere.
  • Every booking write runs in one transaction and the package events fire only once it commits: a refused or failed booking leaves no row and fires no event.
  • The builder, the handle and the participants accessor are built for you — start from schedule() and for(), never new.

Show 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 crypto

By 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.