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

Approvers and resolvers.

Assign a specific approver or resolve several approvers from an application rule.

An approver entry points to one saved model. Only that model can respond to the assignment.

$stage->approvers()->create([
    'approver_type' => $financeUser->getMorphClass(),
    'approver_id' => $financeUser->getKey(),
]);

A Team, Role, Position, or other container model is not expanded into its members. Assigning a team assigns the team record itself, and only something acting as that team can approve. To assign the members, use an approver resolver.

Approver columns are polymorphic, so an assignment can name any model. The package never assumes your users are App\Models\User.

Approver resolvers

A resolver converts an application rule into a list of approver models. It runs when a request is submitted:

use ByRcsc\LaravelApproval\Contracts\ApproverResolver;
use ByRcsc\LaravelApproval\Data\StageSnapshot;
use Illuminate\Database\Eloquent\Model;

final class FinanceTeamMembers implements ApproverResolver
{
    public function resolve(
        StageSnapshot $stage,
        Model $approvable,
        Model $requester,
        array $config,
    ): iterable {
        return Team::findOrFail($config['team'])->members;
    }
}

Register it on the stage instead of a concrete approver:

$stage->approvers()->create([
    'resolver' => FinanceTeamMembers::class,
    'resolver_config' => ['team' => $financeTeam->getKey()],
]);

An approver entry names either a model or a resolver, never both. The engine enforces that, since no portable database constraint expresses it.

Rules resolvers must follow

  • Return persisted models. An assignment records an exact actor identity, so an unsaved model raises InvalidWorkflowException.
  • Be free of side effects. Every stage is planned before the request is written, so a resolver can run and the submission still be abandoned because a later stage turns out to be invalid.
  • Tolerate an empty answer. Returning nothing is allowed; the engine decides whether that is fatal. It is fatal for a stage whose condition passed, because a stage with no approvers can never complete.

Resolvers are resolved from the container, so constructor injection works.

Resolvers run exactly once

A resolver is a question asked at a point in time, not a live query. Its answer is written into the stage's assignments and never consulted again, so an approver list that changes tomorrow does not rewrite what was asked today.

Getting a stale assignment fixed is deliberate work: delegate it, reassign it, or resync the request.

Duplicates collapse

Two entries that name the same record are one assignment, however differently they were reached. The same person returned by two resolvers on one stage produces one assignment. Duplicate assignments do not increase the number of approvals available to the stage.

Shipped resolvers

StaticUsersResolver

A list of primary keys, useful when several stages share one list and you would rather edit it in one place:

use ByRcsc\LaravelApproval\Resolvers\StaticUsersResolver;

$stage->approvers()->create([
    'resolver' => StaticUsersResolver::class,
    'resolver_config' => ['ids' => [4, 11], 'model' => Team::class],
]);

model is optional and defaults to approval.user_model.

CallbackResolver

Delegates to a class the application already has, so an existing ManagerLookup::forEmployee() becomes an approver rule without being rewritten around this package's interface:

use ByRcsc\LaravelApproval\Resolvers\CallbackResolver;

$stage->approvers()->create([
    'resolver' => CallbackResolver::class,
    'resolver_config' => ['callback' => 'App\Org\ManagerLookup@forEmployee'],
]);

The class comes out of the container, so its own dependencies are injected, and the method receives the same four arguments an ApproverResolver does. Omit @method to call __invoke. Anything the method returns that is not an Eloquent model is dropped.

Reach for this when the logic already exists somewhere. When it does not, write the resolver: an implementation of the interface is about fifteen lines and says what it is in its own name.

Assignment sources

Every assignment records how its holder came to hold it:

SourceMeaning
assignedCopied from a concrete approver entry at submission
resolvedProduced by a resolver at submission
delegatedAn approver handed the assignment on themselves
reassignedAn administrator rewrote who the assignment belongs to
escalatedAn overdue stage's escalation policy added the approver
resyncedAn administrative resync added the approver

assigned and resolved are what the submit-time snapshot produced; the rest record a deliberate intervention on a request already in flight.

Self-approval

A requester may not approve their own request unless the policy allows it. Enable it globally:

APPROVAL_ALLOW_SELF_APPROVAL=true

Or per workflow, which takes precedence:

$workflow->update(['settings' => ['allow_self_approval' => true]]);

A denied attempt surfaces as Ineligibility::SelfApproval from an eligibility check, or SelfApprovalException from a decision.

Stranded stages

An approval rule counts assignments. If assigned models are deleted, a stage can require more approvals than the remaining people can provide. The stage can no longer finish without administrative action.

approval:doctor finds them:

php artisan approval:doctor

Delegation and reassignment are judged against whether they made a stage's deficit worse, not against perfection, so an already-stranded stage stays open to the interventions that are trying to repair it.

What to read next

  • Workflows and stages to set approval thresholds for resolved assignments.
  • Delegation and reassignment to change assignments on a live request.
  • Eligibility and authorization to check whether an assigned actor can decide.
  • Testing to use resolvers and assignments in application tests.
PreviousChoosing the workflowNextRequests and state
View source

On this page

  1. Approver resolvers
  2. Rules resolvers must follow
  3. Resolvers run exactly once
  4. Duplicates collapse
  5. Shipped resolvers
  6. StaticUsersResolver
  7. CallbackResolver
  8. Assignment sources
  9. Self-approval
  10. Stranded stages
  11. What to read next