›
byrcsc/laravel-approval · 1.x
Choose which workflow a model uses when several workflows could apply.
You can name a workflow when submitting a model or let Laravel Approval choose one. Automatic selection lets the same model follow different processes. For example, a small purchase order can use a short workflow while a larger one also needs executive approval.
Approval::submit($order); // work it out
Approval::submit($order, 'purchase-order-executive'); // use this oneThe argument always wins. When you name a workflow, Laravel Approval does not check its selection condition or compare its priority with other workflows.
The active check still applies. Submission refuses an inactive workflow with
InvalidWorkflowException::inactive(), whether you named the workflow or the
package selected it. Naming a workflow skips selection, not validation.
Reactivate a disabled workflow before you submit a request to it.
The package never replaces a named workflow with another workflow. If it cannot run, submission throws an exception.
With nothing named, the package gathers candidates in this order:
approvalWorkflow() method, returning an ApprovalWorkflow or a
slug;approval.workflows config map, keyed by class name or morph alias;approvable_type column names the model.// 1. On the model
public function approvalWorkflow(): string
{
return 'purchase-order';
}
// 2. In config/approval.php
'workflows' => [
App\Models\PurchaseOrder::class => 'purchase-order',
],
// 3. On the workflow row
ApprovalWorkflow::create([
'slug' => 'purchase-order',
'approvable_type' => PurchaseOrder::class,
]);Use the config map for models you do not own, or to keep every mapping in one
file. Use approvable_type when a model has more than one workflow, since it is
the only source that can name several.
A slug that names no workflow raises WorkflowNotFoundException. The package
does not skip it. A typo in a model or in configuration is a mistake, not a
reason to move on to the next source.
The config map accepts a list. Each entry is its own step in the preference order:
'workflows' => [
App\Models\PurchaseOrder::class => [
'purchase-order-capital',
'purchase-order',
],
],The first workflow that applies wins. The package tries the second only when the first is inactive or its condition rules it out. Each named entry has its own level of specificity, so the list has a clear order and cannot be ambiguous.
Workflows registered by approvable_type are different. They share the last
position, so conditions and priority choose between them as a set.
If a workflow is reachable from more than one source, the package considers it only at its most specific position. Listing a workflow in config and also registering it by type does not give it two chances.
The package then filters the candidates. It removes inactive workflows and
workflows whose condition does not apply to the current submission. It orders
the remaining candidates by source, and then by priority, highest first.
When no candidate remains, submission raises WorkflowNotFoundException.
Register each workflow against the model type and give it the condition that selects it. Priority orders the workflows that remain:
ApprovalWorkflow::create([
'name' => 'Executive purchase orders',
'slug' => 'purchase-order-executive',
'approvable_type' => PurchaseOrder::class,
'condition' => AmountAbove::class,
'condition_config' => ['amount' => 10_000],
'priority' => 10,
]);
ApprovalWorkflow::create([
'name' => 'Standard purchase orders',
'slug' => 'purchase-order-standard',
'approvable_type' => PurchaseOrder::class,
'priority' => 0,
]);A ₱25,000 order matches both, and the executive workflow's higher priority wins. A ₱400 order fails the condition, so the standard workflow is the only candidate. The unconditional low-priority workflow is the fallback. Without it, a submission that matches no condition has no workflow.
A workflow condition implements WorkflowCondition:
use ByRcsc\LaravelApproval\Contracts\WorkflowCondition;
use ByRcsc\LaravelApproval\Models\ApprovalWorkflow;
use Illuminate\Database\Eloquent\Model;
final class AmountAbove implements WorkflowCondition
{
public function applies(
ApprovalWorkflow $workflow,
Model $approvable,
?Model $requester,
array $config,
): bool {
return $approvable->amount > ($config['amount'] ?? 0);
}
}Laravel Approval resolves conditions from the container, so constructor injection works.
Take care with $approvable and $requester.
$approvable is the proposal, not the record. When the submission has an
attribute draft, the package fills the drafted values onto a clone before the
condition runs. Conditional stages work the same way.
Take a purchase order saved at ₱4,000 and submitted with a draft that raises it
to ₱20,000. The condition sees ₱20,000, because the request asks for approval
of the proposed value.
$requester may be null. The requester is null when nobody is submitting
yet. For example, it is null when your application asks which workflows a
record could be submitted against. A condition that uses the requester
without a null check works at submission and fails in your workflow picker.
public function applies(
ApprovalWorkflow $workflow,
Model $approvable,
?Model $requester,
array $config,
): bool {
// Nobody is submitting yet: offer the workflow rather than hiding it.
return $requester?->isContractor() ?? true;
}Two workflows that are equally specific and equally prioritized raise
AmbiguousWorkflowException:
More than one approval workflow applies to [App\Models\PurchaseOrder] at
priority 0: [purchase-order-capital], [purchase-order-operating]. Give one of
them a higher priority, narrow them with a condition, or name the workflow at
the call site.If the package picked one arbitrarily, insertion order would decide which workflow ran. The same record could then follow a different workflow tomorrow for no configured reason. The message names the fixes.
Only the type-registered position can hold more than one workflow, so ambiguity can arise only there.
Build a workflow picker from these queries:
Approval::workflowsFor($order);
// Collection of applicable workflows, best first. Nobody is submitting yet,
// so conditions receive a null requester.
Approval::workflowsFor($order, $requester, ['amount' => 20_000]);
// Applicable to this specific submission, draft and all.
Approval::workflowFor($order);
// The single workflow a submission would use. Raises on ambiguity.
Approval::workflows();
// Builder over every active workflow, ordered by name. The maintenance-screen
// query, not the one to offer a requester.workflowsFor() returns all matching workflows in order. workflowFor()
returns the first match. Use workflowsFor() to render a choice and
workflowFor() to preview automatic selection.
Pass the same draft to
workflowsFor()that you will pass tosubmit(). Conditions judge the proposal. If you compute the choice without the draft and then submit with it, submission can select a different workflow than the one you showed.
The fluent builder sets every selection key, so workflow selection stays under version control with the rest of the definition:
WorkflowBuilder::make('purchase-order-executive')
->forApprovable(PurchaseOrder::class)
->when(AmountAbove::class, ['amount' => 10_000])
->priority(10)
->stage('Finance', fn ($stage) => $stage->approver($cfo));forApprovable() accepts a model instance or a class-string. See
workflow definitions for the array equivalent and how
approval:sync reconciles changes.
The two condition types answer different questions and run at different times.
A workflow condition decides which workflow runs. The package checks it only while choosing a workflow, never for a workflow named at the call site. It runs before the request exists.
A stage condition decides whether a stage inside the running workflow
applies. It runs once, during submission. The package creates a stage that
fails its condition in the skipped state.
A workflow can use both. For example, a workflow condition selects the workflow for capital purchases, and stage conditions skip legal review inside it.