byrcsc/laravel-approval · 1.x
Queries and timelines.
Build approver inboxes, filter models by approval state, read stage progress, and render a request's stage-by-stage timeline.
Facade queries
Each returns an Eloquent builder unless stated 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, which keeps a
request out of their inbox 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() is the odd one out: it returns a model rather than a builder,
and answers the question submit() asks itself when no requester is passed —
the record's approvalRequester() method if it has one, otherwise the
authenticated user. It raises MissingRequesterException when neither answers,
which makes it useful for failing early in a queued job or console command where
there is no authenticated user to fall back on.
Request scopes
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 — everything the
progress, approver, and timeline read models touch. Without it, rendering a
list of requests will run those queries per row.
Model scopes
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);Two general forms take any number of states:
use ByRcsc\LaravelApproval\Enums\RequestState;
PurchaseOrder::query()->approvalState(RequestState::Rejected, RequestState::Returned);
PurchaseOrder::query()->everInApprovalState(RequestState::Rejected);approvalState() asks about the latest request — the current-state
question. everInApprovalState() searches the whole history, however long ago
and whatever came after.
withoutPendingApproval() matches models with no unresolved request, neither
pending nor conflicted.
Current stage and approvers
$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 through 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: the delegate where an assignment was delegated, the assigned approver otherwise.
Stage progress
$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.
Timelines
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
}One entry per stage, in sequence order. timeline() eager-loads what it needs,
so it is safe to call on a request loaded without withProgress() — though on
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
keep their own, reachable through resubmittedFrom() and resubmissions().
$order->latestApprovalTimeline() is the model-side shorthand for the newest
request's timeline.
Reconstructing a full chain
$timelines = collect();
for ($node = $order->latestApprovalRequest; $node !== null; $node = $node->resubmittedFrom) {
$timelines->prepend(['request' => $node, 'entries' => $node->timeline()]);
}Reading history directly
$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 as captured when the line was
written and never refreshed, so the trail still names them after the account is
renamed or deleted. See audit trail and evidence.