byrcsc/laravel-approval · 1.x
Events and listeners.
Every lifecycle event the package fires, what each carries, and how to listen safely from inside the transaction that caused it.
Every event extends ApprovalEvent and carries the request it happened to, so
a listener that only cares which record moved can type-hint the base class:
use ByRcsc\LaravelApproval\Events\ApprovalEvent;
Event::listen(ApprovalEvent::class, function (ApprovalEvent $event) {
$event->request;
});Request lifecycle
| Event | Fires when | Also carries |
|---|---|---|
ApprovalSubmitted | A request exists and its first stage is somebody's turn | stage (nullable) |
ApprovalCompleted | Every applicable stage was satisfied | — |
ApprovalApproved | The request reached its final approved state | — |
ApprovalRejected | The rejection policy ended the request | stage, actor, comment |
ApprovalReturned | An approver sent it back to the requester | stage, actor, comment |
ApprovalWithdrawn | The requester withdrew it | actor, comment |
ApprovalCancelled | An administrator cancelled it | actor, comment |
ApprovalSystemClosed | The engine closed it, for example on record deletion | reason |
ApprovalResubmitted | A closed request was revised into a new one | previous |
ApprovalCompleted and ApprovalApproved are deliberately separate.
Completion means the routing finished; approval means the request reached its
final state, draft applied. A request that completes and then conflicts fires
the first and not the second.
On ApprovalRejected, actor is null when the clock rejected it — an
auto_reject stage whose deadline passed.
On ApprovalResubmitted, $request is the replacement and $previous is the
request it replaces.
Stages
| Event | Fires when | Also carries |
|---|---|---|
StageApprovalRecorded | One approval was recorded | stage, actor, approvalsReceived, approvalsRequired |
StageCompleted | A stage's approval rule was satisfied | stage, next (nullable) |
StageOverdue | A deadline passed, once per stage | stage, action |
StageOverdueReminder | An overdue stage is still undecided | stage, action |
StageStranded | Living holders can no longer meet the threshold | stage, livingHolders, requiredApprovals |
StageCompleted carries the stage now waiting as next, or null when this was
the last one — in which case ApprovalCompleted follows.
StageOverdue fires before the escalation strategy acts, so a listener
sees the state that went overdue.
Drafts
| Event | Fires when | Also carries |
|---|---|---|
DraftApplied | The draft was written onto the record | approvable, draft |
DraftConflicted | Fully approved, but the record had moved | approvable (nullable), attributes |
On DraftApplied, the record is already saved, so a listener reads the new
values.
On DraftConflicted, attributes names what drifted and approvable is null
when the record itself is gone. This is the event to listen for if somebody
should be told to go and look — a conflicted request sits there until a person
resolves it.
Assignments
| Event | Fires when | Also carries |
|---|---|---|
ApprovalDelegated | An approver handed their assignment on | assignment, approver, delegate, comment |
ApproverReassigned | An administrator rewrote an assignment | assignment, from (nullable), to, actor |
ApproversResynced | A live request's assignments were rebuilt | changes, actor, addedAssignments |
ApproversEscalated | An overdue stage's policy added approvers | stage, assignments |
On ApproverReassigned, from is null when the record that held the
assignment has itself been deleted.
ApproversEscalated is dispatched after the new assignments exist, so
recipient calculation can only ever see the new holders. Escalation is
automated: the system is the source, not a human actor.
Events fire inside the transaction
Every event this package fires is dispatched inside the transaction that caused it. Two consequences:
A listener that does anything slow or external should be queued.
use Illuminate\Contracts\Queue\ShouldQueue;
final class PostToSlack implements ShouldQueue
{
public function handle(ApprovalCompleted $event): void { /* … */ }
}A queued listener should dispatch after commit. The request it was handed may still be rolled back:
public $afterCommit = true;The shipped notification listener already defers its send with
DB::afterCommit(), for exactly this reason.
Listening to the base class
Because every event extends ApprovalEvent, a single listener can feed an
activity stream:
Event::listen(ApprovalEvent::class, function (ApprovalEvent $event): void {
ActivityLog::create([
'type' => class_basename($event),
'request_ulid' => $event->request->ulid,
]);
});Note that the package already keeps its own append-only history — see audit trail and evidence. Use events to drive your application's reactions, not to reimplement the trail.
Events versus notifications
The notification layer is a thin listener over a subset of these events, and it is off by default. Anything the shipped notifications do not cover, you can reach by listening directly. See notifications.