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

Approvals are polymorphic on both sides. Any model can be an actor that decides — a user, a team, a service account — and any model can be approvable — a deployment, a document, a comment. Add the trait for each role:

use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Approvals\Traits\GivesApprovals;
use RoundlyConsulting\Approvals\Traits\HasApprovals;
use RoundlyConsulting\Approvals\Traits\RequiresApproval;

class User extends Model
{
    use GivesApprovals;     // the actor — decides on things
}

class Deployment extends Model
{
    use HasApprovals;       // the approvable — gets decided on
}

class Release extends Model
{
    use HasApprovals;
    use RequiresApproval;   // a subject that needs multi-approver sign-off
}
TraitInterfaceAdd it to
GivesApprovalsGivesApprovalsInterfaceThe actor that decides — a user, a team, a service account.
HasApprovalsHasApprovalsInterfaceThe approvable that gets decided on — a deployment, a document, a comment.
RequiresApprovalRequiresApprovalInterfaceA subject that needs multi-approver sign-off through a request.

One model may use all three — a team that approves others’ work and needs sign-off itself. The actor side’s relation is givenApprovals() and the approvable side’s is approvals(), so the traits never collide.

Actor side — GivesApprovals

$user->approve($deployment, 'LGTM');       // Approval
$user->reject($deployment, 'needs work');  // Approval
$user->cancelApproval($deployment);        // ?Approval
$user->toggleApproval($deployment);        // bool (simple on/off)

$user->hasApproved($deployment);           // bool — holds an approval still in force
$user->hasRejected($deployment);           // bool — holds a rejection
$user->approvalFor($deployment);           // ?Approval (latest)
$user->givenApprovals;                     // Collection<Approval> — every decision the user recorded

$user->delegateApprovalsTo($assistant);    // ApprovalDelegation — granted at once (see Delegation)
$user->revokeApprovalDelegation();         // int — delegations revoked
$user->approvalDelegations;                // delegations this actor handed out

hasApproved() and hasRejected() mean “holds a decision with that status” — not merely “a row exists” — and an approval past its expiry stops counting at once.

Approvable side — HasApprovals

$deployment->hasBeenApprovedBy($user);    // bool
$deployment->hasBeenRejectedBy($user);    // bool
$deployment->isApprovedBy($user);         // bool — alias of hasBeenApprovedBy()
$deployment->approvalCount();             // int — approvals still in force
$deployment->pendingApprovals();          // Collection<Approval> — asks not yet past their deadline
$deployment->approvals;                   // Collection<Approval> — every decision on the deployment

Request subject — RequiresApproval

$release->requestApproval([$lead, $qa, $pm]);   // ApprovalRequest — Unanimous by default
$release->requestStagedApproval($stages);       // ApprovalRequest — list<StageDefinition>

$release->currentApprovalStatus();   // ApprovalStatus — Pending when no request exists, Expired once it is overdue
$release->isApproved();              // bool
$release->isPendingApproval();       // bool
$release->currentStage();            // ?ApprovalRequestStage — the open stage
$release->approvalProgress();        // ?ApprovalProgress
$release->approvalRequests;          // every request whose subject is this model

currentApprovalStatus(), isApproved(), isPendingApproval(), currentStage() and approvalProgress() all read the subject’s latest request. With no request yet, currentApprovalStatus() returns Pending and approvalProgress() returns null; once an open request is past its expiry, the status reads Expired before the sweep lapses it.

Type-hint the capability

Each trait has a matching interface in RoundlyConsulting\Approvals\Interfaces — depend on the capability, not the concrete model:

use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Approvals\Interfaces\GivesApprovalsInterface;
use RoundlyConsulting\Approvals\Interfaces\HasApprovalsInterface;
use RoundlyConsulting\Approvals\Traits\GivesApprovals;

class User extends Model implements GivesApprovalsInterface
{
    use GivesApprovals;
}

function approvalBadge(HasApprovalsInterface $approvable): string
{
    return $approvable->approvalCount().' approvals';
}

The interfaces cover the core decision methods; the delegation and staging helpers live on the traits only.

Sugar over the manager

The traits are shorthand: every state change they make — approve(), reject(), requestApproval(), delegateApprovalsTo() and the rest — goes through ApprovalsManager, the same class behind the Approvals facade, so Approvals::fake() records it too.

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.