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 migrateevents:install refuses while a configured table already exists that is not the package’s own — events is a common host table name (analytics, event sourcing) — and names the events.tables.* key to change; the package’s own tables, once migrated, are recognised by their columns. 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. Running it again is safe, also after migrate — to publish slug history’s migrations later, say.
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| Config | Env | Governs | Must equal |
|---|---|---|---|
events.primary_key_type | EVENTS_PRIMARY_KEY_TYPE | events.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_type | EVENTS_KEY_TYPE | every column pointing at your models: organizer, attendee, co-host, invitee, inviter, venue owner, added_by, checked_in_by | your 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 endNothing 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.
Upgrading from 1.0
1.1.0 adds one migration. Republish the migrations and migrate:
php artisan vendor:publish --tag=events-migrations # adds only 0008_store_event_instants_as_datetime
php artisan migrateOn MySQL, 0008_store_event_instants_as_datetime turns every instant column of the package’s seven tables from TIMESTAMP into DATETIME: 1.0’s TIMESTAMP columns refuse dates after 2038-01-19 and, with a DST zone as the MySQL session time zone, any value in that zone’s spring-forward gap. It rebuilds those tables, so run it in a quiet moment and with the same database connection settings as your app — the stored values convert in that session’s time zone, the one that wrote them, so they come through unchanged. On PostgreSQL and SQLite it changes nothing. events:doctor warns while the events table still stores TIMESTAMP.
If you published routes/events.php, add UseConfiguredGuard to its outer group, so read routes recognise a user of the guard your auth_middleware names (see JSON API):
// routes/events.php, published before 1.1 — the outer group
use RoundlyConsulting\Events\Http\Middleware\UseConfiguredGuard;
Route::prefix(EventsConfig::routesPrefix())
->middleware([...EventsConfig::routesMiddleware(), UseConfiguredGuard::class])
->name('events.')
->group(static function (): void {
// …
});Behaviour that changed in 1.1:
- The default reference resources render type as the model’s events.organizers or events.attendees alias (user) — the one request bodies and alias:key paths take — instead of the class basename (User). A class in neither map still renders its basename. Clients that compared type with the basename should compare with the alias, or point events.resources.* at a resource of your own.
- A manager’s confirm() without a payment reference now means paid another way, such as cash at the door: the hold’s online checkout, its URL and amount are dropped, so a later online payment of that checkout is not found — refund it. With a reference the seat counts as paid, as before.
- A going or held attendee who adds guests may no longer take free seats a waitlisted party of that ticket type would fit (EventFullException, or TierSoldOutException on a ticket type). A manager’s changeGuests() is unchanged.
- An attendee’s own paid answer asks for its checkout through the manager, so Events::fake() records it: assertPaymentRequested() passes after a paid RSVP and assertNoPaymentRequested() fails.
Without the installer
php artisan vendor:publish --tag=events-config --tag=lifecycle-migrations \
--tag=tags-migrations --tag=media-migrations --tag=events-migrationsThe migrations are publish-only — the package never loads them — and timestamped on publish. The eighth, 0008, switches the instant columns to DATETIME on MySQL and changes nothing elsewhere.
| Tag | Lands in |
|---|---|
events-config | config/events.php |
events-migrations | Eight migrations: …_0001_create_event_venues_table.php to …_0007_create_event_reminder_deliveries_table.php, plus …_0008_store_event_instants_as_datetime.php — one second apart; republishing lands on the same files and adds only new ones. |
events-translations | lang/vendor/events/{en,sk}/*.php |
events-views | resources/views/vendor/events/json-ld.blade.php |
events-routes | routes/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'sLocales 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:
| File | Holds |
|---|---|
enums.php | Enum labels (label()). |
messages.php | Every exception and denial message — the API’s message. |
notifications.php | Subjects, greetings, bodies and buttons of the notices. |
validation.php | The API’s field names in validation messages. |
tags.php | The labels of the two tag types. |
feeds.php | Calendar 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=eventsThe Events section shows the shape of the installation — never a secret:
| Row | Example |
|---|---|
| Key type | bigint (events), bigint (people) |
| Lifecycle | EventLifecycle |
| Routes | ON / OFF |
| Notifications | ON (mail, database) / OFF |
| Reminders | P1D, PT2H / NONE |
| Payments | NoPaymentCollector |
| Geocoding | ON / OFF |
| Slug history | ON / 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 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.