eloquent-approval maintained by x-laravel
Eloquent Approval
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_attodatetimein your model's$caststo getCarboninstances.
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.