---
title: "The Events facade — Events for Laravel | Roundly"
description: "Tour the Events facade — make(), the for($event) handle and its sub-handles, venues, RSVPs, invitations, the builders and the model traits."
url: https://roundly-consulting.com/open-source/docs/events-for-laravel/facade
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Events for Laravel](https://roundly-consulting.com/open-source/docs/events-for-laravel.md)

# 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:

```php
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

| Method | Returns | Does |
| --- | --- | --- |
| `make(array\|string $title)` | `EventBuilder` | Fluent builder; a locale map or a string in the current locale. |
| `create(CreateEventData $data)` | `Event` | A draft — the builder’s code path. |
| `find(string $uuid)` | `?Event` | Live events only; a malformed uuid is null without a query; no visibility rule. |
| `findOrFail(string $uuid)` | `Event` | EventNotFoundException otherwise. |
| `findBySlug(string $slug, ?Model $viewer = null)` | `?Event` | Along the locale chain; null for unlisted, private and draft events $viewer may not see without the link. |
| `for(Event\|string $event)` | `EventHandle` | By 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\Venues` | Venues — below. |
| `categories() / labels()` | `Tags\Handles\TypeTags` | tags-for-laravel’s handle for events:category / events:label, unchanged. |
| `rsvps()` | `Accessors\Rsvps` | Payments, holds, codes and QR — below. |
| `invitations()` | `Accessors\Invitations` | Answering from the invitation link — below. |
| `checkIn(string $code, Model $by, ?int $guests = null, bool $ignoreWindow = false)` | `CheckInResult` | The unscoped door scan — the code finds its event. |
| `ics(iterable $events, ?Model $viewer = null)` | `IcsCalendar` | A calendar file; skips events $viewer may not see. |
| `feeds()` | `Calendar\Feeds` | public(EventFeedQuery) and for($attendee, includePrivate:). |
| `jsonLd(Event $event)` | `?array` | schema.org data, or null. |
| `sweep(?SweepOptions $options = null)` | `SweepResult` | What events:sweep runs. |
| `repair(?RepairOptions $options = null)` | `RepairReport` | What events:repair runs. |
| `Events::fake()` | `EventsFake` | Facade 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:

```php
$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
```

| Method | Returns | Ability (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)` | `static` | `update` |
| `edit()` | `EventEditor` | update, on save() |
| `poster(UploadedFile\|string\|null $file = null, ?string $draft = null)` | `static` | `update` |
| `categorize(Tag\|int\|string\|null $category)` | `static` | `update` |
| `labels()` | `EventLabels` | — |
| `tiers()` | `EventTiers` | — |
| `rsvp(Model $attendee)` | `PendingRsvp` | — |
| `attendees()` | `EventAttendees` | — |
| `waitlist()` | `EventWaitlist` | — |
| `checkIn(string $code, ?int $guests = null, bool $ignoreWindow = false)` | `CheckInResult` | `checkIn` |
| `hosts()` | `EventHosts` | — |
| `invitations()` | `EventInvitations` | — |
| `transferTo(Model&Organizer $organizer, bool $keepAsHost = false)` | `static` | `transfer` |
| `counts()` | `EventCounts` | `viewCounts` |
| `publish() · unpublish() · complete()` | `static` | `publish · unpublish · complete` |
| `postpone(?string $reason = null) · cancel(?string $reason = null)` | `static` | `postpone · cancel` |
| `reschedule(CarbonInterface\|string $startsAt, CarbonInterface\|string\|null $endsAt = null, ?string $reason = null)` | `static` | `reschedule` |
| `moveOnline(string $url, bool $keepVenue = false)` | `static` | `moveOnline` |
| `transition(string $name, ?string $reason = null, array $payload = [])` | `TransitionResult` | `transition` |
| `can(string $transition) · allowedTransitions()` | `bool · list<string>` | false / \[\] when the actor lacks transition |
| `phase() · status()` | `EventPhase · EventState` | — |
| `delete(bool $force = false) · restore()` | `static` | `delete · restore` |
| `ics(?Model $viewer = null)` | `IcsCalendar` | `view` |
| `jsonLd()` | `?array` | — |

```php
$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):

