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| Action | execute() | Notes |
|---|---|---|
ApproveAction | ($actor, $approvable, ?DecisionData, ?ApprovalRequest): Approval | Gate 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): Approval | Gate check, named approvers, delegation, weight and stage; supersedes a held approval; resolves the request. |
RequestApprovalAction | ($actor, $approvable, ?DecisionData, ?ApprovalRequest): Approval | Records a pending decision — or returns the actor’s live one unchanged; fires ApprovalRequested. |
CancelApprovalAction | ($actor, $approvable, ?string $reason, ?ApprovalRequest): ?Approval | Withdraws the actor’s live decision, or one it made as a delegate. |
ToggleApprovalAction | ($actor, $approvable): bool | Approves through ApproveAction, or withdraws and soft-deletes the live approval. |
OpenApprovalRequestAction | (ApprovalRequestData $data): ApprovalRequest | Opens a flat request and stores its named approvers; required_approvers defaults to one per named approver. |
RequestStagedApprovalAction | ($subject, $stages, $rejectOnStageRejection = true, ?$expiresAt, ?$workflow): ApprovalRequest | Opens a staged pipeline; each stage stores its named approvers. |
OpenWorkflowRequestAction | ($subject, WorkflowPreset, $approvers): ApprovalRequest | Opens a request from a resolved preset. |
DelegateApprovalsAction | ($delegator, $delegate, ?$startsAt, ?$endsAt): ApprovalDelegation | Validates and saves a delegation; fires ApprovalDelegated. |
RevokeApprovalDelegationAction | ($delegator, ?$delegate): int | Revokes active and scheduled delegations; returns the count. |
ExpireApprovalsAction | (?$now, ?$subjectType): int | Lapses 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:
| Constructor | Result |
|---|---|
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 cryptoBy 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.