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

Participants aren’t limited to users — any Eloquent model can take part through the polymorphic participants table: a customer, a staff member, a meeting room, a vehicle. Add HasAppointments to each model that should see its appointments:

use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\User as Authenticatable;
use RoundlyConsulting\Appointments\Concerns\HasAppointments;

class User extends Authenticatable
{
    use HasAppointments;
}

class Room extends Model
{
    use HasAppointments;
}

$user->appointments()->upcoming()->ordered()->get();   // appointments the user takes part in
$room->appointments()->between($monday, $friday)->get();
$user->appointmentParticipations;                       // the participant rows (MorphMany)

appointments() returns a query builder rather than a relation, so chain any scope before get(). appointmentParticipations() is the MorphMany of participant rows.

Roles and per-participant meta

Give each participant an optional role and any meta you need — a host flag, party size, a seat number:

use RoundlyConsulting\Appointments\Enums\ParticipantRole;
use RoundlyConsulting\Appointments\Facades\Appointments;

Appointments::schedule('Property viewing')
    ->startingAt('2026-07-03 14:00')
    ->lasting(30)
    ->withParticipant($agent, ParticipantRole::Organiser)
    ->withParticipant($buyer, ParticipantRole::Attendee, ['party_size' => 2])
    ->withParticipant($partner, ParticipantRole::Optional)
    ->withParticipant($flat)            // any Eloquent model — role stays null
    ->create();
CaseValueNotes
ParticipantRole::OrganiserorganiserThe person running the appointment.
ParticipantRole::AttendeeattendeeReturned by ParticipantRole::default().
ParticipantRole::OptionaloptionalInvited, but not required.

Reading participants

$appointment->participants;                    // HasMany<Participant>, loaded after create()

$participant = $appointment->participants->first();
$participant->participant;                     // MorphTo — the underlying model (e.g. a User)
$participant->appointment;                     // BelongsTo<Appointment>
$participant->role;                            // ?ParticipantRole
$participant->meta?->get('party_size');        // meta is cast to a Collection

Adding and removing participants later

Appointments::for($appointment)->participants() manages who takes part in one booking:

use RoundlyConsulting\Appointments\Enums\ParticipantRole;
use RoundlyConsulting\Appointments\Facades\Appointments;

$participants = Appointments::for($appointment)->participants();

$row = $participants->add($interpreter, ParticipantRole::Optional, meta: ['language' => 'sk']);
$participants->add($room, preventConflicts: true);   // refuse if the room is booked then

$participants->has($interpreter);    // true
$participants->all();                // the participant rows (role, meta), models eager-loaded
$participants->remove($interpreter); // by model…
$participants->remove($row);         // …or by one of this appointment's participant rows
  • Adding a model that already takes part throws DuplicateParticipantException.
  • With preventConflicts: true (or prevent_conflicts in config), adding a model that is booked elsewhere in the appointment’s window throws SchedulingConflictException.
  • Removing a model that doesn’t take part — or a participant row that belongs to another appointment — throws ParticipantNotFoundException. Nothing is deleted.
  • Removal soft-deletes the row and fires ParticipantDeleted. Adding the same model again restores that row with the new role and meta (ParticipantUpdated); a first-time add fires ParticipantCreated.

Both models use soft deletes: deleting a participant or an appointment keeps the row with a deleted_at timestamp.

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.