Browse documentationOpen

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.

QueryReturns
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 not

The 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();
PropertyMeaning
assignedApproversHow many assignments the stage has
decisionsRecordedHow many holders have responded, in any direction
approvalsReceivedHow many approved
approvalsRequiredThe threshold, with a null rule already resolved
remainingApprovalsHow many more are needed, floored at zero
rejectionsReceivedHow many rejected
possibleApprovalsThe best case still reachable
rejectionPolicyThe stage's policy
statusIts StageStatus
$progress?->isSatisfied();   // approvalsReceived >= approvalsRequired
$progress?->isRejected();    // per the stage's rejection policy

stageProgress() 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:

StateMeaning
condition_skippedA condition turned the stage off at submission
skippedThe 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 reference

Actions 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.