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

Models & schema

Four models back the package. Each uses soft deletes, ships a factory and is swappable via config:

Config keyModelTable
modelApprovalapprovals
request_modelApprovalRequestapproval_requests
stage_modelApprovalRequestStageapproval_request_stages
delegation_modelApprovalDelegationapproval_delegations

Tables

TableColumns
approvalsid, actor (morph), approvable (morph), status (default approved), reason, approval_request (nullable morph), decision_scope, live (nullable), decided_by (nullable morph), weight (default 1), approval_request_stage_id, decided_at, expires_at, timestamps, deleted_at — unique index approvals_live_decision_unique
approval_requestsid, subject (nullable morph), rule, quorum, required_approvers, approvers (nullable json), status (default pending), staged (default false), reject_on_stage_rejection (default true), workflow, resolved_at, expires_at, timestamps, deleted_at
approval_request_stagesid, approval_request_id, position, name, rule, quorum, required_approvers, approvers (nullable json), status (default pending), opened_at, cleared_at, timestamps, deleted_at
approval_delegationsid, delegator (morph), delegate (morph), starts_at, ends_at, revoked_at, timestamps, deleted_at

decision_scope and live back the one-live-decision-per-slot rule: the unique index covers the approvable, the actor, decision_scope (the request and stage, empty for a standalone decision) and live, which turns NULL once a decision is withdrawn, superseded, expired or soft-deleted. The approvers JSON column holds the named approvers with the weight each carried at open — NULL when a request or stage names nobody.

Relations & scopes

  • Approval — actor(), approvable(), decidedBy() and approvalRequest() (morph-to) and stage() (belongs-to). Scopes: pending(), approved(), rejected(), expired(), active() (pending or approved), live() (each slot’s live decision), inForce(?$moment) and expiringBefore($moment). Methods: isInForce(?$moment) and wasDelegated().
  • ApprovalRequest — subject() (morph-to), decisions() (morph-many) and stages() (has-many, ordered by position). Methods: namedApprovers(), approversFor(?$stage), hasNamedApprover($model), currentStage(), resolve(), isOverdue(?$moment), lapseIfOverdue(?$moment) and approvalProgress().
  • ApprovalRequestStage — request() (belongs-to) and decisions() (has-many). Methods: namedApprovers(), hasNamedApprover($model), isOpen() and markOpened().
  • ApprovalDelegation — delegator() and delegate() (morph-to). Scopes active(?$moment) and inForceOrScheduled(?$moment); methods isActiveAt(?$moment) and revoke(?$at).
use RoundlyConsulting\Approvals\Models\Approval;
use RoundlyConsulting\Approvals\Models\ApprovalDelegation;

Approval::query()->approved()->inForce()->whereMorphedTo('approvable', $post)->get();
Approval::query()->live()->whereMorphedTo('actor', $user)->get();   // each slot's live decision
Approval::query()->active()->expiringBefore(now())->get();

ApprovalDelegation::query()->active()->whereMorphedTo('delegate', $assistant)->get();
ApprovalDelegation::query()->inForceOrScheduled()->whereMorphedTo('delegator', $manager)->get();

Morph key type

key_type types the seven polymorphic id columns — actor, approvable, approval_request and decided_by on approvals, subject on approval_requests, and delegator and delegate on approval_delegations. Your morph targets must share one key type:

key_typeMorph id columnUse when your models…
bigintbigintuse Laravel’s default auto-incrementing keys (the default)
uuiduuiduse HasUuids
ulidulid (26 chars)use HasUlids
APPROVALS_KEY_TYPE=uuid   # bigint (default), uuid or ulid — set before you migrate

Unset or blank reads as bigint; any other value throws InvalidConfigurationException when the migrations run, so a typo never silently builds bigint columns for UUID- or ULID-keyed models. The package’s own ids — including approval_request_stage_id — are always auto-incrementing.

Swapping models

Point any model key at your own subclass. The package resolves the configured class everywhere — actions, traits and relations included:

// config/approvals.php
'model' => App\Models\Approval::class,
'request_model' => App\Models\ApprovalRequest::class,
'stage_model' => App\Models\ApprovalRequestStage::class,
'delegation_model' => App\Models\ApprovalDelegation::class,
namespace App\Models;

use RoundlyConsulting\Approvals\Models\Approval as BaseApproval;

class Approval extends BaseApproval
{
    // your relations, scopes and casts
}

A class that doesn’t extend the expected base model throws InvalidApprovalModelException.

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.