Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Events For Laravel Laravel Package

roundly-consulting/events-for-laravel

A lightweight events package for Laravel that streamlines defining, dispatching, and listening to application events. It offers a clean, framework-native API with sensible defaults and simple registration, so you can add event-driven behaviour to your app without boilerplate.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package, then publish and run its migrations before touching anything else:

composer require roundlyconsulting/events
php artisan vendor:publish --tag=events-migrations
php artisan migrate
php artisan events:doctor

events:doctor is your first stop on any environment — it is the package's own health check and will flag, for example, an events table still storing TIMESTAMP on MySQL/MariaDB. Run it after every deploy that touches the database.

Where to look first:

  • config/events.php — schedule limits (events.schedule.max_duration), events.routes.auth_middleware, events.resources.*, waitlist.stop_promoting_before.
  • The published routes/events.php — if you have published it, you own keeping it in sync (see below).
  • The Events facade — the entire day-to-day surface (Events::make(), Events::rsvps(), Events::checkIn(), Events::fake()).

First use case — a working event with a tier and a paid RSVP:

$event = Events::make()
    ->named('Laravel Meetup')
    ->startsAt('2026-03-14 18:00')
    ->tier('General', price: 25_00, currency: 'EUR')
    ->create();

$rsvp = Events::rsvps()->request($event, $user, tier: 'general');
$checkout = Events::rsvps()->requestPayment($rsvp);   // attendee-initiated, no actor, no policy check

Then wrap the same flow in Events::fake() in tests and assert on it — the fake is the fastest way to learn the API's vocabulary.

Implementation Patterns

Upgrading a 1.0 app to 1.1. Two mandatory steps, in this order:

  1. php artisan vendor:publish --tag=events-migrations (adds only 0008_store_event_instants_as_datetime) then php artisan migrate.
  2. If you published routes/events.php, add RoundlyConsulting\Events\Http\Middleware\UseConfiguredGuard to the outer group's middleware.

The second step is not optional in practice: without it, callers authenticated by the guards named in events.routes.auth_middleware (e.g. auth:sanctum tokens) read as guests on read routes — private events they were invited to, their own RSVP, filter[mine], and the online link will all be wrong. Guests are still allowed through, so the failure is silent-ish rather than a hard 403.

Run migrations with your app's real connection settings. The 0008 migration rewrites instants into DATETIME; the conversion happens in the session time zone, which is the one that wrote the values, so they round-trip unchanged. A migration run with a different time_zone session setting is how you corrupt a year of timestamps.

Modelling a payment flow. requestPayment() is the single entry point for both attendee-initiated and post-promotion checkout — it takes no actor, so no policy check runs. That means it is safe to call from a job, a webhook handler, or a queued listener. Manager confirmation has two distinct meanings:

Events::rsvps()->confirm($rsvp);                 // "paid another way" (cash at the door): online checkout dropped
Events::rsvps()->confirm($rsvp, 'terminal-42');  // paid, reference retained

Only the second form counts the seat as paid. With the first form, a later online payment against the old checkout will not be found — handle that as a refund path, and expect wasPaid: false when the attendee leaves.

Testing with the fake. The builder is recorded now, so assert on the parts of a chain rather than the whole thing:

Events::fake();

Events::make()->coHostedBy($user)->tier('VIP', 50_00)->poster($file)->category('talks')->labels(['php'])->create();

Events::assertTierCreated();
Events::assertCoHostAdded();
Events::assertNoCategoryCreated(); // ...etc.

Fake assertions are outcome-based, not call-based: assertCoHostAdded() fails if you added the organizer or an existing co-host (nothing changed), assertCheckInUndone() fails unless a check-in was actually undone, and assertNothingCheckedIn() ignores repeat scans the same way assertCheckedIn() does. Write assertions about state transitions, not about how many times a method was called.

Reschedule and update. Prefer reschedule() / update() over manual date assignment: they preserve the exact event length when only one bound moves (a 28-day event stays 28 days across a 30-day month), they clear the doors time when a draft moves, they lock the event row before deciding between draft edit and announced reschedule, and they emit EventRescheduled with previous_starts_at. All-day JSON-LD gets previousStartDate on the announced first day.

