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

The Events facade

Everything goes through one facade, RoundlyConsulting\Events\Facades\Events — sugar over the EventsManager singleton, with no global alias. A venue, a category, an event with two ticket types, an RSVP, a door scan and a near-me search:

use RoundlyConsulting\Events\Facades\Events;
use RoundlyConsulting\Money\Money;

// A reusable venue: its timezone is how every wall-clock time below is read
$venue = Events::venues()->make('Blue Note')
    ->address(street: 'Hlavná 1', city: 'Bratislava', postalCode: '811 01', country: 'SK')
    ->coordinates(48.1439, 17.1097)
    ->timezone('Europe/Bratislava')
    ->create();

Events::categories()->make(['en' => 'Jazz', 'sk' => 'Jazz'])->create();   // categories are curated

$event = Events::make(['en' => 'Jazz Night', 'sk' => 'Jazzový večer'])
    ->description(['en' => 'A live quartet, then an open jam session.'])
    ->organizedBy($user)
    ->at($venue)
    ->startsAt('2026-11-14 20:00')                 // 20:00 in Bratislava, stored as UTC
    ->duration('3 hours')                          // or 'PT3H', a CarbonInterval, or ->endsAt(...)
    ->capacity(120)
    ->category('jazz')
    ->labels(['Live', 'Late night'])
    ->poster($request->file('poster'))
    ->tier('Free entry', key: 'free')
    ->tier('Supporter', price: Money::ofMajor('15', 'EUR'), capacity: 30)   // key "supporter"
    ->publish();

// RSVP: a seat while there is one, the waitlist once it is full
$rsvp = Events::for($event)->rsvp($attendee)->going(tier: 'free');
$rsvp->isGoing();                                   // or isWaitlisted(), isHeld()

// At the door: scan the attendee's QR code, as the signed-in staff member
$result = Events::for($event)->by($doorStaff)->checkIn($scannedCode);   // organizers and co-hosts only
$result->firstCheckIn;                              // false if the code was already used

// Discovery: upcoming published events (cancelled ones flagged by phase) within 25 km, nearest first
Events::browse()->upcoming()->near($here, km: 25)->orderByDistance($here)->paginate();

Manager methods

MethodReturnsDoes
make(array|string $title)EventBuilderFluent builder; a locale map or a string in the current locale.
create(CreateEventData $data)EventA draft — the builder’s code path.
find(string $uuid)?EventLive events only; a malformed uuid is null without a query; no visibility rule.
findOrFail(string $uuid)EventEventNotFoundException otherwise.
findBySlug(string $slug, ?Model $viewer = null)?EventAlong the locale chain; null for unlisted, private and draft events $viewer may not see without the link.
for(Event|string $event)EventHandleBy model or uuid; an unknown or trashed uuid throws EventNotFoundException.
browse(?Model $viewer = null)Builder<Event>Visible to $viewer, listed (public) and announced; eager-loads venue, organizer, tiers, media and tags.
visibleTo(?Model $viewer)Builder<Event>The policy’s view as a query — drafts and private events of managed events included.
venues()Accessors\VenuesVenues — below.
categories() / labels()Tags\Handles\TypeTagstags-for-laravel’s handle for events:category / events:label, unchanged.
rsvps()Accessors\RsvpsPayments, holds, codes and QR — below.
invitations()Accessors\InvitationsAnswering from the invitation link — below.
checkIn(string $code, Model $by, ?int $guests = null, bool $ignoreWindow = false)CheckInResultThe unscoped door scan — the code finds its event.
ics(iterable $events, ?Model $viewer = null)IcsCalendarA calendar file; skips events $viewer may not see.
feeds()Calendar\Feedspublic(EventFeedQuery) and for($attendee, includePrivate:).
jsonLd(Event $event)?arrayschema.org data, or null.
sweep(?SweepOptions $options = null)SweepResultWhat events:sweep runs.
repair(?RepairOptions $options = null)RepairReportWhat events:repair runs.
Events::fake()EventsFakeFacade only — the recording, still-performing fake (see Testing).

