›
›
›
  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

Eligibility and authorization.

Explain whether an actor can act and connect that result to Laravel authorization.

Eligibility is a read. It uses one calculation for a disabled button and for a decision about to be recorded, so the two can never disagree.

$eligibility = Approval::eligibility($request, $user);

if ($eligibility->eligible) {
    Approval::approve($request, $user);
}

What the result carries

$eligibility->eligible;        // bool
$eligibility->reason;          // null, or an Ineligibility enum
$eligibility->stage;           // the active stage, when the check got that far
$eligibility->stageProgress;   // its progress, same condition
$eligibility->assignment;      // the actor's assignment, when granted

The result includes the stage, its progress, and the assignment. A view can render a control, a reason for a disabled control, and the stage state from one call without repeating the same queries.

Refusal reasons

use ByRcsc\LaravelApproval\Enums\Ineligibility;
ReasonMeaning
RequestClosedThe request is in any state other than pending
NoActiveStageNo stage is currently waiting
NotAnApproverThis actor has no assignment on the active stage
AssignmentDelegatedTheir assignment now belongs to a delegate
DecisionAlreadyRecordedA decision has already been recorded for their assignment
SelfApprovalThey submitted it, and this workflow does not allow that

The order follows the check itself, from request state through to the active assignment. Each reason maps to the exception a decision would throw. See recording decisions.

RequestClosed also covers conflicted, and that case surprises people. A conflicted request is still unresolved, so you cannot submit the record again. The request is also no longer pending, so nobody can decide it. A conflicted request needs a conflict resolution, not an approval.

The short forms

Approval::canApprove($request, $user);              // bool
$request->canBeApprovedBy($user);                   // the same, from the request
$order->canBeApprovedBy($user);                     // via the model's open request

$order->canBeApprovedBy() returns false both for "not this user's stage" and for "no unresolved request". $order->approvalEligibility($user) distinguishes them by returning null in the second case.

Blade directives

Two Blade conditionals ask the same questions in a view:

@pendingApproval($order)
    <span>Awaiting approval</span>
@endpendingApproval

@canApprove($order)
    <button>Approve</button>
@endcanApprove

@pendingApproval is true when the record has an unresolved request. @canApprove takes a request or an approvable record, and checks the signed-in user. Pass a second argument to check another user:

@canApprove($request, $reviewer)

@canApprove is false when nobody is signed in or the record has no unresolved request.

Withdrawal and delegation

Both are request-level questions, so ask them on the request:

$request->canBeWithdrawnBy($requester);
$request->canBeDelegatedBy($approver, $deputy);

Approval::withdrawalEligibility($request, $actor);
Approval::delegationEligibility($request, $actor, $delegate);

Withdrawal refuses with RequestClosed or NotRequester. For the delegation refusal reasons, see delegation and reassignment.

The shipped policy

ApprovalRequestPolicy follows Laravel's discovery convention, so you do not need to register it:

$user->can('approve', $request);
$this->authorize('withdraw', $request);
AbilityRule
viewThe actor is involved in the request
approveApproval::canApprove()
rejectApproval::canApprove()
returnForRevisionApproval::canApprove()
withdrawApproval::canWithdraw()
delegateApproval::canDelegate()
cancelDenied, host-owned

Approve, reject, and return share one gate because each one is a decision on the same assignment.

view is deliberately wider

$request->involves($user);

involves() returns true when the actor requested the request or appears on any assignment. That includes delegates and stages already decided. An approver who decided stage one last week is still a participant, and a read authorization check needs to know that. Reading a decision is not making one.

If your application needs a wider audience, such as auditors or department heads, widen view in your own policy.

cancel denies by default

Administrative cancellation belongs to your application. Granting it through requester authority would be wrong, so the shipped policy returns false and your application grants it.

Extending the policy

Extend the shipped policy rather than replace it, so your rules add to the package's rules instead of replacing them:

use ByRcsc\LaravelApproval\Policies\ApprovalRequestPolicy as BasePolicy;

final class RequestPolicy extends BasePolicy
{
    public function view(Model $user, ApprovalRequest $request): bool
    {
        return parent::view($user, $request) || $user->isAuditor();
    }

    public function cancel(Model $user, ApprovalRequest $request): bool
    {
        return $user->hasRole('approval-administrator');
    }
}

Register it the ordinary way. Explicit registration takes precedence over discovery:

Gate::policy(ApprovalRequest::class, RequestPolicy::class);

The engine checks anyway

The eligibility check is the minimum, not the whole authorization story. The engine runs it inside every decision regardless of what a gate said. The approver list is the engine's own data, and nothing outside the engine can check it.

Your policies and gates run in addition to that check. A gate that passes cannot make an unassigned actor's approval succeed.

Administrative cancellation

Approval::cancel($request, $administrator, 'Duplicate of PO-1002');

Cancellation requires an attributable actor and runs no eligibility check of its own. Your policy's cancel ability authorizes it, and that ability denies by default. The request state becomes cancelled, and ApprovalCancelled fires with the actor and comment.

Cancellation and withdrawal both close a pending request. Withdrawal belongs to the requester. Cancellation belongs to somebody else.

What to read next

  • Recording decisions to see which exceptions match eligibility results.
  • Delegation and reassignment for assignment-change checks.
  • Queries and timelines to load requests before checking authorization.
PreviousSLAs and escalationNextQueries and timelines
View source

On this page

  1. What the result carries
  2. Refusal reasons
  3. The short forms
  4. Blade directives
  5. Withdrawal and delegation
  6. The shipped policy
  7. view is deliberately wider
  8. cancel denies by default
  9. Extending the policy
  10. The engine checks anyway
  11. Administrative cancellation
  12. What to read next