The Approvals facade
Everything goes through one API — the Approvals facade, the injectable ApprovalsManager behind it, or the action classes it runs. All three execute the same code, and the facade is the recommended entry point. It is registered automatically. The full surface:
use RoundlyConsulting\Approvals\Facades\Approvals;
// One actor's decision on one approvable
Approvals::for($deployment)->as($user)->because('Looks good to me')->approve();
Approvals::for($deployment)->as($user)->because('Please add tests')->reject(); // withdraws the approval above
Approvals::for($deployment)->as($user)->ask(); // ask the actor: a pending decision
Approvals::for($deployment)->as($user)->cancel(); // withdraw the actor's live decision
Approvals::for($deployment)->as($user)->toggle(); // simple on/off
Approvals::for($deployment)->as($user)->expiresIn(86400)->approve(); // valid for a day
Approvals::for($deployment)->as($user)->weight(3)->approve(); // override the decision's weight
Approvals::for($invoice)->as($user)->within($request)->approve(); // pin to one request
// Reads
Approvals::for($deployment)->as($user)->isApproved(); // bool
Approvals::for($deployment)->as($user)->isRejected(); // bool
Approvals::for($deployment)->hasPending(); // bool — any pending decision
// Multi-approver requests (flat, staged, or from a workflow preset)
Approvals::request($invoice)->from([$a, $b, $c])->quorum(2)->expiresIn(3600)->open();
Approvals::request($release)->stages([...])->continueOnRejection()->expiringAt($t)->open();
Approvals::request($budget)->workflow('payout')->open([$a, $b, $c]);
Approvals::status($invoice); // ApprovalStatus of the latest request (Pending when none)
Approvals::progress($invoice); // ?ApprovalProgress
Approvals::currentStage($release); // ?ApprovalRequestStage
Approvals::preset('payout'); // WorkflowPreset from config
// Delegation
Approvals::delegations($boss)->to($deputy)->from($monday)->until($friday)->grant();
Approvals::delegations($boss)->revoke($deputy); // or ->revoke() for all; returns int
Approvals::delegations($boss)->active(); // Collection<ApprovalDelegation>
Approvals::delegationFor($deputy); // ?ApprovalDelegation in force now
// Housekeeping
Approvals::expire(); // lapse overdue asks, approvals and requests; returns int
Approvals::expire(subjectType: Invoice::class); // only those on invoices (class or morph alias)Facade methods
| Method | Returns | Purpose |
|---|---|---|
for($approvable) | PendingApproval | Start a decision on an approvable; set the actor with as(). |
as($actor) | PendingApproval | Start a decision by an actor; set the approvable with for(). |
request($subject) | PendingApprovalRequest | Open a multi-approver request — flat, staged or from a preset. |
status($subject) | ApprovalStatus | Status of the subject’s latest request; Pending when it has none, Expired once an open request is past its expiry. |
progress($subject) | ?ApprovalProgress | Progress snapshot of the latest request. |
currentStage($subject) | ?ApprovalRequestStage | The open stage of the latest staged request. |
preset($name) | WorkflowPreset | Resolve and validate a preset from config('approvals.workflows'). |
delegations($delegator) | DelegationsHandle | Grant, revoke and list one delegator’s delegations. |
delegationFor($delegate, ?$at) | ?ApprovalDelegation | The delegation the model acts under at $at (now when omitted). |
expire(?$now, ?$subjectType) | int | Lapse overdue asks, approvals and requests; returns how many. With $subjectType (a model class or morph alias) only those on that type. |
fake() | ApprovalsFake | Swap in the recording fake; recorded() and every assert*() are then callable on the facade too (see Testing). |
Decisions — PendingApproval
Start from the approvable with for() or from the actor with as(), chain the context, then call a terminal method. A request that names its approvers refuses anyone else with UnauthorizedApprovalException, and a closed request refuses every decision with ClosedApprovalRequestException:
| Method | Returns | Purpose |
|---|---|---|
for($approvable) | self | Set the model being decided on. |
as($actor) | self | Set the deciding model. |
within($request) | self | Pin the decision to this request instead of the approvable’s latest open one. It must belong to the approvable (else InvalidApprovalRequestException) and still be open (else ClosedApprovalRequestException). |
because($reason) | self | Attach a reason — stored by approve(), reject() and cancel(). |
weight($n) | self | Override the weight this decision carries — approve() and reject(). |
expiresIn($seconds) | self | Expire N seconds from now — applied by approve() and ask(). |
expiringAt($at) | self | Absolute expiry — applied by approve() and ask(). |
approve() | Approval | Record an approval. It counts towards the pinned request, else the approvable’s latest open request; repeating it is a no-op. |
reject() | Approval | Record a rejection — an approval the actor held is withdrawn. |
ask() | Approval | Ask the actor for a pending decision; returns the actor’s live decision unchanged when it already holds one. |
cancel() | ?Approval | Withdraw the actor’s live decision — or one it made as a delegate — in the round a decision would land in; null when there is none. |
toggle() | bool | Simple on/off — true approved (through approve()), false withdrawn. |
isApproved() | bool | Whether the actor holds an approval of the approvable that is still in force. |
isRejected() | bool | Whether the actor holds a rejection for the approvable. |
hasPending() | bool | Whether the approvable has any pending decision not yet past its deadline — needs only for(). |
Requests — PendingApprovalRequest
| Method | Returns | Purpose |
|---|---|---|
from([...]) | self | The named approvers of a flat request — only they (or their delegates) can decide it. Each is stored once; required_approvers defaults to their count. |
rule($rule, ?$quorum) | self | The flat request’s rule and threshold — Unanimous by default. |
any() / quorum($n) / weighted($n) | self | Shortcuts for rule(Any), rule(Quorum, $n) and rule(Weighted, $n). |
stages([StageDefinition, ...]) | self | Make it a staged request. Combined with from(), open() throws InvalidApprovalRequestException. |
continueOnRejection() | self | Staged: keep going past a rejected stage. |
expiresIn($s) / expiringAt($t) | self | Stamp expires_at on the request. |
open() | ApprovalRequest | Open the flat or staged request; a threshold its approvers could never reach is refused. |
workflow($name)->open([...]) | ApprovalRequest | Open from a named preset — a flat list, or one list per stage. |
Delegation — DelegationsHandle
| Method | Returns | Purpose |
|---|---|---|
delegations($d)->to($delegate) | PendingDelegation | Start a delegation — nothing is written until grant(). |
->from($t) / ->until($t) / ->for($seconds) | PendingDelegation | Set the window; for() counts from the start (now without from()). |
->grant() | ApprovalDelegation | Validate the window, save it and fire ApprovalDelegated. |
delegations($d)->revoke(?$delegate) | int | Revoke this delegator’s active and scheduled delegations — or only those to $delegate. |
delegations($d)->active(?$at) | Collection<ApprovalDelegation> | Delegations in force at $at (now), newest first. |
Pinning to a request, the approvals() helper, absolute expiry and incomplete builders are covered under Fluent builders; the same API without the facade is under DI and actions.
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.