›
byrcsc/laravel-approval · 1.x
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);
}$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 grantedThe 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.
use ByRcsc\LaravelApproval\Enums\Ineligibility;| Reason | Meaning |
|---|---|
RequestClosed | The request is in any state other than pending |
NoActiveStage | No stage is currently waiting |
NotAnApprover | This actor has no assignment on the active stage |
AssignmentDelegated | Their assignment now belongs to a delegate |
DecisionAlreadyRecorded | A decision has already been recorded for their assignment |
SelfApproval | They 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.
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.
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.
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.
ApprovalRequestPolicy follows Laravel's discovery convention, so you do not
need to register it:
$user->can('approve', $request);
$this->authorize('withdraw', $request);| Ability | Rule |
|---|---|
view | The actor is involved in the request |
approve | Approval::canApprove() |
reject | Approval::canApprove() |
returnForRevision | Approval::canApprove() |
withdraw | Approval::canWithdraw() |
delegate | Approval::canDelegate() |
cancel | Denied, 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 defaultAdministrative cancellation belongs to your application. Granting it through requester authority would be wrong, so the shipped policy returns false and your application grants it.
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 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.
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.