›
byrcsc/laravel-approval · 1.x
Build approver inboxes, overdue lists, progress summaries, and timelines.
Each query returns an Eloquent builder unless the table says otherwise, so ordinary constraints, eager loads, and pagination all apply.
| Query | Returns |
|---|---|
Approval::awaiting($user) | Pending requests whose active stage waits on them |
Approval::overdue() | Pending requests past their active stage's deadline |
Approval::requestsFor($model) | That model's request history, newest first |
Approval::pendingFor($model) | Its one unresolved request, or null |
Approval::requesterFor($model) | Who a submission would be attributed to, as a model |
$inbox = Approval::awaiting($user)
->withProgress()
->paginate(20);awaiting() excludes requests the user has already approved. A request leaves
the user's inbox once they approve, even while the stage waits on the remaining
approvers. A
delegated assignment belongs to the delegate rather than the delegating
approver, so it appears in exactly one inbox.
requesterFor() returns a model rather than a builder. It returns the
requester that submit() uses when you pass no requester. The method uses the
record's approvalRequester() result first, then the authenticated user. It
raises MissingRequesterException when neither gives a requester. Call it to
fail early in a queued job or console command, where there is no authenticated
user.
use ByRcsc\LaravelApproval\Models\ApprovalRequest;
ApprovalRequest::query()->pending();
ApprovalRequest::query()->unresolved(); // pending or conflicted
ApprovalRequest::query()->overdue();
ApprovalRequest::query()->forApprovable($order);
ApprovalRequest::query()->awaitingUser($user);
ApprovalRequest::query()->withProgress();withProgress() eager-loads stages.assignments.approver,
stages.assignments.delegatedTo, and stages.actions. The progress, approver,
and timeline read models use those relations. Without withProgress(), a list
of requests runs those queries once per row.
The Approvable concern adds scopes to the model under approval:
PurchaseOrder::query()->pendingApproval();
PurchaseOrder::query()->approvalApproved();
PurchaseOrder::query()->approvalRejected();
PurchaseOrder::query()->approvalReturned();
PurchaseOrder::query()->approvalWithdrawn();
PurchaseOrder::query()->approvalCancelled();
PurchaseOrder::query()->approvalConflicted();
PurchaseOrder::query()->approvalSuperseded();
PurchaseOrder::query()->withoutPendingApproval();
PurchaseOrder::query()->awaitingApprovalBy($user);approvalState() and everInApprovalState() take any number of states:
use ByRcsc\LaravelApproval\Enums\RequestState;
PurchaseOrder::query()->approvalState(RequestState::Rejected, RequestState::Returned);
PurchaseOrder::query()->everInApprovalState(RequestState::Rejected);approvalState() checks the latest request, so it tells you the record's
current state. everInApprovalState() searches the whole history and matches a
state from any request, whatever happened after it.
withoutPendingApproval() matches models with no unresolved request, neither
pending nor conflicted.
$request = Approval::pendingFor($order);
$request?->currentStage(); // the active ApprovalRequestStage
$request?->pendingApprovers(); // holders who have not responded
$request?->currentStageApprovers(); // every holder, responded or notThe model-side equivalents read the model's own open request:
$order->currentApprovalStage();
$order->pendingApprovalApprovers();
$order->currentStageApprovers();
$order->isPendingApproval();Each returns an empty collection, or null, when there is no unresolved request.
Both approver lists return the holder of each assignment. The holder is the delegate if the assignment was delegated, and the assigned approver otherwise.
$progress = $request->stageProgress();| Property | Meaning |
|---|---|
assignedApprovers | How many assignments the stage has |
decisionsRecorded | How many holders have responded, in any direction |
approvalsReceived | How many approved |
approvalsRequired | The threshold, with a null rule already resolved |
remainingApprovals | How many more are needed, floored at zero |
rejectionsReceived | How many rejected |
possibleApprovals | The best case still reachable |
rejectionPolicy | The stage's policy |
status | Its StageStatus |
$progress?->isSatisfied(); // approvalsReceived >= approvalsRequired
$progress?->isRejected(); // per the stage's rejection policystageProgress() reads the stage the request points at with
current_stage_sequence, and returns null when there is none.
foreach ($request->timeline() as $entry) {
$entry->stage; // ApprovalRequestStage
$entry->state; // TimelineState
$entry->approvers; // Collection<Model> of assignment holders
$entry->actions; // Collection<ApprovalAction> for this stage
}timeline() returns one entry per stage, in sequence order. It eager-loads
what it needs, so you can call it on a request loaded without withProgress().
For a list of requests, loading progress up front is cheaper.
TimelineState adds one case to StageStatus:
| State | Meaning |
|---|---|
condition_skipped | A condition turned the stage off at submission |
skipped | The request ended before the stage ran |
A timeline covers one request. Earlier and later requests in a linked chain
have their own timelines. Reach them through resubmittedFrom() and
resubmissions().
$order->latestApprovalTimeline() is the model-side shorthand for the newest
request's timeline.
$timelines = collect();
for ($node = $order->latestApprovalRequest; $node !== null; $node = $node->resubmittedFrom) {
$timelines->prepend(['request' => $node, 'entries' => $node->timeline()]);
}$request->actions; // every audit action, oldest first
$request->attachments; // every attachment referenceActions carry action, comment, metadata, actor_label, and the hash
chain. actor_label is the actor's readable name at the time the action was
written. The package never refreshes it, so the trail still names the actor
after the account is renamed or deleted. See
audit trail and evidence.