Participants
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();| Case | Value | Notes |
|---|---|---|
ParticipantRole::Organiser | organiser | The person running the appointment. |
ParticipantRole::Attendee | attendee | Returned by ParticipantRole::default(). |
ParticipantRole::Optional | optional | Invited, 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 CollectionAdding 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 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.