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

Installation

Require the package, run its installer, then migrate. The service provider is auto-discovered together with the providers of the Roundly packages it builds on. The facade has no global alias — import RoundlyConsulting\Events\Facades\Events:

composer require roundly-consulting/events-for-laravel
php artisan events:install
php artisan migrate

events:install refuses while any configured table already exists — events is a common host table name (analytics, event sourcing) — and names the events.tables.* key to change. Otherwise it publishes events-config and the migrations lifecycle-migrations, tags-migrations, media-migrations and events-migrations (plus sluggable-migrations with slug history on), keeps files you already published, and prints the key types side by side with the next steps.

1. Settle the key types before migrating

The migrations read two key types to pick their column types. With bigint user ids the defaults fit. With UUID or ULID keys, set them before php artisan migrate:

EVENTS_PRIMARY_KEY_TYPE=uuid   # events and every column pointing at one (= lifecycle.key_type, tags.key_type)
EVENTS_KEY_TYPE=uuid           # organizers, attendees, co-hosts, invitees, actors (= your users)
LIFECYCLE_KEY_TYPE=uuid
LIFECYCLE_ACTOR_KEY_TYPE=uuid
TAGS_KEY_TYPE=uuid
ConfigEnvGovernsMust equal
events.primary_key_typeEVENTS_PRIMARY_KEY_TYPEevents.id and every column pointing at an event (lifecycle subjects, tag taggables, media owners, slug history)lifecycle.key_type, tags.key_type (+ sluggable.key_type with slug history)
events.key_typeEVENTS_KEY_TYPEevery column pointing at your models: organizer, attendee, co-host, invitee, inviter, venue owner, added_by, checked_in_byyour users’ key type and lifecycle.actor_key_type

With events.tags.scope = organizer the organizer is a tag scope, so events.key_type must equal tags.key_type too. A model whose key does not fit is refused before any SQL, naming events.key_type — PostgreSQL would otherwise answer with a raw “invalid input syntax for type bigint” — and events:doctor reports every mismatch.

2. Rename the tables if you need to

Every table name is configurable. Rename them before installing when your app already has an events table:

// config/events.php — set before events:install and php artisan migrate
'tables' => [
    'events' => 'meetup_events',            // your app already has an events table
    'venues' => 'event_venues',
    'tiers' => 'event_admission_tiers',
    'rsvps' => 'event_rsvps',
    'hosts' => 'event_hosts',
    'invitations' => 'event_invitations',
    'reminder_deliveries' => 'event_reminder_deliveries',
],

3. Add the traits to your models

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use RoundlyConsulting\Events\Concerns\AttendsEvents;
use RoundlyConsulting\Events\Concerns\OrganizesEvents;
use RoundlyConsulting\Events\Contracts\Organizer;

class User extends Authenticatable implements Organizer
{
    use AttendsEvents;     // $user->rsvp($event)->going(), isGoingTo(), eventFeed(), seats freed on deletion
    use Notifiable;        // notices
    use OrganizesEvents;   // the Organizer contract's defaults
}

AttendsEvents lets the model RSVP and read its events, OrganizesEvents gives the Organizer contract’s defaults and Notifiable receives the notices. Then list the models the API may reference, by alias — teams as organizers are covered on Organizers, co-hosts and invitations:

// config/events.php — the aliases the JSON API uses, never class names
'organizers' => [
    'user' => App\Models\User::class,
    'team' => App\Models\Team::class,      // list every organizer class, teams included
],

'attendees' => [
    'user' => App\Models\User::class,
],

4. Schedule the sweeps

// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('events:sweep')->everyMinute();      // held seats expire, waitlists move up, reminders go out
Schedule::command('lifecycle:sweep')->everyMinute();   // finished events complete at their end

Nothing is put on the scheduler for you. Without events:sweep held seats expire only on the next write to their event and no reminder goes out; without lifecycle:sweep finished events never complete by themselves.

5. Check the installation

php artisan events:doctor
php artisan events:doctor --counts     # also compare every event's counters with a recount
php artisan events:doctor --json       # [{severity, check, message}]

Run the doctor after installing and after every configuration change — the list of checks is on Artisan commands.

Without the installer

php artisan vendor:publish --tag=events-config --tag=lifecycle-migrations \
    --tag=tags-migrations --tag=media-migrations --tag=events-migrations

The migrations are publish-only — the package never loads them — and timestamped on publish.

TagLands in
events-configconfig/events.php
events-migrationsSeven migrations, …_0001_create_event_venues_table.php to …_0007_create_event_reminder_deliveries_table.php — one second apart; republishing lands on the same files.
events-translationslang/vendor/events/{en,sk}/*.php
events-viewsresources/views/vendor/events/json-ld.blade.php
events-routesroutes/events.php — once there, it loads instead of the package’s file.
php artisan vendor:publish --tag=events-translations   # lang/vendor/events/{en,sk}/*.php
php artisan vendor:publish --tag=events-views          # resources/views/vendor/events/json-ld.blade.php
php artisan vendor:publish --tag=events-routes         # routes/events.php — loads instead of the package's

Locales and translations

Titles, descriptions, ticket type and venue names and event slugs are per locale through translatable-for-laravel: pass a locale map (['en' => 'Jazz Night', 'sk' => 'Jazzový večer']) or a string in the current locale; reads follow the current locale, then the fallback chain. The app’s locales must be in translatable.locales (default en, sk) — tagging by name throws invalid_tag otherwise. The package’s own strings ship in English and Slovak with identical keys, in the events:: namespace:

FileHolds
enums.phpEnum labels (label()).
messages.phpEvery exception and denial message — the API’s message.
notifications.phpSubjects, greetings, bodies and buttons of the notices.
validation.phpThe API’s field names in validation messages.
tags.phpThe labels of the two tag types.
feeds.phpCalendar texts — the join line, the personal feed’s name.

Publish them with --tag=events-translations to change a line or add a language.

php artisan about

php artisan about --only=events

The Events section shows the shape of the installation — never a secret:

RowExample
Key typebigint (events), bigint (people)
LifecycleEventLifecycle
RoutesON / OFF
NotificationsON (mail, database) / OFF
RemindersP1D, PT2H / NONE
PaymentsNoPaymentCollector
GeocodingON / OFF
Slug historyON / OFF

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.