Models & schema
Four models back the package. Each uses soft deletes, ships a factory and is swappable via config:
| Config key | Model | Table |
|---|---|---|
model | Approval | approvals |
request_model | ApprovalRequest | approval_requests |
stage_model | ApprovalRequestStage | approval_request_stages |
delegation_model | ApprovalDelegation | approval_delegations |
Tables
| Table | Columns |
|---|---|
approvals | id, 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_requests | id, 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_stages | id, approval_request_id, position, name, rule, quorum, required_approvers, approvers (nullable json), status (default pending), opened_at, cleared_at, timestamps, deleted_at |
approval_delegations | id, 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_type | Morph id column | Use when your models… |
|---|---|---|
bigint | bigint | use Laravel’s default auto-incrementing keys (the default) |
uuid | uuid | use HasUuids |
ulid | ulid (26 chars) | use HasUlids |
APPROVALS_KEY_TYPE=uuid # bigint (default), uuid or ulid — set before you migrateUnset 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 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.