forgetAttendee(), container() and perform() are public for the package’s own wiring and marked @internal — don’t call them from host code.

The for($event) handle

Events::for($event) returns an immutable EventHandle. by($actor) checks the event policy before every following call, asSystem() runs as trusted code and silently() writes without notices — each returns a new handle. Without by(), a call is trusted PHP (a job, a seeder, a command) and the policy is not asked. A mutator returns the handle so calls chain; a creator or a query returns its result. The bound event is updated in place, so event() returns it current:

$handle = Events::for($event)->by($organizer);    // checks the policy before every call

$handle->tiers()->add('Crew', hidden: true, key: 'crew');
$handle->hosts()->add($partnerTeam);
$handle->publish()->event();                      // a mutator returns the handle; event() is the bound, current model

Events::for($event)->asSystem()->postpone('Storm warning');          // trusted code: no actor, no policy
Events::for($event)->silently()->reschedule('2026-12-05 20:00');     // domain events still fire, no notices
MethodReturnsAbility (with by())
by(?Model $actor)EventHandle—
asSystem()EventHandle— (no actor, no policy)
silently()EventHandle— (domain events still fire, no notices)
event()Event—
update(UpdateEventData $data)staticupdate
edit()EventEditorupdate, on save()
poster(UploadedFile|string|null $file = null, ?string $draft = null)staticupdate
categorize(Tag|int|string|null $category)staticupdate
labels()EventLabels—
tiers()EventTiers—
rsvp(Model $attendee)PendingRsvp—
attendees()EventAttendees—
waitlist()EventWaitlist—
checkIn(string $code, ?int $guests = null, bool $ignoreWindow = false)CheckInResultcheckIn
hosts()EventHosts—
invitations()EventInvitations—
transferTo(Model&Organizer $organizer, bool $keepAsHost = false)statictransfer
counts()EventCountsviewCounts
publish() · unpublish() · complete()staticpublish · unpublish · complete
postpone(?string $reason = null) · cancel(?string $reason = null)staticpostpone · cancel
reschedule(CarbonInterface|string $startsAt, CarbonInterface|string|null $endsAt = null, ?string $reason = null)staticreschedule
moveOnline(string $url, bool $keepVenue = false)staticmoveOnline
transition(string $name, ?string $reason = null, array $payload = [])TransitionResulttransition
can(string $transition) · allowedTransitions()bool · list<string>false / [] when the actor lacks transition
phase() · status()EventPhase · EventState—
delete(bool $force = false) · restore()staticdelete · restore
ics(?Model $viewer = null)IcsCalendarview
jsonLd()?array—
$handle = Events::for($event);

$handle->phase();                // EventPhase — what every package rule reads
$handle->status();               // the lifecycle state (EventStatus with the shipped lifecycle)
$handle->can('cancel');          // false when the actor lacks the transition ability
$handle->allowedTransitions();   // list<string> of transition names allowed now
$handle->counts();               // EventCounts {going, interested, waitlisted, seatsLeft}
$handle->ics($viewer);           // IcsCalendar
$handle->jsonLd();               // schema.org data, or null

manager(), actor(), isSystem(), isSilent(), authorize() and context() are public for the sub-handles and marked @internal.

Ticket types: ->tiers()

A ticket type of another event is never found (TierNotFoundException):

MethodReturnsAbility
make(array|string $name)TierBuildermanageTiers
create(TierData $data)AdmissionTiermanageTiers
add(array|string $name, ?Money $price = null, ?int $capacity = null, bool $hidden = false, ?string $key = null)AdmissionTiermanageTiers
update(AdmissionTier|int|string $tier, UpdateTierData $data)AdmissionTiermanageTiers
remove($tier) · restore($tier)EventTiers · AdmissionTiermanageTiers
reorder(array $order)EventTiersmanageTiers
all()Collection<AdmissionTier>— (hidden ones only for manageTiers)
available()Collection<AdmissionTier>— (no policy)

