NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Approvals for Laravel

Actions & DTOs

The actions in RoundlyConsulting\Approvals\Actions are the code the facade and the manager run — single-purpose classes with execute() for composing into your own actions, jobs and commands. The facade-to-action map is under DI and actions.

Action signatures

use RoundlyConsulting\Approvals\Actions\ApproveAction;
use RoundlyConsulting\Approvals\Actions\CancelApprovalAction;
use RoundlyConsulting\Approvals\Actions\ExpireApprovalsAction;
use RoundlyConsulting\Approvals\Actions\RejectAction;
use RoundlyConsulting\Approvals\Actions\RequestApprovalAction;
use RoundlyConsulting\Approvals\Actions\ToggleApprovalAction;
use RoundlyConsulting\Approvals\DataTransferObjects\DecisionData;

app(ApproveAction::class)->execute($user, $post, DecisionData::approved('Looks good'));
app(RejectAction::class)->execute($user, $post, DecisionData::rejected('Needs work'));
app(RequestApprovalAction::class)->execute($user, $post);                    // pending Approval
app(CancelApprovalAction::class)->execute($user, $post, 'Changed my mind');   // ?Approval
app(ToggleApprovalAction::class)->execute($user, $post);                     // bool
app(ExpireApprovalsAction::class)->execute();                                // int
app(ExpireApprovalsAction::class)->execute(subjectType: Post::class);        // int — posts only
Actionexecute()Notes
ApproveAction($actor, $approvable, ?DecisionData, ?ApprovalRequest): ApprovalGate check, named approvers, delegation, weight and stage; supersedes a held rejection; a repeat is a no-op; resolves the request.
RejectAction($actor, $approvable, ?DecisionData, ?ApprovalRequest): ApprovalGate check, named approvers, delegation, weight and stage; supersedes a held approval; resolves the request.
RequestApprovalAction($actor, $approvable, ?DecisionData, ?ApprovalRequest): ApprovalRecords a pending decision — or returns the actor’s live one unchanged; fires ApprovalRequested.
CancelApprovalAction($actor, $approvable, ?string $reason, ?ApprovalRequest): ?ApprovalWithdraws the actor’s live decision, or one it made as a delegate.
ToggleApprovalAction($actor, $approvable): boolApproves through ApproveAction, or withdraws and soft-deletes the live approval.
OpenApprovalRequestAction(ApprovalRequestData $data): ApprovalRequestOpens a flat request and stores its named approvers; required_approvers defaults to one per named approver.
RequestStagedApprovalAction($subject, $stages, $rejectOnStageRejection = true, ?$expiresAt, ?$workflow): ApprovalRequestOpens a staged pipeline; each stage stores its named approvers.
OpenWorkflowRequestAction($subject, WorkflowPreset, $approvers): ApprovalRequestOpens a request from a resolved preset.
DelegateApprovalsAction($delegator, $delegate, ?$startsAt, ?$endsAt): ApprovalDelegationValidates and saves a delegation; fires ApprovalDelegated.
RevokeApprovalDelegationAction($delegator, ?$delegate): intRevokes active and scheduled delegations; returns the count.
ExpireApprovalsAction(?$now, ?$subjectType): intLapses overdue asks, approvals and requests — optionally of one subject type; returns the count.

Attaching to a specific request

The approve, reject, ask and cancel actions accept the request to attach to as their fourth argument — the facade passes it with within(). Without it, they use the approvable’s latest open request, or record a standalone decision when the approvable never had one. A request of another subject throws InvalidApprovalRequestException and a closed one ClosedApprovalRequestException, before anything is written:

use RoundlyConsulting\Approvals\Actions\ApproveAction;
use RoundlyConsulting\Approvals\DataTransferObjects\DecisionData;
use RoundlyConsulting\Approvals\Facades\Approvals;

$request = Approvals::request($release)->from([$lead, $qa])->open();

// The action's fourth argument is what within($request) passes on the facade.
app(ApproveAction::class)->execute($lead, $release, DecisionData::approved('Ship it'), $request);

DecisionData

DecisionData carries the intent of a decision. Build it with a named constructor:

ConstructorResult
DecisionData::approved(?$reason, ?$expiresAt, ?$weight)An approval — weight overrides the resolved weight for this one decision.
DecisionData::rejected(?$reason, ?$weight)A rejection — weight works as for approved().
DecisionData::pending(?$expiresAt)A pending decision.
DecisionData::cancelled(?$reason)A cancellation.

Staged requests with a workflow name

RequestStagedApprovalAction accepts an expiry and a workflow name, which it stamps on the request — the facade and the trait set the expiry, the action also takes the name:

use RoundlyConsulting\Approvals\Actions\RequestStagedApprovalAction;
use RoundlyConsulting\Approvals\DataTransferObjects\StageDefinition;
use RoundlyConsulting\Approvals\Enums\ApprovalRule;

app(RequestStagedApprovalAction::class)->execute(
    $release,
    [
        new StageDefinition([$eng1, $eng2], name: 'engineering'),
        new StageDefinition([$product], ApprovalRule::Any, name: 'product'),
    ],
    rejectOnStageRejection: true,
    expiresAt: now()->addDays(3),
);

ApprovalRequestData

ApprovalRequestData is the read-only input of OpenApprovalRequestAction: subject, approvers (the named approvers — none named means any approver may decide), rule, quorum, expiresAt, plus an optional requiredApprovers (defaults to one per named approver) and workflow name.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.