Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
Build and work with cjmellor/approval features, including approval workflows, custom states, time-based expirations, rollbacks, and event handling.
When to use this skill
Use this skill when working with model approval workflows, intercepting create/update operations for review, managing approval states, setting up time-based expirations, or handling approval events using cjmellor/approval.
Core Concepts
Models use MustBeApproved trait — Intercepts creating and updating events, storing dirty data in the approvals table instead of persisting directly.
Approvals have states — Pending, Approved, Rejected (via ApprovalStatus enum), plus custom states defined in config.
Approve to persist — Data only reaches the model's table when explicitly approved.
Bypass with withoutApproval() — Skip the approval workflow for admin actions, seeders, etc.
Expiration support — Approvals can auto-reject, auto-postpone, or trigger custom logic after a deadline.
Setup
Add the MustBeApproved trait to any model that needs approval:
// Set any configured state$approval->setState('in_review');
// Get current state (returns custom state if set, otherwise standard state)$state = $approval->getState(); // 'in_review'// Query by any stateApproval::whereState('in_review')->get();
Approval::whereState('pending')->get(); // Only genuinely pending, excludes custom states
Time-Based Expirations
// Set expiration with automatic action$approval->expiresIn(hours: 48)->thenReject();
$approval->expiresIn(days: 7)->thenPostpone();
$approval->expiresIn(minutes: 30)->thenCustom(); // Handle via ApprovalExpired event listener// Specific datetime$approval->expiresIn(datetime: now()->addWeek());
// Check expiration$approval->isExpired(); // bool// Query by expirationApproval::expired()->get();
Approval::notExpired()->get();
Approval::hasExpiration()->get();
Process expired approvals via the scheduler:
// In routes/console.php (Laravel 11+)Schedule::command('approval:process-expired')->everyMinute();
Rollbacks
// Roll back an approved change (keeps Approved state by default)$approval->rollback();
// Roll back and reset to Pending (requires re-approval)$approval->rollback(bypass: false);
// Conditional rollback$approval->rollback(condition: fn ($approval) => $approval->created_at->isToday());
Requestor / Creator Tracking
// Get who created the approval$requestor = $approval->requestor; // The authenticated user at creation time$creator = $approval->creator; // Same relationship, different name// Filter by requestorApproval::requestedBy($user)->get();
// Check if a specific user requested it$approval->wasRequestedBy($user); // bool
Foreign Keys
// Default foreign key is 'user_id' — customize per model:publicfunctiongetApprovalForeignKeyName(): string{
return'author_id';
}
// Include the foreign key when creating:Post::create(['title' => 'Hello', 'user_id' => auth()->id()]);
Events
All events extend ApprovalEvent with Approval $approval and ?Authenticatable $user:
Use the MustBeApprovedFactory trait in model factories to bypass approval in tests:
useCjmellor\Approval\Concerns\MustBeApprovedFactory;
classPostFactoryextendsFactory{
useMustBeApprovedFactory;
// ...
}
// In tests:Post::factory()->withoutApproval()->create();
Bypassing Approval
// Bypass for a single operation$post->withoutApproval()->update(['title' => 'Admin Override']);
// Check if bypass is active$post->isApprovalBypassed(); // bool
How Data Flows
When a model with MustBeApproved is created or updated:
The trait intercepts the creating/updating Eloquent event
Dirty attributes are captured as new_data, original values as original_data
If $approvalAttributes is defined, only those attributes go through approval — the rest persist immediately
The foreign key (default user_id) is extracted and stored separately in the foreign_key column
An Approval record is created with state = pending
The original model operation is cancelled (returns false from the event)
When approved, new_data is applied to the model via forceFill() + withoutApproval()->save()
The new_data and original_data columns are cast to AsArrayObject, so you can access them as arrays:
$approval->new_data['title']; // The proposed new title$approval->original_data['title']; // The original title before the change
Common Patterns
Building an Approval Dashboard
// Get all pending approvals with their related models$approvals = Approval::pending()
->with('approvalable', 'requestor')
->latest()
->paginate(20);
// In a Blade view
@foreach ($approvalsas$approval)
<div>
<p>{{ $approval->requestor?->name }} wants to change {{ class_basename($approval->approvalable_type) }}</p>
<p>Changes: {{ json_encode($approval->new_data->toArray()) }}</p>
<form method="POST" action="/approvals/{{ $approval->id }}/approve">
@csrf
<button type="submit">Approve</button>
</form>
</div>
@endforeach
The package prevents duplicate pending approvals for the same model with identical data. If a user submits the same change twice, only one approval record is created.
JSON / Array Cast Attributes
Models with json or array casts work correctly. The approval system decodes JSON values when storing new_data and re-casts them when approving. Nested arrays and complex data structures are preserved.