Browse documentationOpen

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

EventFires whenAlso carries
ApprovalSubmittedA request exists and its first stage is somebody's turnstage (nullable)
ApprovalCompletedEvery applicable stage was satisfied
ApprovalApprovedThe request reached its final approved state
ApprovalRejectedThe rejection policy ended the requeststage, actor, comment
ApprovalReturnedAn approver sent it back to the requesterstage, actor, comment
ApprovalWithdrawnThe requester withdrew itactor, comment
ApprovalCancelledAn administrator cancelled itactor, comment
ApprovalSystemClosedThe engine closed it, for example on record deletionreason
ApprovalResubmittedA closed request was revised into a new oneprevious

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

EventFires whenAlso carries
StageApprovalRecordedOne approval was recordedstage, actor, approvalsReceived, approvalsRequired
StageCompletedA stage's approval rule was satisfiedstage, next (nullable)
StageOverdueA deadline passed, once per stagestage, action
StageOverdueReminderAn overdue stage is still undecidedstage, action
StageStrandedLiving holders can no longer meet the thresholdstage, 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

EventFires whenAlso carries
DraftAppliedThe draft was written onto the recordapprovable, draft
DraftConflictedFully approved, but the record had movedapprovable (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

EventFires whenAlso carries
ApprovalDelegatedAn approver handed their assignment onassignment, approver, delegate, comment
ApproverReassignedAn administrator rewrote an assignmentassignment, from (nullable), to, actor
ApproversResyncedA live request's assignments were rebuiltchanges, actor, addedAssignments
ApproversEscalatedAn overdue stage's policy added approversstage, 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.