Answering: ->rsvp($attendee)

The attendee answering for themselves needs rsvp; anyone answering for someone else needs manageAttendees:

MethodReturnsNotes
by(?Model $actor)PendingRsvpDefaults to the handle’s actor.
going(AdmissionTier|int|string|null $tier = null, int $guests = 0, ?string $note = null)RsvpGoing, held or waitlisted.
interested() · notGoing()Rsvp—

The guest list: ->attendees()

Every method takes the attendee model or its Rsvp; an RSVP of another event is refused:

MethodReturnsAbility
add(Model $attendee, $tier = null, int $guests = 0, bool $overCapacity = false)RsvpmanageAttendees
remove(Rsvp|Model $who)EventAttendeesmanageAttendees
changeTier(Rsvp|Model $who, AdmissionTier|int|string $tier)RsvpmanageAttendees
changeGuests(Rsvp|Model $who, int $guests)RsvpmanageAttendees
confirm(Rsvp|Model $who, ?string $reference = null) · release(Rsvp|Model $who)RsvpmanageAttendees
checkIn(Rsvp|Model $who, ?int $guests = null) · undoCheckIn(Rsvp|Model $who)CheckInResult · EventAttendeescheckIn
find(Model $attendee)?Rsvpthe actor’s own, else manageAttendees
going() · interested() · checkedIn()Collection<Rsvp>viewAttendees · viewInterested · checkIn

Waitlist, co-hosts, invitations and labels

MethodReturnsAbility
waitlist()->all() · position(Model $attendee) · promote(?int $limit = null)Collection<Rsvp> · ?int · Collection<Rsvp>viewWaitlist · viewWaitlist · manageAttendees
hosts()->add(Model&Organizer $host) · remove(Model $host)EventHostsmanageHosts
hosts()->all() · has(Model $host)Collection<EventHost> · boolview
invitations()->send(Model $invitee, ?string $message = null)EventInvitationinvite
invitations()->revoke(Model $invitee) · all() · pending()EventInvitations · Collection<EventInvitation>invite
labels()->attach($labels) · detach($labels) · sync(iterable $labels)EventLabelsupdate
labels()->all()Collection<Tag>—

Events::venues()

Immutable: by() returns a new accessor whose writes check the VenuePolicy:

MethodReturnsPolicy (with an actor)
by(?Model $actor)VenuesAttributes create, update, delete, restore, geocode and the builder to $actor.
make(array|string $name)VenueBuilderCarries the accessor’s actor.
create(VenueData $data)Venuecreate (+ manageShared without an owner, or the owner must allow the actor); a DTO naming another actor than by() is refused.
update(Venue $venue, UpdateVenueData $data)Venueupdate
delete(Venue $venue) · restore(Venue $venue)void · Venuedelete — refused (venue_in_use) while an unfinished event uses it · restore
geocode(Venue $venue, bool $force = false)Venuegeocode; only missing coordinates unless $force.
find(int $id) · ownedBy(Model $owner) · near(Coordinates $point, float $km)?Venue · Builder<Venue>Queries, no policy.

Events::rsvps()

MethodReturnsNotes
by(?Model $actor)Rsvpsconfirm, release and rotateCode then check manageAttendees; requestPayment lets the RSVP’s own attendee through too.
confirmPayment(string $reference)RsvpYour payment webhook: held → going, idempotent; no policy.
confirm(Rsvp $rsvp, ?string $reference = null)RsvpA held RSVP, by hand.
release(Rsvp $rsvp)RsvpGive a hold’s seats back now.
rotateCode(Rsvp $rsvp)RsvpA new check-in code; the old one stops working.
qr(Rsvp $rsvp, ?QrOptions $options = null)?Qr\ValueObjects\SvgThe code as an inline SVG; null without a code.
findByCode(string $code)?RsvpThe RSVP behind a code, or scanner input containing one.
requestPayment(Rsvp $rsvp)?PaymentRequestAsk the collector (again) for a held RSVP’s checkout.

