›
byrcsc/laravel-approval · 1.x
Store workflows in source control and preview changes before syncing database rows.
Editable workflow rows suit an application with a workflow management screen.
If your approval rules belong in code, declare them as definitions instead.
Then run approval:sync to update the workflow rows to match.
// 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 has no slug.
Sequences default to array order and must be contiguous from one.
A definition can also include the keys that decide when the package chooses it. These keys put workflow selection under version control with 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. |
The package validates condition at definition time. A class that does not
exist or does not implement WorkflowCondition raises
InvalidWorkflowDefinitionException. A negative priority raises the same
exception.
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 you declare them.
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 at each 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 running. See
choosing the workflow and
conditional stages.
You can put builders in the config array or pass them to
Approval::syncDefinitions().
Key every definition by slug.
approval:syncreadsdefinitionsas a string-keyed map. The command drops an unkeyed list, such as'definitions' => [WorkflowBuilder::make('purchase-order')], without a warning and reports everything already in sync.
The package validates definitions as you build them. A mistake fails where you
wrote it, not at the next submission. InvalidWorkflowDefinitionException
covers:
ApproverResolver;StageCondition;The package checks concrete approvers against the database. If an approver has been deleted, the sync fails instead of producing a workflow that nobody can approve.
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.The dry run writes nothing. Run it in CI to fail a build when the definitions no longer match the deployed workflows.
php artisan approval:syncThe sync runs in one database transaction. The command exits with a non-zero code when it refuses any change.
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-emptyThe sync applies additive and supported configuration changes. It refuses these destructive changes:
The package cannot judge the consequences of these changes. A stage removed from a definition might be a deliberate simplification or a typo in a config file. Make these changes yourself, against the workflow rows.
A refusal stops the whole sync before it writes anything. The sync never applies only part of the definitions.
The sync edits workflow rows. Requests already running use their own snapshot and do not change, even when the sync changed their workflow.
To apply a workflow change to a running request, call
Approval::resync(). A resync
is a separate action that records who performed it.
For an application that keeps its workflows in code, run the commands in this order:
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