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

The facade is the shortest route and the recommended default, but it is one of three equivalent entry points — pick whichever fits the code you are writing:

  • The Appointments facade — static calls, the least code.
  • The manager — RoundlyConsulting\Appointments\AppointmentManager, the facade root. Inject it through the constructor for the identical API as an explicit dependency, with no static calls.
  • Actions — single-purpose classes with execute(), for composing into your own actions, queued jobs and commands.

Injecting the manager

use RoundlyConsulting\Appointments\AppointmentManager;

final class BookingController
{
    public function __construct(private AppointmentManager $appointments) {}

    public function store(): void
    {
        $appointment = $this->appointments->schedule('Consultation')->startingAt(now()->addDay())->create();
        $this->appointments->for($appointment)->participants()->add(auth()->user());
    }
}

Appointments::fake() swaps the fake in behind the facade and in the container, so an injected AppointmentManager is faked too. The manager is deliberately not final so the fake can extend it.

Running an action

Resolve an action from the container and call execute() — from a queued job, a command or your own action:

use RoundlyConsulting\Appointments\Actions\AttachParticipantAction;
use RoundlyConsulting\Appointments\Actions\CreateAppointmentAction;
use RoundlyConsulting\Appointments\Actions\DetachParticipantAction;
use RoundlyConsulting\Appointments\Actions\RescheduleAppointmentAction;
use RoundlyConsulting\Appointments\Actions\ScheduleRecurringAppointmentAction;
use RoundlyConsulting\Appointments\Actions\TransitionAppointmentAction;
use RoundlyConsulting\Appointments\DataTransferObjects\ParticipantData;
use RoundlyConsulting\Appointments\Enums\Status;

$appointment = app(CreateAppointmentAction::class)->execute($appointmentData);
app(AttachParticipantAction::class)->execute($appointment, new ParticipantData($user), preventConflicts: true);
app(DetachParticipantAction::class)->execute($appointment, $user);
app(RescheduleAppointmentAction::class)->execute($appointment, $newStart, durationMinutes: 45);
app(TransitionAppointmentAction::class)->execute($appointment, Status::Confirmed);
app(ScheduleRecurringAppointmentAction::class)->execute($appointmentData, $rule);

Actions called directly bypass Appointments::fake(); the facade, an injected manager and the model shortcuts don’t.

Facade method → action

Facade methodAction
create() · schedule()->…->create()CreateAppointmentAction
createRecurring() · schedule()->…->createRecurring()ScheduleRecurringAppointmentAction (CreateAppointmentAction when there is no rule)
for($a)->reschedule()RescheduleAppointmentAction
for($a)->transition() · confirm() · cancel() · complete() · decline() · markNoShow()TransitionAppointmentAction
for($a)->participants()->add()AttachParticipantAction
for($a)->participants()->remove()DetachParticipantAction
conflicts() · isAvailable() · ics() · occurrences()— read-only, no action
  • CreateAppointmentAction — rejects duplicate participants, then in one transaction locks the participants and checks conflicts (when prevented), stores the appointment in UTC with its timezone, attaches participants and contacts, opens the approval request and returns the appointment with participants loaded.
  • ScheduleRecurringAppointmentAction — expands a rule in the appointment’s timezone and creates every occurrence in one transaction under a shared recurrence_group.
  • RescheduleAppointmentAction — moves the start, keeps the duration unless you pass one, and fires AppointmentRescheduled; with conflicts prevented it locks the appointment and its participants first.
  • TransitionAppointmentAction — validates and applies a status change and fires AppointmentStatusChanged.
  • AttachParticipantAction — adds one participant behind the duplicate guard and the optional conflict guard; a participant removed earlier is restored rather than inserted again.
  • DetachParticipantAction — soft-deletes a participant by model or by one of the appointment’s own rows, and throws ParticipantNotFoundException for anything else.

The manager’s public rescheduleFor(), transitionFor(), addParticipantFor() and removeParticipantFor() are @internal — they are the bodies of the handle methods and the one place the fake overrides. Call the for() handle instead.

create(), createRecurring() and every action take the typed DTOs described in Typed DTOs.

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.