›
byrcsc/laravel-approval · 1.x
Store workflows in source control and preview changes before syncing database rows.
Workflow rows are freely editable, which suits an application with a workflow
management screen. An application whose approval rules belong in code instead
declares them as definitions and reconciles them with approval:sync.
// config/approval.php
'definitions' => [
'purchase-order' => [
'name' => 'Purchase orders',
'description' => 'Finance, then the board above ₱100,000.',
'stages' => [
[
'name' => 'Finance',
'required_approvals' => 1,
'sla_minutes' => 1440,
'approvers' => [
['resolver' => App\Approvals\FinanceTeam::class],
],
],
[
'name' => 'Board',
'required_approvals' => 2,
'rejection_policy' => 'any_rejection_vetoes',
'condition' => App\Approvals\AmountExceeds::class,
'condition_config' => ['threshold' => 100_000],
'approvers' => [
['resolver' => App\Approvals\BoardMembers::class],
],
],
],
],
],The array key becomes the slug when the definition does not carry one. Sequences default to array order and must be contiguous from one.
A definition can also carry the keys that decide when it is chosen, which is what puts workflow selection under version control alongside the stages:
'purchase-order-executive' => [
'name' => 'Executive purchase orders',
'approvable_type' => App\Models\PurchaseOrder::class,
'condition' => App\Approvals\AmountAbove::class,
'condition_config' => ['amount' => 10_000],
'priority' => 10,
'active' => true,
'stages' => [/* … */],
],| Key | |
|---|---|
approvable_type | Model class or morph alias this workflow is registered against. |
condition | A WorkflowCondition class deciding whether it applies. |
condition_config | Configuration passed to that condition. |
priority | Higher wins among equally specific workflows. Defaults to 0. |
active | Defaults to true. An inactive workflow is never chosen. |
condition is validated at definition time: a class that does not exist or does
not implement WorkflowCondition raises
InvalidWorkflowDefinitionException, as does a negative priority.
WorkflowBuilder produces the same structure with a typed API:
use ByRcsc\LaravelApproval\Definitions\WorkflowBuilder;
use ByRcsc\LaravelApproval\Enums\EscalationAction;
use ByRcsc\LaravelApproval\Enums\RejectionPolicy;
WorkflowBuilder::make('purchase-order', 'Purchase orders')
->description('Finance, then the board above ₱100,000.')
->settings(['allow_self_approval' => false])
->stage('Finance', fn ($stage) => $stage
->minimumApprovals(1)
->sla(1440)
->remindersEvery(240)
->escalate(EscalationAction::Escalate, ['resolver' => ManagerOf::class])
->resolver(FinanceTeam::class))
->stage('Board', fn ($stage) => $stage
->minimumApprovals(2)
->rejectionPolicy(RejectionPolicy::AnyRejectionVetoes)
->when(AmountExceeds::class, ['threshold' => 100_000])
->approver($chairperson)
->resolver(BoardMembers::class));| Stage method | Sets |
|---|---|
minimumApprovals(?int) | required_approvals; null means all |
rejectionPolicy(RejectionPolicy) | rejection_policy |
approver(Model) | A concrete approver entry |
resolver(string, array) | A resolver entry |
sla(?int) | sla_minutes |
remindersEvery(?int) | remind_every_minutes |
escalate(EscalationAction, array) | Escalation strategy and config |
when(string, array) | Condition class and config |
Stages take their sequence from the order they are declared.
The builder has workflow-level methods for the selection keys above:
WorkflowBuilder::make('purchase-order-executive', 'Executive purchase orders')
->forApprovable(PurchaseOrder::class)
->when(AmountAbove::class, ['amount' => 10_000])
->priority(10)
->stage('Finance', fn ($stage) => $stage->approver($cfo));| Workflow method | Sets |
|---|---|
description(?string) | description |
active(bool) | active; defaults to true |
forApprovable(Model|string) | approvable_type |
when(string, array) | condition and condition_config |
priority(int) | priority |
settings(array) | settings |
when() appears in both tables and means different things by level. On the
workflow it decides whether this workflow runs at all; on a stage it decides
whether that stage applies once the workflow is already running. See
choosing the workflow and
conditional stages.
Builders can go straight into the config array, or be passed to
Approval::syncDefinitions() directly.
Key every definition by slug.
approval:syncreadsdefinitionsas a string-keyed map. An unkeyed list,'definitions' => [WorkflowBuilder::make('purchase-order')], is silently dropped, and the command reports everything already in sync.
Definitions are validated as they are built, so a mistake surfaces where it was
written rather than at the next submission. InvalidWorkflowDefinitionException
covers:
ApproverResolver;StageCondition;Concrete approvers are checked against the database, so an approver that has since been deleted fails the sync rather than producing a workflow nobody can satisfy.
php artisan approval:sync --dry-run create ......... workflow:purchase-order
create ......... stage:purchase-order:1
create ......... approver:purchase-order:1:resolver:App\Approvals\FinanceTeam:null
Dry run complete; no approval workflow records were changed.Nothing is written. Run it in CI to fail a build whose definitions drifted from the deployed workflows.
php artisan approval:syncThe whole reconciliation runs in one transaction. The command exits non-zero when anything was refused.
Approval::syncDefinitions($definitions, $dryRun) is the programmatic form and
returns a WorkflowSyncResult:
$result->created; // list<string>
$result->updated;
$result->unchanged;
$result->refused;
$result->dryRun;
$result->successful(); // refused === []
$result->changed(); // created or updated is non-emptyAdditive and supported configuration changes are applied. Destructive drift is refused rather than performed:
Those are changes with consequences the package cannot judge, a stage removed from a definition might be a deliberate simplification or a typo in a config file. Make them deliberately, against the workflow rows.
A refusal aborts the whole sync before writing anything, so a partially applied reconciliation is not a state you can end up in.
Syncing edits workflow rows. Requests already running hold their own snapshot and are unaffected. This includes requests on a workflow the sync changed.
To push a workflow change into a live request, that is a separate,
attributable act: Approval::resync().
A workable order for an application whose workflows live in code:
php artisan approval:sync --dry-run # in CI, on the pull request
php artisan migrate --force
php artisan approval:sync # after deploy
php artisan approval:status # confirm nothing needs attention