›
›
›
  1. docs
  2. ›
  3. byrcsc/laravel-approval
1.x
Browse documentationOpenClose

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Workflows and stages
  • Choosing the workflow
  • Approvers and resolvers
  • Requests and state
  • Conditional stages

Approval flow

  • Submitting for approval
  • Recording decisions
  • Attribute drafts
  • Returns and resubmission
  • Bulk decisions

Assignments and deadlines

  • Delegation and reassignment
  • SLAs and escalation

Reading and authorization

  • Eligibility and authorization
  • Queries and timelines
  • Events and listeners
  • Notifications
  • Notification content

Operations

  • Workflow definitions
  • Audit trail and evidence
  • Console commands
  • Testing
  • Troubleshooting

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Workflows and stages
  • Choosing the workflow
  • Approvers and resolvers
  • Requests and state
  • Conditional stages

Approval flow

  • Submitting for approval
  • Recording decisions
  • Attribute drafts
  • Returns and resubmission
  • Bulk decisions

Assignments and deadlines

  • Delegation and reassignment
  • SLAs and escalation

Reading and authorization

  • Eligibility and authorization
  • Queries and timelines
  • Events and listeners
  • Notifications
  • Notification content

Operations

  • Workflow definitions
  • Audit trail and evidence
  • Console commands
  • Testing
  • Troubleshooting

byrcsc/laravel-approval · 1.x

Delegation and reassignment.

Delegate, reassign, or resync approvers without erasing the original assignment.

Each operation that moves an assignment answers a different question.

OperationQuestionWho performs it
delegate()"Someone else should decide this one"The approver
grantDelegation()"I am on holiday from Monday to Friday"The approver
reassign()"They left the company"An administrator
resync()"The whole department reorganized"An operator

Every one of them keeps the original approver on the record where it matters, because who was asked is part of the history.

Delegation

An approver hands their own assignment on the waiting stage to somebody else:

Approval::delegate($request, $financeUser, $deputy, 'On leave this week');

Delegation keeps the same assignment and changes only who may fill it. The approver_* columns keep naming the original approver, and delegated_to_* names the delegate. The stage's approval count does not change, because one holder leaves and one holder arrives.

Afterwards, the request appears in exactly one inbox, the delegate's.

Delegation eligibility

Check eligibility before you render a delegate control:

$request->canBeDelegatedBy($approver);                       // bool
$request->canBeDelegatedBy($approver, $deputy);              // including the target
Approval::delegationEligibility($request, $approver, $deputy);

The eligibility object carries eligible, reason, the stage, its progress, and the assignment. The refusal reasons are:

ReasonMeaning
RequestClosedThe request is no longer pending
NoActiveStageNothing is waiting
NotAnApproverThis actor holds no assignment on the active stage
AssignmentDelegatedTheir assignment already belongs to a delegate
DecisionAlreadyRecordedThey have already decided
ApproverMissingThe approver record no longer exists
CannotDelegateToSelfThe delegate is the approver
DelegateAlreadyAssignedThe delegate already holds an assignment on the stage
DelegateMissingThe delegate record does not exist

DelegateAlreadyAssigned stops a delegation from reducing the number of distinct people a stage is waiting on.

ApprovalDelegated fires with the assignment, the approver, and the delegate.

Standing delegations

A standing delegation covers a window of time instead of one request. While the window is open, every undecided assignment the approver holds on an active stage goes to the delegate:

$delegation = Approval::grantDelegation(
    $financeUser,
    $deputy,
    now()->next('Monday'),
    now()->next('Monday')->addWeek(),
    'Annual leave',
);

A null start opens the window now. A null end leaves it open until you revoke it. To end a window early, revoke it:

Approval::revokeDelegation($delegation);

Each routed assignment works the same way as a hand delegation. delegated_to_* names the delegate, approver_* keeps naming the approver, and the request appears only in the delegate's inbox. The assignment also records the delegation that routed it in delegation_id, so the package can return it to the approver when the window closes.

When assignments move

Assignments move at these points:

  • When you grant a delegation whose window is already open.
  • When you revoke a delegation.
  • When a stage becomes active while the approver's window is open.
  • When approval:delegations runs.

Only approval:delegations needs a scheduler. A window that opens or closes later moves nothing until approval:delegations runs, so schedule it when you grant windows in advance or with an end date:

Schedule::command('approval:delegations')->everyFifteenMinutes();

The command is safe to run as often as you like. Approval::applyDelegations() does the same work and returns how many assignments moved.

When a window closes, each undecided assignment it routed goes back to the approver. A decision the delegate already recorded stays as it was recorded.

What routing skips

