| Event | When | Payload |
|---|---|---|
LifecycleTransitioning | Inside the transaction, before the write (a throwing listener aborts). | subject, lifecycle, transition, from, to, actor, system |
LifecycleTransitioned | After commit, once per state change: kinds transition, expiry, scheduled, rollback, adopted. | subject, subjectType, subjectId, lifecycle, kind, transition, from, to, actor, system, historyId, version |
LifecycleTransitionDenied | After the refused attempt rolled back (even when your own transaction then rolls back). | subjectType, subjectId, lifecycle, transition, actor, denials |
LifecycleExpired | After commit, with the expiry’s LifecycleTransitioned. | subject, lifecycle, from, to, historyId, expiresAt |
LifecycleExpiring | After commit, once per warning lead. | subject, lifecycle, state, expiresAt, lead, scheduleId |
LifecycleRolledBack | After commit, once per rollback call. | subject, lifecycle, from, to, revertedIds, rollbackIds, actor |
LifecycleFrozen / LifecycleUnfrozen | After commit. | subject, lifecycle, until, reason, actor |
LifecycleAdopted | After commit. | subject, lifecycle, recordedState, actualState, historyId |
ScheduledTransitionFailed | After commit. | scheduleId, subjectType, subjectId, lifecycle, transition, denials, attempts, final |
Listen for LifecycleTransitioned rather than Eloquent’s updated: the state write is a direct compare-and-swap, so model events do not fire for it. It covers every state change — transitions, expiries, scheduled runs, rollback steps and adoptions — but not creation, where your model’s created event and the initial history row are the signal:
use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Lifecycle\Enums\TransitionKind;
use RoundlyConsulting\Lifecycle\Events\LifecycleTransitioned;
Event::listen(function (LifecycleTransitioned $event): void {
if ($event->subject instanceof Listing && $event->to === ListingStatus::Active) {
SearchIndex::add($event->subject);
}
if ($event->kind === TransitionKind::Rollback) {
AuditLog::record("Rolled back to {$event->to->value}", $event->historyId);
}
});- LifecycleTransitioning runs inside the transaction; a throwing listener aborts and rolls everything back, but denials belong in guards. A deadlock retry runs it again.
- LifecycleTransitionDenied is deliberately not after-commit — it fires even when your own outer transaction then rolls back because of the denial.
- After-commit events inside your own transaction are held until your outermost transaction commits, and discarded if it rolls back.
- Payloads never contain the transition payload. Read the history row (historyId) when you need the stored, validated context.
- Queued listeners should re-fetch the subject by subjectType / subjectId.
- Idempotent replays and Lifecycles::fake() fire nothing.
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.