Each transition dispatches an event carrying the relevant model. All events live in RoundlyConsulting\Approvals\Events and expose their payload as public properties:
| Event | Payload | Fired when |
|---|---|---|
ApprovalRequested | approval | a pending decision is asked for (ask()) |
ApprovalApproved | approval | a decision is approved |
ApprovalRejected | approval | a decision is rejected |
ApprovalCancelled | approval | a decision is withdrawn — by cancel(), by a toggle-off, or an outstanding ask retired because its request resolved, its stage settled or your app closed the round (close()) |
ApprovalExpired | approval | a pending or approved decision lapses |
ApprovalRequestResolved | request | a request reaches approved, rejected or expired — or your app closes it as cancelled or expired (close()); once, after its outstanding asks are retired |
ApprovalToggled | actor, entity, hasBeenApproved | toggle() / toggleApproval() runs |
ApprovalStageOpened | stage | a staged request opens a stage |
ApprovalStageCleared | stage | a stage clears as approved |
ApprovalDelegated | delegation | an approver delegates authority |
ApprovalDelegationRevoked | delegation | a delegation is revoked |
ApprovalStatusChanged | subject, from, to, actor | umbrella — every status transition of a decision or request, alongside the granular event |
The umbrella event
Subscribe to ApprovalStatusChanged once to observe all transitions: a decision approved, rejected, withdrawn — including by a toggle or by a change of mind that supersedes it — or expired, an outstanding ask retired when its round closes, and a request resolved, expired or closed by your app. It carries the subject (the Approval or ApprovalRequest that changed), the from and to ApprovalStatus, and the actor when one is known — null for a resolved request or a retired ask:
use RoundlyConsulting\Approvals\Events\ApprovalStatusChanged;
class AuditApprovalChanges
{
public function handle(ApprovalStatusChanged $event): void
{
// $event->from, $event->to, $event->subject, $event->actor
logger()->info("approval moved {$event->from->value} -> {$event->to->value}");
}
}Granular listeners
use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Approvals\Events\ApprovalApproved;
use RoundlyConsulting\Approvals\Events\ApprovalRequestResolved;
Event::listen(function (ApprovalApproved $event): void {
$approval = $event->approval; // the Approval model
});
Event::listen(function (ApprovalRequestResolved $event): void {
$request = $event->request; // approved, rejected or expired — see $request->status
$subject = $request->subject; // e.g. the Release
});An ask() creates a pending decision rather than changing one, so it fires only ApprovalRequested. Stage changes and delegation also fire only their own events — not the umbrella one.
When a round closes
Whether its decisions resolve it, it lapses or your app closes it, a round’s events come in one order: ApprovalCancelled and ApprovalStatusChanged for each retired ask, then ApprovalRequestResolved, then ApprovalStatusChanged for the request. A listener of the resolution never sees an ask still waiting on the round.
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.