Routing leaves an assignment with its approver in these cases:

  • The assignment is decided. A decision is history.
  • The assignment was delegated by hand. It stays where the approver put it, and a window that closes later does not move it back.
  • The delegate requested the approval. Moving it would let them approve their own request.
  • The delegate already holds an assignment on the stage. Moving it would let one person decide twice.

Delegations do not chain. A delegate who has their own window open keeps what was routed to them.

Granting rules

An approver has at most one delegation covering any moment, so the delegate an assignment goes to never depends on row order. grantDelegation() throws DelegationException when:

CheckMessage
The delegations migration has runNames the commands to publish and run it
The delegate is not the approvercannot delegate their approval to themselves
Both models existno longer exists
The end is after the startA delegation cannot end before it starts.
No current delegation overlaps the windowalready has a delegation covering part of this window

To change a window, revoke it and grant a new one. Revoking a delegation twice also throws.

List an approver's delegations that have not ended or been revoked, earliest first:

Approval::delegationsFrom($financeUser)->get();

Revoking stamps revoked_at on the ApprovalDelegation row instead of deleting it, so the window that routed an assignment stays readable.

Events and history

Each move is a line in the request's audit trail. Routing writes a delegated action attributed to the approver, with the delegation's comment. Handing back writes a delegation_ended action with no actor.

Routing on grant or from the scheduled command fires ApprovalDelegated. Routing on stage activation fires no event of its own, because ApprovalSubmitted or StageCompleted already tells the delegate. Handing back fires DelegationEnded with the assignment, the approver, and the delegate.

Reassignment

Administrative reassignment rewrites which approver an assignment belongs to:

Approval::reassign($request, $assignment, $newApprover, $administrator, 'Left the company');

Unlike delegation, reassignment works on any undecided stage, not only the active one. You can replace a stage three approver while stage one is still running.

Reassignment requires an attributable actor. ApproverReassigned fires with the assignment, the previous holder, the new holder, and the operator. The previous holder is null when someone has deleted the record that held the assignment. A deleted approver record is the most common reason to reassign.

Resynchronizing a live request

Approver snapshots go stale on purpose, because a fixed snapshot is what makes an audit trail mean something. Resync is the supported way to update a snapshot. Use it when a reorganization has left half a department's pending requests pointing at people who no longer approve anything.

$changes = Approval::resync($request, $operator);
$changes = Approval::resync($request, $operator, prune: true);

The console command is the intended path:

php artisan approval:resync 01JBK… --operator=42
php artisan approval:resync 01JBK… --operator="App\Models\Staff:7" --prune

--operator is required, because shell history does not attribute the change to a person.

What resync touches

  • Only undecided stages. A stage that has been decided is history.
  • Never an assignment that has decided or been delegated. Both are records of something a person did, and a resync is a correction to the future, not to the past.
  • Never a request stage whose workflow stage was deleted. Resync has no current stage to compare it against, so the snapshot runs as it always would have.

Pruning is off by default. Adding the people who should now be asked is almost always safe. Removing the ones who should not is a judgement about whether their opinion still counts, and that judgement belongs to whoever runs the command.

The return value

[
    1 => ['added' => ['App\Models\User:9'], 'removed' => []],
    2 => ['added' => [], 'removed' => ['App\Models\User:4']],
]

The array keys are stage sequence numbers. An empty array means the request already matched its workflow.

ApproversResynced fires with the changes, the operator, and the assignment rows that were created. A resync can satisfy the active stage's threshold after pruning holders who had not responded. In that case, the stage completes immediately.

Stranded stages

None of delegate(), reassign(), or resync() may leave a stage worse off than before. A stage is stranded when the assignments whose holders still exist are fewer than the approvals the stage requires. Nobody can finish a stranded stage.

The package judges each mutation by whether it increased the deficit, not by whether the result is perfect. Refusing every mutation that leaves a stage stranded would also refuse the mutations that repair it. For example, you replace two dead assignments one at a time. A strict rule would roll back the first replacement because the second assignment is still dead.

A mutation that worsens the deficit raises InvalidWorkflowException::livingHoldersBelowRequired(). A mutation that repairs a stage clears its stranded_at mark.

Find stranded stages before somebody reports them:

php artisan approval:doctor

StageStranded fires when the escalation sweep first detects one. See SLAs and escalation.

What to read next

  • Console commands to schedule standing delegations.
  • Approvers and resolvers to understand the assignments changed by these operations.
  • Eligibility and authorization to authorize delegation and reassignment.
  • SLAs and escalation to detect stranded stages.
PreviousBulk decisionsNextSLAs and escalation
View source

On this page

  1. Delegation
  2. Delegation eligibility
  3. Standing delegations
  4. When assignments move
  5. What routing skips
  6. Granting rules
  7. Events and history
  8. Reassignment
  9. Resynchronizing a live request
  10. What resync touches
  11. The return value
  12. Stranded stages
  13. What to read next