Artisan commands
| Command | Does |
|---|---|
events:install | Refuses 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 endevents: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}]:
| Check | Severity | Finds |
|---|---|---|
key_types | error | lifecycle.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. |
lifecycle | error | The configured lifecycle breaks the contract (initial state not a draft, a state without a phase, it does not compile). |
tag_types | error / warning | The 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. |
tables | error | A configured table is missing; slug history on without slug_history. |
distance | error | The near-me distance query fails on the default connection. |
policy, venue_policy, resources, payments | error | A configured class is missing or not an implementation; payments also a hold that is not a positive duration. |
reminders | error | Offsets outside PT5M–P31D, more than five, or not durations; an unreadable max_lateness. |
payments | warning | Paid ticket types, the null collector and no RsvpHeld listener. |
urls · routes | warning | events.urls.event is blank · the API is on with an empty auth_middleware. |
notifications | warning | The database channel without a notifications table; attendee types that cannot be notified. |
timezones · organizers · postponed | warning | Events in a timezone other than their venue’s · a stored organizer_type not in events.organizers · postponed for more than 90 days. |
counters | warning | With --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 batchHolds 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 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.