Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Approval Laravel Laravel Package

afiqiqmal/approval-laravel

A lightweight Laravel package for adding approval workflows to Eloquent models. Attach approvers, track approval status, and gate actions until sign-off is complete. Simple configuration for apps that need a review or approval step before changes take effect.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require afiqiqmal/approval-laravel

# Publish config + migrations (provider path may differ by version)
php artisan vendor:publish --provider="Afiqiqmal\Approval\ApprovalServiceProvider"
php artisan migrate

Then make a model approvable:

use Afiqiqmal\Approval\Traits\Approvable;

class PurchaseOrder extends Model
{
    use Approvable;
}

Where to look first, in this order:

  1. The published migration — it decides everything downstream: a single status column on your table vs. a separate approvals table with one row per approver. A separate table means multi-approver and audit history; a column means a simple state flag. Read this before designing anything.
  2. The config file — model bindings, table names, and any status enum values you can override.
  3. The Approvable trait — the actual public API surface (submit, approve, reject, scopes). Trait method names are the contract you'll be writing against daily, so skim them once rather than guessing.

First use case worth wiring up: a purchase order that needs sign-off before it can be paid. Submit it, let an approver list only what's pending on them, record approve/reject, and gate the "Pay" action on the approved state. That single flow exercises submission, querying, transitions, and guarding — and it's the shape 80% of approval requirements take.

Implementation Patterns

Submit / decide / inspect is the core loop. Keep it in a service or action class rather than sprinkling trait calls through controllers, because you'll almost always need to add notification, audit, and authorization around each transition:

class SubmitForApproval
{
    public function __construct(private ApprovalNotifier $notifier) {}

    public function handle(PurchaseOrder $order): void
    {
        DB::transaction(function () use ($order) {
            $order->submitForApproval();      // confirm exact trait method name
            $this->notifier->pending($order);
        });
    }
}

Query the approver's inbox with scopes, not loops. Whatever scope the package exposes for pending records, use it — and pair it with eager loading:

PurchaseOrder::query()
    ->pendingApproval()          // package scope; verify name
    ->with(['approvals', 'requester'])
    ->latest()
    ->paginate(25);

Guard actions on status. The most common bug in approval features is UI-and-controller drift: the button disappears but the endpoint still accepts the POST. Enforce it server-side in a Policy or a canBePaid() check on the model, and reuse that same check in the Blade component.

Map package state to your domain state explicitly. Don't let approved leak into every if in your codebase. Add a small enum or accessor ($order->isPayable()) so that if the package's status strings change, or you later add a "partially approved" state, you change one file.

Chain multi-step approvals sequentially, not in parallel, unless the package explicitly supports parallel approvers. If there's an approvals table, ordering matters: approve step N only after step N−1. If the package has no built-in ordering, keep an order/sequence column and enforce it in your own action class rather than relying on row insertion order.

Hook notifications off transitions, not controllers. If the package fires events, listen to them; if it doesn't, your action classes are the seam. Either way, never send mail directly from the trait call site — you'll duplicate notifications when a retry happens.

Testing. Use RefreshDatabase, a factory state for each status, and assert both the happy path and the forbidden path (approving an already-approved record, rejecting without permission). Approval logic regressions are almost always "the guard was missing," so the negative tests carry the weight.

Gotchas and Tips

  • 2.0.0 is a major bump whose only stated change is "update dependencies." Treat that as a red flag, not a non-event: majors usually mean a raised PHP floor, a raised illuminate/* constraint, or a swapped underlying package (e.g. a new Symfony/mail/HTTP major). Before upgrading, diff composer.json in the tag, run composer why-not afiqiqmal/approval-laravel 2.0.0 against your app's lock, and read the compare link (1.6.0...2.0.0) — the release note alone tells you nothing about API changes. Run your approval test suite on a branch before merging.
  • Status storage mismatch is the classic trap. If the migration stores a string but your model casts to an enum, an unrecognized value from the package throws on hydration. Check the cast against the config's status list.
  • Trait boot method collisions. If your model already defines boot/booted or uses another package that hooks model events for the same trait, initialization silently wins or loses. If approvals stop firing after adding a second trait, look here first.
  • Concurrency. Two approvers clicking at once on a per-record status column will both "succeed" and the last write wins. Wrap transitions in a transaction with a row lock (lockForUpdate) or a conditional UPDATE ... WHERE status = 'pending' and check affected rows.
  • Existing rows after migrating. Adding the approval column/table to a populated table leaves NULL statuses that won't match pending. Backfill in the migration or treat NULL as a valid pre-approval state in your scopes.
  • N+1 on lists. $order->status via a relation without eager loading will quietly double your query count on an inbox page. Add the relation to a $with default if it's read on every row.
  • Extension points worth using, in priority order: (1) published config for model/table overrides — don't fork the package for that; (2) events/listeners if present; (3) your own action classes wrapping the trait. Avoid editing published migrations in place after they've run in production; add a new migration.
  • Debugging tip: if behaviour contradicts the docs, you're probably reading the package's default config while a stale published copy sits in config/. Run php artisan config:clear and compare both files side by side.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator