Browse documentationOpen

byrcsc/laravel-approval · 1.x

Quick start.

Build a two-stage purchase-order workflow, submit a draft, approve it through both stages, and render an approver inbox.

This walkthrough builds a two-stage workflow, submits a purchase order with a proposed amount held until approval, and takes it through to completion.

1. Make the model approvable

use ByRcsc\LaravelApproval\Concerns\Approvable;
use Illuminate\Database\Eloquent\Model;

class PurchaseOrder extends Model
{
    use Approvable;

    public function approvalWorkflow(): string
    {
        return 'purchase-order';
    }

    public function approvableAttributes(): array
    {
        return ['amount', 'supplier_id', 'notes'];
    }
}

approvalWorkflow() declares which workflow this model uses, so callers need not repeat the slug. approvableAttributes() is an allow-list for drafts; returning null — the default — allows every attribute.

2. Create the workflow

Workflows are ordinary Eloquent models, so workflow management screens stay part of your application:

use ByRcsc\LaravelApproval\Models\ApprovalWorkflow;

$workflow = ApprovalWorkflow::create([
    'name' => 'Purchase orders',
    'slug' => 'purchase-order',
]);

$finance = $workflow->stages()->create([
    'sequence' => 1,
    'name' => 'Finance',
    'required_approvals' => 1,
]);

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

$directors = $workflow->stages()->create([
    'sequence' => 2,
    'name' => 'Directors',
    'required_approvals' => 2,
]);

foreach ($board as $director) {
    $directors->approvers()->create([
        'approver_type' => $director->getMorphClass(),
        'approver_id' => $director->getKey(),
    ]);
}

required_approvals is how many approvals the stage needs. Omit it, or set it to null, and every resolved assignment must approve.

For workflows that belong in version control rather than in a management screen, see workflow definitions.

3. Submit

$order = PurchaseOrder::create([
    'supplier_id' => $supplier->id,
    'amount' => 4_000,
]);

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

The record already exists and its amount is still 4000. The proposed 10000 is held on the request until the workflow completes. Omit newAttributes entirely to submit the record itself rather than an edit to it.

Submission resolves the workflow from four places, most specific first: the argument, the model's approvalWorkflow(), the approval.workflows config map, then the workflows registered against the model's type. The requester resolves from the argument, the model's approvalRequester(), then the authenticated user.

4. Decide

use ByRcsc\LaravelApproval\Facades\Approval;

Approval::approve($request, $financeUser, 'Within the quarterly budget');

Stage one is satisfied, so stage two activates. Two directors must approve:

Approval::approve($request, $firstDirector);
Approval::approve($request, $secondDirector);

With the last stage satisfied, the draft is applied: $order->fresh()->amount is now 10000, and the request state is approved.

The other two decisions both require a reason:

Approval::reject($request, $financeUser, 'Over budget for this quarter');
Approval::returnForRevision($request, $financeUser, 'Attach the supplier quote');

5. Build an inbox

$requests = Approval::awaiting($user)
    ->withProgress()
    ->get();

awaiting() returns pending requests whose active stage is waiting on this user and which they have not already approved. withProgress() eager-loads the stages, assignments, and actions that progress summaries and timelines read.

6. Ask before you render

Never render an approve button on hope. The eligibility read model runs the same calculation the decision runs, and explains a refusal:

$eligibility = Approval::eligibility($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?->remainingApprovals;

Through the shipped policy, the same question reads as $user->can('approve', $request). See eligibility and authorization.

7. Show progress

$request = Approval::pendingFor($order);

$request?->currentStage()->name;        // "Directors"
$request?->pendingApprovers();          // holders who have not responded yet
$request?->stageProgress()?->approvalsReceived;
$request?->timeline();                  // one entry per stage, in order