cjmellor/approval
Laravel package to stage and review model changes before they’re persisted. It stores pending/new or amended data (approve/reject states) so you can build your own approval workflow. Includes migrations and configurable states/tables.
Installation:
composer require cjmellor/approval
php artisan vendor:publish --tag="approval-migrations"
php artisan vendor:publish --tag="approval-config"
php artisan migrate
Apply to a Model:
Add the MustBeApproved trait to your model (e.g., Post):
use Cjmellor\Approval\Concerns\MustBeApproved;
class Post extends Model
{
use MustBeApproved;
}
First Use Case:
Create or update a model instance. The package automatically stores changes in the approvals table with a pending state:
$post = Post::create(['title' => 'Draft Post']);
// Approval record is created in the `approvals` table.
Model Creation/Update:
MustBeApproved will trigger approval logic on create()/update().new_data and original_data columns.Approval State Management:
Approval::pending()->get(); // All pending approvals
Approval::approved()->get(); // All approved
$approval = Approval::find(1);
$approval->approve(); // Updates model and marks as approved
$approval->reject(); // Discards changes
Conditional Logic:
$approval->approveIf($post->isValid());
$approval->rejectUnless($user->isAdmin());
Rollbacks:
$approval->rollback(); // Reverts to `original_data`, sets state to `pending`
Time-Based Actions:
$approval->expiresIn(hours: 24)->thenReject();
// Schedule processing:
Schedule::command('approval:process-expired')->everyMinute();
Foreign Keys:
Ensure foreign keys (e.g., user_id) are included when creating models directly:
Post::create(['title' => 'Post', 'user_id' => auth()->id()]);
Override default foreign key name in your model:
public function getApprovalForeignKeyName(): string
{
return 'author_id';
}
Custom Attributes: Limit approval to specific attributes:
protected array $approvalAttributes = ['title', 'content'];
Events:
Listen for state changes (e.g., ModelApproved, ModelRejected) to trigger notifications or workflows:
event(new ModelApproved($post));
Bypassing Approval: Temporarily skip approval for specific actions:
$post->withoutApproval()->update(['status' => 'published']);
Missing Foreign Keys:
user_id), the approvals table will store null in creator_id. This can cause issues with requestor queries.Custom States in Scopes:
pending() scope excludes custom states (e.g., in_review). Use whereState('pending') for backward compatibility.Approval::whereState('in_review')->get();
Expiration Events:
thenCustom() action does not auto-process expired approvals. You must listen for ApprovalExpired events manually:
Event::listen(ModelRolledBack::class, function ($event) {
// Custom logic here
});
Rollback Behavior:
rollback() bypasses re-approval (bypass: true). To force re-approval:
$approval->rollback(bypass: false);
Attribute Whitelisting:
$approvalAttributes is defined, only those attributes are tracked. Omitting it tracks all attributes.$approvalAttributes may lead to unexpected approvals for unrelated attributes.Check Approval Records:
approvals table to verify stored data:
$approval = Approval::find($id);
dd($approval->new_data->toArray());
Event Debugging:
AppServiceProvider@boot() to debug workflows:
ModelApproved::listen(function ($event) {
Log::info('Approved:', $event->approval->approvalable);
});
Expiration Quirks:
approval:process-expired command is scheduled in App\Console\Kernel.php:
$schedule->command('approval:process-expired')->everyMinute();
Config Overrides:
config/approval.php:
'states' => [
'pending' => ['name' => 'Pending', 'default' => true],
'in_review' => ['name' => 'Under Review'],
],
Custom States:
ApprovalStatus enum or use the custom_state column for ad-hoc states:
$approval->setState('custom_state_name');
Rollback Logic:
ModelRolledBack events.Expiration Actions:
thenCustom() via ApprovalExpired listeners.Model-Specific Logic:
protected static function booted()
{
static::created(function ($model) {
if (!$model->isValidForApproval()) {
$model->withoutApproval()->save();
}
});
}
Testing:
$this->partialMock(Post::class, function ($mock) {
$mock->shouldReceive('save')->andReturnTrue();
});
```markdown
### Pro Tips
- **Bulk Approvals**:
Use Laravel's query builder to approve multiple records:
```php
Approval::where('approvalable_type', Post::class)
->whereState('pending')
->approve();
Soft Deletes:
If your model uses soft deletes, ensure the approvals table also has a deleted_at column (publish migrations include this).
Performance:
For large datasets, use cursor() when processing expired approvals:
Approval::expired()->cursor()->each(function ($approval) {
$approval->reject();
});
Localization:
Translate state names in your language files (e.g., lang/en/approval.php):
return [
'states' => [
'pending' => 'Pending Review',
'approved' => 'Approved',
],
];
How can I help you explore Laravel packages today?