API resources. Default resources render a host model's type as its configured alias (user), not the class basename — this is what request bodies and alias:key paths accept, so a client can round-trip what it reads. If you override events.resources.*, keep the alias contract or accept breaking your own clients.

Waitlist-first seating. If automatic promotion is off, or you are past waitlist.stop_promoting_before, a going attendee who adds guests can no longer absorb seats a waitlisted party would fit — you get EventFullException or TierSoldOutException on a ticket type. Manager changeGuests() bypasses this. Design guest-add flows to surface those exceptions as a "join the waitlist" prompt rather than a 500.

Gotchas and Tips

  • Version pinning and upgrade windows. 1.1 needs 0008 plus the middleware line if you published routes. Treat that pair as atomic; a deploy that runs the new code against old tables (or vice versa) is the most likely source of weirdness. events:doctor is the tripwire.
  • The 2038 problem is fixed only after migrating. startsAt('2040-…') was an SQL error and an HTTP 500 on MySQL/MariaDB pre-0008. If you store far-future events, confirm the migration ran on every environment, including read replicas used for reporting.
  • DST spring-forward holes. With a DST session zone such as Europe/Bratislava, writes whose UTC value landed in the skipped hour previously failed en masse "for that hour of the year". Post-1.1 they succeed — but if you have a monitoring gap around a DST change, expect that to explain a historical write outage rather than a new bug.
  • events.schedule.max_duration is measured in the event's own timezone, or elapsed time, whichever allows more. A 31-day all-day event across an autumn DST switch is legal. Don't reimplement the validation client-side without the same rule.
  • Announced events can't be rescheduled into a past date — that's a 422 invalid_schedule. Drafts may take any date, and a new date that has started but not ended is fine. Check for this before showing a confirmation UI that promises the move.
  • Immutable dates. If you call Date::use(CarbonImmutable::class), 1.0's models threw TypeError on save. Post-1.1 it works; keep the setting if you rely on it, and add a smoke test for one save/update cycle.
  • Money validation is strict now. A price with explicit currency and too many decimals (15.999 EUR, 15.5 JPY) returns 422 invalid_tier instead of a 500. Validate with the same rule in your form requests to keep the error user-facing.
  • All-digit tier keys are legal. A tier keyed 2027 (or derived from such a name) is addressable: /tiers/{tier} resolves the key first and only reads a numeric segment as an id when no tier has that key. If you generate tier keys from names, assume ids and keys can collide and always reference tiers by key in your own code.
  • PATCH /events/{event}/attendees/{rsvp} checks manageAttendees first. An empty body from a non-manager is a 403 (previously it leaked the managers' view of the RSVP); a manager gets a 422 naming the missing tier / guests. Don't rely on the empty-body response as a "read" endpoint.
  • Check-ins follow status. Any exit from going (including a manager's attendees()->remove()) clears the check-in record, attendees()->checkedIn() lists going RSVPs only, and a re-added attendee checks in fresh (AttendeeCheckedIn) and may decline again. Reconcile any external check-in reporting against this rather than caching codes.
  • Force-deletes and events:repair --orphans do announce — RsvpChanged plus RsvpCancelled with wasPaid for paid seats. If you previously missed refund webhooks for hard-deleted attendees, this is the fix. The RSVP row is already gone when listeners run, so read the payment reference off the event, not the RSVP.
  • RsvpPaymentRequestDiscarded fires when a hold was refreshed mid-collector (new party size/type → new amount) and the stale checkout is dropped. Log and surface it; otherwise a user pays a smaller checkout and nothing confirms.
  • Invitations. Re-sending after acceptance mails the new link (the notice used to be dropped at delivery); a notice whose link died before delivery is no longer mailed. Don't build retry logic that assumes every send again produces an email.
  • Extension points to keep in mind: point events.resources.* at your own resource classes (and keep the alias contract), supply your own middleware stack around routes/events.php, and hook listeners on RsvpChanged / RsvpCancelled / AttendeeCheckedIn / EventRescheduled for downstream sync. Those events are the stable seam — internal call paths are not.
  • Unscoped check-in lookups. RecordedEventOperation::event() and concerns() can find the event from an unscoped Events::checkIn($code, by:) result, so audit-style tooling no longer needs the event id up front.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox