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

Artisan commands

CommandDoes
events:installRefuses while a configured table exists (naming the events.tables.* key); publishes the config and the migrations, prints the key types side by side and the next steps.
events:doctor {--counts} {--json}Checks the installation; exits non-zero on an error, warnings alone exit 0.
events:sweep {--limit=} {--chunk=100}Expires lapsed holds, moves waitlists up, sends due reminders — schedule every minute.
events:repair {--counts} {--orphans} {--dry-run}Recomputes counters / releases orphaned RSVPs — both without a flag.
events:geocode {--missing} {--venue=} {--force} {--chunk=100}Geocodes venues from their addresses.

The provider registers the commands but puts nothing on the scheduler:

// 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

events:install

  • Exits 1 while a configured table already exists, naming the events.tables.* key to change.
  • Publishes events-config, lifecycle-migrations, tags-migrations, media-migrations and events-migrations (plus sluggable-migrations with slug history on); files you already published are kept.
  • Prints events.primary_key_type, lifecycle.key_type, tags.key_type, events.key_type and lifecycle.actor_key_type side by side, then the next steps: migrate, the model traits, the two scheduler lines, the payment collector and events:doctor.

events:doctor

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}]

Exits non-zero when an error is found; warnings alone exit 0. Each finding prints as [check] message (or “Everything looks right.”), and --json prints [{severity, check, message}]:

CheckSeverityFinds
key_typeserrorlifecycle.key_type / tags.key_type ≠ events.primary_key_type; lifecycle.actor_key_type ≠ events.key_type; organizer-scoped labels with tags.key_type ≠ events.key_type; slug history with sluggable.key_type ≠ events.primary_key_type.
lifecycleerrorThe configured lifecycle breaks the contract (initial state not a draft, a state without a phase, it does not compile).
tag_typeserror / warningThe tag types are missing, or an override allows more than one category · an override makes the category not curated, global or hierarchical, labels not private, or either type a free-form tree.
tableserrorA configured table is missing; slug history on without slug_history.
distanceerrorThe near-me distance query fails on the default connection.
policy, venue_policy, resources, paymentserrorA configured class is missing or not an implementation; payments also a hold that is not a positive duration.
reminderserrorOffsets outside PT5M–P31D, more than five, or not durations; an unreadable max_lateness.
paymentswarningPaid ticket types, the null collector and no RsvpHeld listener.
urls · routeswarningevents.urls.event is blank · the API is on with an empty auth_middleware.
notificationswarningThe database channel without a notifications table; attendee types that cannot be notified.
timezones · organizers · postponedwarningEvents in a timezone other than their venue’s · a stored organizer_type not in events.organizers · postponed for more than 90 days.
counterswarningWith --counts: counters that drifted from a recount.

events:sweep

php artisan events:sweep                  # expire lapsed holds, move waitlists up, send due reminders
php artisan events:sweep --limit=500      # at most this many events per part (holds, reminders)
php artisan events:sweep --chunk=100      # events per batch

Holds expire per event under its lock, trashed events included; promotions follow; reminders are claimed once — only for scheduled or rescheduled events starting within the next 32 days. --limit caps the events of each part (holds, reminders). The command prints the four counts.

events:repair

php artisan events:repair                 # both: counters and orphaned RSVPs
php artisan events:repair --counts        # recompute every event's counters under its row lock
php artisan events:repair --orphans       # release the RSVPs of attendees whose row is gone
php artisan events:repair --dry-run       # report without writing
  • --counts recomputes every event’s counters under its row lock — a drift means a bug or a hand-made write.
  • --orphans releases the RSVPs of attendees whose row is gone — a mass delete fires no model events: seats returned, waitlist promoted, rows removed, one query per attendee type.
  • --dry-run reports without writing. The command prints the three counts, marked as a dry run when nothing was written.

events:geocode

php artisan events:geocode                 # venues without coordinates (--missing, the default)
php artisan events:geocode --venue=7       # one venue (skipped when it has coordinates, unless --force)
php artisan events:geocode --force         # every venue again
php artisan events:geocode --chunk=200     # venues per batch (default 100)

Venues are read in id order, --chunk at a time; a failing venue is counted as not found and warned about, never fatal. It prints Geocoded and Not found and exits 0.

From PHP

use RoundlyConsulting\Events\DataTransferObjects\RepairOptions;
use RoundlyConsulting\Events\DataTransferObjects\SweepOptions;

$swept = Events::sweep(new SweepOptions(limit: null, chunk: 100));
// SweepResult {events, expiredHolds, promoted, reminders, notified}

$report = Events::repair(new RepairOptions(counts: true, orphans: true, dryRun: false, chunk: 100));
// RepairReport {eventsChecked, countersFixed, orphansReleased, dryRun}

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.