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

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 Approved

Every 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 met

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.