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

Submission review

Enable forms.approvals.enabled (FORMS_APPROVALS_ENABLED=true) and route a whole submission — the FormSubmission aggregate — through the approvals engine for sign-off by the reviewers you name. Reviewers are models using the GivesApprovals trait from approvals-for-laravel:

use RoundlyConsulting\Approvals\Traits\GivesApprovals;

class User extends Authenticatable
{
    use GivesApprovals; // approvals-for-laravel: lets the model approve and reject
}

Open a review over a submission — pass its FormSubmission model or its uuid. The decision is mirrored back onto the submission status automatically:

use RoundlyConsulting\Forms\Facades\Forms;

$submission = Forms::submission($result->uuid)->model();

Forms::review($submission)         // or Forms::review($uuid), Forms::submission($uuid)->review()
    ->requiring([$lead, $qa, $pm])
    ->quorum(2)                    // or ->unanimous(), ->any(), ->weighted($threshold)
    ->open();                      // submission status -> Pending

$lead->approve($submission);       // decisions flow into the open request
$qa->approve($submission);         // quorum met -> status Approved, SubmissionApproved fired

$submission->refresh();            // the status moved in the database: reload this instance
$submission->isApproved();         // true
$submission->isPendingApproval();  // false
  • requiring() is mandatory: only the reviewers it names — or their delegates — can decide. Anyone else, the submitter included, gets approvals’ UnauthorizedApprovalException and nothing is recorded; a review that names nobody throws SubmissionNotReviewableException.
  • Rules — unanimous() (the default), any(), quorum($n) or weighted($threshold); or pass an ApprovalRule to rule().
  • open() returns the approvals ApprovalRequest and moves the submission to pending.
  • When the request resolves, the status moves to approved or rejected and SubmissionApproved / SubmissionRejected fire — once per outcome, even when two decisions race to resolve it; a rejection carries the rejecting actor. The status moves in the database, so refresh() a model you hold.
  • A decided submission can be reviewed again — a new request, back to pending — and the same reviewers decide it afresh.
  • Opening a review while reviews are disabled throws ReviewsDisabledException; on a draft, SubmissionNotReviewableException.
  • With reviews disabled (the default), submissions keep the plain submit/finalize lifecycle.

The FormSubmission aggregate

$submission->isDraft();             // status is Draft
$submission->isPendingApproval();   // status is Pending
$submission->isApproved();          // status is Approved
$submission->isRejected();          // status is Rejected

FormSubmission::query()->pendingApproval()->get(); // everything awaiting sign-off

$submission->submissions;           // the per-field Submission rows
$submission->sender;                // morphTo — who submitted
$submission->form;                  // the Form

Status lifecycle

SubmissionStatus guards every move — the review listener only applies transitions the lifecycle allows, and is a no-op when the status is already at its target:

StatusCan move toMeaning
draftfinalSaved but not yet finalized.
finalpending, approved, rejectedFinalized; terminal when no review is used.
pendingapproved, rejectedRouted to review, awaiting sign-off.
approvedpending, rejectedThe review resolved in favour.
rejectedpending, approvedThe review resolved against.

The enum ships the enums-for-laravel helpers for filters and UIs:

use RoundlyConsulting\Forms\Enums\SubmissionStatus;

SubmissionStatus::values();          // 'draft', 'final', 'pending', 'approved', 'rejected'
SubmissionStatus::toOptions();       // value => readable label, for selects
SubmissionStatus::validationRule();  // 'in:draft,final,pending,approved,rejected'
SubmissionStatus::Pending->readable(); // 'Pending'

SubmissionStatus::Final->canTransitionTo(SubmissionStatus::Pending); // true
SubmissionStatus::Draft->allowedTransitions();                        // [SubmissionStatus::Final]
SubmissionStatus::Approved->isFinalized();                            // true

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.