Events::invitations()

MethodReturnsNotes
asSystem()InvitationsTrusted code: answers for any invitee.
accept(string $token, RsvpResponse $as = RsvpResponse::Going, ?Model $by = null)RsvpRSVPs the invitee (going or interested).
decline(string $token, ?Model $by = null)EventInvitation—
find(string $token)?EventInvitationThe live invitation, or null.

Pass the request’s user as $by: an account invitee answers only signed in as themselves.

The event builder

Events::make($title) returns an EventBuilder. Setters store what you give and resolve the schedule on create(), once the timezone is known, so their order never changes the instant; they validate eagerly where they can:

SetterNotes
description(array|string $description)—
slug(array $slug)Manual per-locale slugs; a taken one throws invalid_event (never suffixed).
organizedBy(Model&Organizer $organizer)Required.
coHostedBy(Model&Organizer ...$hosts)Adds to the list.
at(Venue|int|null $venue)A model or its id.
online(?string $url)http(s), ≤ 2048 characters; stored encrypted.
attendanceMode(AttendanceMode $mode)Must agree with the venue and the link.
timezone(string $timezone)An IANA name.
startsAt() · endsAt() · duration()startsAt clears an all-day range, endsAt a duration, duration an end; duration takes 'PT3H', '3 hours' or a CarbonInterval.
allDay(string $from, ?string $through = null)Y-m-d local days, $through inclusive.
doorsOpenAt(CarbonInterface|string $doorsOpenAt)Within the 12 hours before the start.
visibility(Visibility $visibility) · unlisted() · private()—
capacity(?int $capacity) · maxGuests(int $guests)null capacity = unlimited.
waitlist(bool $on = true, bool $autoPromote = true)—
rsvpWindow($opensAt = null, $closesAt = null)—
guestList(GuestList $guestList) · showCounts(bool $show = true)—
poster(UploadedFile|string|null $file = null, ?string $draft = null)—
category(Tag|int|string|null $category) · labels(iterable $labels)—
tier(array|string $name, ?Money $price = null, ?int $capacity = null, bool $hidden = false, ?string $key = null)Repeatable.
reminders(?array $offsets)≤ 5 ISO-8601 offsets, PT5M–P31D.
meta(array $meta)Private: never rendered by the API.
by(?Model $actor)Checks create and that the organizer allows the actor.
toData() · create() · publish()CreateEventData · a draft · create and publish in one transaction.

Events::for($event)->edit() returns an EventEditor with the same setters for an existing event, plus title(): only the fields you touch change, save() runs the handle’s update() and toData() gives the UpdateEventData. The slug, category, labels, ticket types, poster and co-hosts have their own handle methods. The ticket type builder and the venue builder are covered on their own pages.

Model traits

AttendsEvents memberNotes
rsvp(Event $event): PendingRsvp$user->rsvp($event)->going() — through the manager, so the fake records it.
isGoingTo(Event $event): bool · rsvpFor(Event $event): ?Rsvp—
eventRsvps(): MorphManyEvery RSVP of the model.
attendingEvents() · interestingEvents(): MorphToManyEvents it is going to / interested in.
eventFeed(bool $includePrivate = false): IcsCalendarIts personal calendar.
deletion hookA force delete (or a delete without soft deletes) releases its RSVPs: seats back, waitlist promoted, rows removed.

OrganizesEvents implements the Organizer contract with defaults and adds organizedEvents(), coHostedEvents() and organizeEvent($title) — see Organizers, co-hosts and invitations. Both traits go through the manager, so Events::fake() records what they do.

The PHP API trusts its caller

The facade always enforces the domain rules — seats, windows, phases, invitations, party limits. The policy is asked only when you name an actor with by(). In a controller, always pass the signed-in user: a handle without by() copied into a door controller would let any signed-in user check anyone in.

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.