PHPackages                             x-laravel/eloquent-approval - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. x-laravel/eloquent-approval

ActiveLibrary[Database &amp; ORM](/categories/database)

x-laravel/eloquent-approval
===========================

Approval process for Laravel Eloquent models.

v1.0.0(2mo ago)111↓100%MITPHPPHP ^8.2CI passing

Since Apr 11Pushed 2mo agoCompare

[ Source](https://github.com/x-laravel/eloquent-approval)[ Packagist](https://packagist.org/packages/x-laravel/eloquent-approval)[ RSS](/packages/x-laravel-eloquent-approval/feed)WikiDiscussions master Synced 1w ago

READMEChangelogDependencies (5)Versions (2)Used By (0)

Eloquent Approval
=================

[](#eloquent-approval)

[![Tests](https://github.com/x-laravel/eloquent-approval/actions/workflows/tests.yml/badge.svg)](https://github.com/x-laravel/eloquent-approval/actions/workflows/tests.yml)[![PHP](https://camo.githubusercontent.com/187240af044d09d5b14a1d9d9ebdf3f7a993e4c7bc09bdb46b4ba661a891bf5b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e322532422d626c7565)](https://www.php.net)[![Laravel](https://camo.githubusercontent.com/42e62a9adb05b6cb16993782fd4b04b64a76be3ff5704d170001885eb70c8448/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d313225323025374325323031332d726564)](https://laravel.com)[![License](https://camo.githubusercontent.com/f8df3091bbe1149f398a5369b2c39e896766f9f6efba3477c63e9b4aa940ef14/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e)](LICENSE.md)

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

How It Works
------------

[](#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
------------

[](#requirements)

- PHP ^8.2
- Laravel ^12.0 | ^13.0

Installation
------------

[](#installation)

```
composer require x-laravel/eloquent-approval
```

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

Setup
-----

[](#setup)

### 1. Migration

[](#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

[](#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

[](#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
-----

[](#usage)

### Querying

[](#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

[](#updating-status)

#### On a model instance

[](#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

[](#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

[](#checking-status)

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

### Approval Required Attributes

[](#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

[](#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
------

[](#events)

Each approval action dispatches a before and after event:

ActionBeforeAfter`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
--------------

[](#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
------------------------

[](#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
-------

[](#testing)

```
# Build first (once per PHP version)
DOCKER_BUILDKIT=0 docker compose --profile php82 build

# Run tests
docker compose --profile php82 up
docker compose --profile php83 up
docker compose --profile php84 up
docker compose --profile php85 up
```

License
-------

[](#license)

This package is open-sourced software licensed under the [MIT license](https://opensource.org/license/MIT).

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance88

Actively maintained with recent releases

Popularity7

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

60d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/2bdb64c6c087c331b8bd5906bb1aa7eb06bc83af3654a48ba8ab9da365976651?d=identicon)[X-Adam](/maintainers/X-Adam)

---

Top Contributors

[![x-adam](https://avatars.githubusercontent.com/u/60411758?v=4)](https://github.com/x-adam "x-adam (1 commits)")

---

Tags

laraveleloquentapproval

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/x-laravel-eloquent-approval/health.svg)

```
[![Health](https://phpackages.com/badges/x-laravel-eloquent-approval/health.svg)](https://phpackages.com/packages/x-laravel-eloquent-approval)
```

###  Alternatives

[kirschbaum-development/eloquent-power-joins

The Laravel magic applied to joins.

1.6k29.9M42](/packages/kirschbaum-development-eloquent-power-joins)[mongodb/laravel-mongodb

A MongoDB based Eloquent model and Query builder for Laravel

7.1k8.0M84](/packages/mongodb-laravel-mongodb)[spatie/laravel-sluggable

Generate slugs when saving Eloquent models

1.5k12.4M291](/packages/spatie-laravel-sluggable)[psalm/plugin-laravel

Psalm plugin for Laravel

3325.1M337](/packages/psalm-plugin-laravel)[watson/validating

Eloquent model validating trait.

9743.4M53](/packages/watson-validating)[yajra/laravel-oci8

Oracle DB driver for Laravel via OCI8

8733.1M23](/packages/yajra-laravel-oci8)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
