Looking to hire Laravel developers? Try LaraJobs

eloquent-approval maintained by x-laravel

Description
Approval process for Laravel Eloquent models.
Author
Last update
2026/04/11 03:32 (dev-master)
License
Links
Downloads
2

Comments
comments powered by Disqus

Eloquent Approval

Tests PHP Laravel License

A Laravel package that adds a three-state approval workflow to Eloquent models — pending, approved, and rejected.

How It Works

  • Newly created models are automatically set to pending
  • Only approved models are returned by default queries
  • Updating attributes that require approval re-suspends the model back to pending
  • Status changes dispatch model events you can hook into

Requirements

  • PHP ^8.2
  • Laravel ^12.0 | ^13.0

Installation

composer require x-laravel/eloquent-approval

The service provider is registered automatically via Laravel's package discovery.

Setup

1. Migration

Add the approval columns to your table:

Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->approvals(); // adds approval_status (enum) + approval_at (timestamp)
    $table->timestamps();
});

2. Model

Add the Approvable trait to your model:

use Illuminate\Database\Eloquent\Model;
use XLaravel\EloquentApproval\Approvable;

class Post extends Model
{
    use Approvable;
}

Custom Column Names

class Post extends Model
{
    use Approvable;

    const APPROVAL_STATUS = 'status';
    const APPROVAL_AT = 'status_changed_at';
}

Cast approval_at to datetime in your model's $casts to get Carbon instances.

Usage

Querying

// Default: only approved records
Post::all();
Post::find(1); // null if pending or rejected

// Include all statuses
Post::withAnyApproval()->get();
Post::withAnyApproval()->find(1);

// Filter by status
Post::onlyPending()->get();
Post::onlyApproved()->get();
Post::onlyRejected()->get();

To disable the approval scope globally on a model:

class Post extends Model
{
    use Approvable;

    public $approvalScopeDisabled = true;
}

Updating Status

On a model instance

$post->approve();  // ?bool — true on success, false if already approved, null if not persisted
$post->reject();
$post->suspend();

On a query builder

Post::whereIn('id', $ids)->approve(); // returns number of updated rows
Post::whereIn('id', $ids)->reject();
Post::whereIn('id', $ids)->suspend();

Checking Status

$post->isApproved(); // ?bool
$post->isRejected(); // ?bool
$post->isPending();  // ?bool

Approval Required Attributes

By default, all attribute changes trigger re-suspension. You can customise this:

class Post extends Model
{
    use Approvable;

    // Only these attributes trigger re-suspension
    public function approvalRequired(): array
    {
        return ['title', 'body'];
    }

    // These attributes never trigger re-suspension
    public function approvalNotRequired(): array
    {
        return ['view_count'];
    }
}

approvalRequired() acts as a blacklist, approvalNotRequired() as a whitelist — same logic as Eloquent's $fillable and $guarded.

Re-suspension only happens when updating via a model instance, not through a query builder.

Duplicate Approvals

Setting the status to its current value is a no-op — no events are dispatched, approval_at is not updated, and the method returns false.

Events

Each approval action dispatches a before and after event:

Action Before After
approve approving approved
suspend suspending suspended
reject rejecting rejected

A general approvalChanged event is also dispatched on every status change.

Returning false from a before-event listener halts the operation.

// Via static callbacks
Post::approving(function (Post $post) {
    // return false to halt
});

Post::approved(function (Post $post) {
    // notify the author
});

Post::approvalChanged(function (Post $post) {
    // fires on any status change
});

// Via an observer
Post::observe(PostApprovalObserver::class);
class PostApprovalObserver
{
    public function approving(Post $post): void
    {
        //
    }

    public function approved(Post $post): void
    {
        //
    }
}

Factory States

Add ApprovalFactoryStates to your factory to create models with a specific status:

use Illuminate\Database\Eloquent\Factories\Factory;
use XLaravel\EloquentApproval\ApprovalFactoryStates;

class PostFactory extends Factory
{
    use ApprovalFactoryStates;

    public function definition(): array
    {
        return [
            'title' => fake()->sentence(),
        ];
    }
}
Post::factory()->approved()->create();
Post::factory()->rejected()->create();
Post::factory()->suspended()->create();

HTTP Approval Controller

Use the HandlesApproval trait in a controller to handle approval requests:

use App\Http\Controllers\Controller;
use App\Models\Post;
use XLaravel\EloquentApproval\HandlesApproval;

class PostApprovalController extends Controller
{
    use HandlesApproval;

    protected function model(): string
    {
        return Post::class;
    }
}
Route::post('admin/posts/{key}/approval', [PostApprovalController::class, 'performApproval'])
    ->middleware(['auth', 'can:manage-approvals']);

The request must include an approval_status field with one of: approved, pending, rejected.

Testing

# Run tests locally
vendor/bin/phpunit

# Run tests in a specific PHP version via Docker
docker compose --profile php82 run --rm php82
docker compose --profile php83 run --rm php83
docker compose --profile php84 run --rm php84
docker compose --profile php85 run --rm php85

License

MIT — see LICENSE.md.