Resolving requests
Resolve through the facade. Each call takes the request, the acting approver and an optional reason:
use RoundlyConsulting\Requests\Facades\Requests;
$request = Requests::make()->requireApprovalsFrom([$alice, $bob])->create();
Requests::approve($request, $alice); // 1 of 2 — stays New
Requests::reject($request, $bob, reason: 'Out of policy'); // unanimous: one rejection rejects it
Requests::reopen($request, $alice); // back to New — a fresh approval round opens
Requests::approve($request, $alice);
Requests::approve($request, $bob, reason: 'Looks good'); // 2 of 2 — becomes ApprovedEvery decision is recorded through the approvals engine with its actor, reason and timestamp, so you get a full audit trail for free.
- A request with no declared approvers resolves immediately on the first approve or reject.
- Otherwise the approval rule decides when the request flips — by default unanimous, so every declared approver must approve.
- Only the declared approvers — or someone they delegated to — may decide. Anyone else gets RoundlyConsulting\Approvals\Exceptions\UnauthorizedApprovalException and nothing is recorded — straight through the engine too ($mallory->approve($request)).
- Once the approval round has resolved, a new decision would count towards nothing, so approve() and reject() throw RequestAlreadyResolved until the request is reopened (straight through the engine, $alice->approve($request) throws its ClosedApprovalRequestException).
- approve() fires ApprovalRecorded, even when the request stays New.
- reopen() moves the request back to New and fires ApprovalRevoked. While its round is still open (or it has none) the actor’s own decision is withdrawn. If the round is already over, a fresh round opens with the same approvers, rule, stages or preset, and everyone decides again — earlier decisions don’t carry over. A declared approver that no longer exists makes the reopen throw InvalidApprover, and nothing changes.
- The returned Request is refreshed, so its status already reflects the engine’s outcome.
Refused decisions
use RoundlyConsulting\Approvals\Exceptions\UnauthorizedApprovalException;
use RoundlyConsulting\Requests\Exceptions\RequestAlreadyResolved;
try {
Requests::approve($request, $mallory); // not a declared approver, nor a delegate of one
} catch (UnauthorizedApprovalException $e) {
// nothing was recorded
}
try {
Requests::approve($approvedRequest, $alice); // the round has resolved
} catch (RequestAlreadyResolved $e) {
$e->status; // Status::Approved — reopen() first to decide again
}The ResolveRequest action
The underlying action accepts a Status and an optional reason directly:
use RoundlyConsulting\Requests\Actions\ResolveRequest;
use RoundlyConsulting\Requests\Enums\Status;
app(ResolveRequest::class)->execute($request, $alice, Status::Approved, reason: 'Signed off');Decisions made through the engine
Because the sync listener mirrors engine resolutions, a decision recorded straight through the approvals engine moves the request too — as long as the actor is a declared approver or their delegate:
use RoundlyConsulting\Approvals\Facades\Approvals;
// Decide straight through the approvals engine — the request's Status still follows.
Approvals::for($request)->as($alice)->approve();
$request->fresh()->status; // Status::Approved once the rule is metShow 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.