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

Submitting for approval.

Create a request after resolving its workflow, requester, draft, and approvers.

Submission is explicit. The package does not intercept saves to decide that something needs approval; you say so.

$request = $order->submitForApproval(
    workflow: 'purchase-order',
    newAttributes: ['amount' => 10_000],
);

The facade takes the same call with the model first:

use ByRcsc\LaravelApproval\Facades\Approval;

$request = Approval::submit($order, 'purchase-order', ['amount' => 10_000]);

$model->submitForApproval(...) and the guard escape hatch withoutApprovalGuard() are the only model-side aliases the package ships. Every other command lives on the facade.

Signature

Approval::submit(
    Model $approvable,
    ApprovalWorkflow|string|null $workflow = null,
    ?array $newAttributes = null,
    ?Model $requester = null,
    ?string $comment = null,
    array $metadata = [],
): ApprovalRequest;
ArgumentNotes
$approvableMust already be persisted
$workflowA slug, a workflow model, or null to resolve it
$newAttributesA draft, or null to submit the record itself
$requesterNull to resolve it
$commentRecorded on the submitted audit action
$metadataRecorded on the same action; the engine adds the workflow slug

Record-level versus draft submissions

Passing null for $newAttributes creates a record-level request: the already-saved record is what needs approval, and nothing is applied when the request completes.

Passing an array creates a draft request: those values are held on the request and written to the record only after the final approval. See attribute drafts.

An empty array is neither, and raises InvalidDraftException::emptyDraft(). Null and [] are deliberately different: one means "approve the record", the other is almost always a bug.

Workflow resolution

The $workflow argument wins outright: a workflow named at the call site is used as named, active or not.

With nothing named, candidates are gathered from three places, most specific first, the model's approvalWorkflow() method, the approval.workflows config map, and every workflow whose approvable_type column names the model:

// config/approval.php
'workflows' => [
    App\Models\PurchaseOrder::class => 'purchase-order',
],

Whatever is gathered is then filtered. An inactive workflow is out, and so is one whose condition rules it out for this submission. What survives is ordered by where it came from and then by priority, and the first is used. When nothing survives, submission throws WorkflowNotFoundException.

That is the short version, and it is all you need while a model has one workflow. Choosing the workflow covers registering several against one model, writing conditions, and how ties are refused.

Requester resolution

The same shape, three sources:

  1. the $requester argument;
  2. the model's approvalRequester() method;
  3. the authenticated user.

When none answers, submission throws MissingRequesterException. An unattributed request cannot be withdrawn by its requester or meaningfully audited, so the package refuses to create one.

class PurchaseOrder extends Model
{
    use Approvable;

    public function approvalRequester(): ?Model
    {
        return $this->buyer;
    }
}

Returning null falls through to the authenticated user.

What submission validates

Everything that can fail is settled before the transaction opens, so a misconfigured stage three cannot leave a half-built request behind. In order:

CheckFailure
The record is persistedInvalidDraftException::unsavedApprovable()
No unresolved request exists for the recordApprovalInProgressException
The draft is not an empty arrayInvalidDraftException::emptyDraft()
Every drafted key is allowedInvalidDraftException::undraftableAttributes()
The workflow is activeInvalidWorkflowException::inactive()
The workflow has stagesInvalidWorkflowException::hasNoStages()
Each stage's rejection policy is knownInvalidWorkflowException::unknownRejectionPolicy()
Each condition class is a StageConditionInvalidWorkflowException::notACondition()
Each resolver class is an ApproverResolverInvalidWorkflowException::notAResolver()
Resolvers return persisted modelsInvalidWorkflowException::resolverReturnedUnsavedModel()
An applicable stage resolved some approversInvalidWorkflowException::stageHasNoApprovers()
Each threshold is reachableInvalidWorkflowException::approvalRuleUnreachable()

A workflow that cannot produce a decision fails at submit time rather than stalling at stage three weeks later.

What submission writes

Inside the transaction, with a row lock on the record:

  • the request, in pending, with the draft and the original values it was drafted against;
  • one request stage per workflow stage, carrying the full snapshot and the evaluated condition result;
  • one assignment per resolved approver;
  • a submitted audit action, attributed to the requester;
  • a stage_skipped action for each stage a condition turned off.

The first stage whose condition passed is then activated, which is what sets its deadline. ApprovalSubmitted fires with that stage.

Withdrawing

A requester withdraws their own pending request:

Approval::withdraw($request, $requester, 'Raised in error');

Only the request's requester may withdraw it. Ask first when rendering a control:

$request->canBeWithdrawnBy($user);                  // bool
Approval::withdrawalEligibility($request, $user);   // with a reason

The two refusal reasons are WithdrawalIneligibility::RequestClosed and NotRequester.

Administrative cancellation is a separate, host-authorized operation, see eligibility and authorization.

Attaching evidence

Either party may put a file in evidence against an open request:

use ByRcsc\LaravelApproval\Data\AttachmentEvidence;

Approval::attach(new AttachmentEvidence(
    request: $request,
    disk: 's3',
    path: 'quotes/PO-1001.pdf',
    checksum: hash_file('sha256', $localPath),
    name: 'Supplier quote.pdf',
    mimeType: 'application/pdf',
    size: $bytes,
    uploader: $requester,
));

The package records a reference and a digest, never bytes. Storing and serving the file stays with your application. See audit trail and evidence.

What to read next

  • Choosing the workflow for automatic workflow selection.
  • Attribute drafts for proposed attribute rules and stale checks.
  • Recording decisions to move the new request through its stages.
PreviousConditional stagesNextRecording decisions
View source

On this page

  1. Signature
  2. Record-level versus draft submissions
  3. Workflow resolution
  4. Requester resolution
  5. What submission validates
  6. What submission writes
  7. Withdrawing
  8. Attaching evidence
  9. What to read next