| Method | Returns | Ability |
| --- | --- | --- |
| `make(array\|string $name)` | `TierBuilder` | `manageTiers` |
| `create(TierData $data)` | `AdmissionTier` | `manageTiers` |
| `add(array\|string $name, ?Money $price = null, ?int $capacity = null, bool $hidden = false, ?string $key = null)` | `AdmissionTier` | `manageTiers` |
| `update(AdmissionTier\|int\|string $tier, UpdateTierData $data)` | `AdmissionTier` | `manageTiers` |
| `remove($tier) · restore($tier)` | `EventTiers · AdmissionTier` | `manageTiers` |
| `reorder(array $order)` | `EventTiers` | `manageTiers` |
| `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:

| Method | Returns | Notes |
| --- | --- | --- |
| `by(?Model $actor)` | `PendingRsvp` | Defaults to the handle’s actor. |
| `going(AdmissionTier\|int\|string\|null $tier = null, int $guests = 0, ?string $note = null)` | `Rsvp` | Going, 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:

| Method | Returns | Ability |
| --- | --- | --- |
| `add(Model $attendee, $tier = null, int $guests = 0, bool $overCapacity = false)` | `Rsvp` | `manageAttendees` |
| `remove(Rsvp\|Model $who)` | `EventAttendees` | `manageAttendees` |
| `changeTier(Rsvp\|Model $who, AdmissionTier\|int\|string $tier)` | `Rsvp` | `manageAttendees` |
| `changeGuests(Rsvp\|Model $who, int $guests)` | `Rsvp` | `manageAttendees` |
| `confirm(Rsvp\|Model $who, ?string $reference = null) · release(Rsvp\|Model $who)` | `Rsvp` | `manageAttendees` |
| `checkIn(Rsvp\|Model $who, ?int $guests = null) · undoCheckIn(Rsvp\|Model $who)` | `CheckInResult · EventAttendees` | `checkIn` |
| `find(Model $attendee)` | `?Rsvp` | the actor’s own, else manageAttendees |
| `going() · interested() · checkedIn()` | `Collection<Rsvp>` | `viewAttendees · viewInterested · checkIn` |

## Waitlist, co-hosts, invitations and labels

| Method | Returns | Ability |
| --- | --- | --- |
| `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)` | `EventHosts` | `manageHosts` |
| `hosts()->all() · has(Model $host)` | `Collection<EventHost> · bool` | `view` |
| `invitations()->send(Model $invitee, ?string $message = null)` | `EventInvitation` | `invite` |
| `invitations()->revoke(Model $invitee) · all() · pending()` | `EventInvitations · Collection<EventInvitation>` | `invite` |
| `labels()->attach($labels) · detach($labels) · sync(iterable $labels)` | `EventLabels` | `update` |
| `labels()->all()` | `Collection<Tag>` | — |

## Events::venues()

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

| Method | Returns | Policy (with an actor) |
| --- | --- | --- |
| `by(?Model $actor)` | `Venues` | Attributes create, update, delete, restore, geocode and the builder to $actor. |
| `make(array\|string $name)` | `VenueBuilder` | Carries the accessor’s actor. |
| `create(VenueData $data)` | `Venue` | create (+ 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)` | `Venue` | update |
| `delete(Venue $venue) · restore(Venue $venue)` | `void · Venue` | delete — refused (venue\_in\_use) while an unfinished event uses it · restore |
| `geocode(Venue $venue, bool $force = false)` | `Venue` | geocode; only missing coordinates unless $force. |
| `find(int $id) · ownedBy(Model $owner) · near(Coordinates $point, float $km)` | `?Venue · Builder<Venue>` | Queries, no policy. |

## Events::rsvps()

| Method | Returns | Notes |
| --- | --- | --- |
| `by(?Model $actor)` | `Rsvps` | confirm, release and rotateCode then check manageAttendees; requestPayment lets the RSVP’s own attendee through too. |
| `confirmPayment(string $reference)` | `Rsvp` | Your payment webhook: held → going, idempotent; no policy. |
| `confirm(Rsvp $rsvp, ?string $reference = null)` | `Rsvp` | A held RSVP, by hand. |
| `release(Rsvp $rsvp)` | `Rsvp` | Give a hold’s seats back now. |
| `rotateCode(Rsvp $rsvp)` | `Rsvp` | A new check-in code; the old one stops working. |
| `qr(Rsvp $rsvp, ?QrOptions $options = null)` | `?Qr\ValueObjects\Svg` | The code as an inline SVG; null without a code. |
| `findByCode(string $code)` | `?Rsvp` | The RSVP behind a code, or scanner input containing one. |
| `requestPayment(Rsvp $rsvp)` | `?PaymentRequest` | Ask the collector (again) for a held RSVP’s checkout. |

## Events::invitations()

| Method | Returns | Notes |
| --- | --- | --- |
| `asSystem()` | `Invitations` | Trusted code: answers for any invitee. |
| `accept(string $token, RsvpResponse $as = RsvpResponse::Going, ?Model $by = null)` | `Rsvp` | RSVPs the invitee (going or interested). |
| `decline(string $token, ?Model $by = null)` | `EventInvitation` | — |
| `find(string $token)` | `?EventInvitation` | The 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:

| Setter | Notes |
| --- | --- |
| `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 member | Notes |
| --- | --- |
| `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(): MorphMany` | Every RSVP of the model. |
| `attendingEvents() · interestingEvents(): MorphToMany` | Events it is going to / interested in. |
| `eventFeed(bool $includePrivate = false): IcsCalendar` | Its personal calendar. |
| deletion hook | A 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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

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

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/events-for-laravel.md)
