Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Approval Laravel Package

cjmellor/approval

Laravel package to stage and approve new Eloquent model data before it’s persisted. Provides an approval workflow with migrations and configurable behavior, supporting PHP 8.3+ and Laravel 12.4+/13.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Steps

  1. Installation:

    composer require cjmellor/approval
    php artisan vendor:publish --tag="approval-migrations"
    php artisan migrate
    
  2. Apply Trait to Model:

    use Cjmellor\Approval\Concerns\MustBeApproved;
    
    class Post extends Model
    {
        use MustBeApproved;
    }
    
  3. First Use Case: Create/update a Post model. The changes will automatically be stored in the approvals table with a pending state.

Where to Look First

  • Config: config/approval.php (customize states, foreign keys, etc.)
  • Model Methods: isApprovalBypassed(), withoutApproval(), rollback()
  • Query Scopes: approved(), rejected(), pending(), requestedBy($user)
  • Events: ModelApproved, ModelRejected, ModelRolledBack

Implementation Patterns

Core Workflow

  1. Model Creation/Update:

    $post = Post::create(['title' => 'Draft Post', 'user_id' => 1]);
    // Creates an approval record in `pending` state.
    
  2. Approval Process:

    $approval = Approval::where('approvalable_type', Post::class)
                        ->where('approvalable_id', $post->id)
                        ->first();
    
    $approval->approve(); // Persists changes to the model.
    
  3. Conditional Actions:

    $approval->approveIf($post->isReadyForReview());
    $approval->rejectUnless($post->hasValidContent());
    

Integration Tips

  • Foreign Keys: Override getApprovalForeignKeyName() for non-standard keys (e.g., author_id).

    public function getApprovalForeignKeyName(): string { return 'author_id'; }
    
  • Custom States: Extend config/approval.php for workflows like in_review or needs_info.

    'states' => [
        'in_review' => ['name' => 'In Review'],
    ],
    
  • Expiration Handling: Schedule the approval:process-expired command in App\Console\Kernel.php:

    $schedule->command('approval:process-expired')->everyMinute();
    
  • Partial Approvals: Use approvalAttributes to restrict approvals to specific fields:

    protected array $approvalAttributes = ['title', 'content'];
    
  • Rollbacks: Revert changes and reset state:

    $approval->rollback(); // Reverts to original data, sets state to `pending`.
    

Event-Driven Patterns

Listen for lifecycle events to trigger notifications or audits:

use Cjmellor\Approval\Events\ModelApproved;

ModelApproved::listen(function ($model) {
    Notification::send($model->user, new ApprovalGranted($model));
});

Gotchas and Tips

Pitfalls

  1. Missing Foreign Keys:

    • Issue: New models created without the foreign key (e.g., user_id) will lack a creator_id in the approvals table.
    • Fix: Always include the foreign key in create() calls or override getApprovalForeignKeyName().
  2. Schema Migrations:

    • Issue: Upgrading from v1.x to v2.x requires running migrations in order (see UPGRADE.md).
    • Fix: Follow the upgrade guide to avoid data corruption.
  3. Polymorphic Conflicts:

    • Issue: Models with the same approvalable_type but different foreign_key values may cause ambiguity in queries.
    • Fix: Use where('approvalable_type', Model::class)->where('foreign_key', $value) for precision.
  4. Expiration Quirks:

    • Issue: thenCustom() for expired approvals requires manual handling via ApprovalExpired events.
    • Fix: Listen for ApprovalExpired and implement custom logic (e.g., notifications).
  5. Bypass Misuse:

    • Issue: withoutApproval() bypasses all approval checks, including custom attributes.
    • Fix: Use sparingly; prefer approvalAttributes for granular control.

Debugging Tips

  • Inspect Approval Data:

    $approval = Approval::find($id);
    dd($approval->new_data->toArray(), $approval->original_data->toArray());
    
  • Check State Transitions:

    $approval->fresh()->state; // Verify current state after updates.
    
  • Query Performance: Avoid whereHas('approval') on large datasets. Use direct approvals table queries:

    Approval::where('approvalable_type', Post::class)->get();
    

Extension Points

  1. Custom Approval Logic: Override the shouldBeApproved() method in your model to dynamically enable/disable approvals:

    public function shouldBeApproved(): bool
    {
        return $this->isPublished() === false;
    }
    
  2. State Validation: Extend the ApprovalStatus enum (via config/approval.php) to add validation rules:

    'states' => [
        'draft' => ['name' => 'Draft', 'valid_transitions' => ['pending', 'rejected']],
    ],
    
  3. Rollback Callbacks: Listen for ModelRolledBack to trigger side effects (e.g., log changes):

    ModelRolledBack::listen(function ($approval) {
        Log::info("Rolled back {$approval->approvalable_type} ID {$approval->approvalable_id}");
    });
    
  4. Custom Expiration Actions: Create a service to handle thenCustom() logic:

    ApprovalExpired::listen(function ($approval) {
        if ($approval->expiration_action === 'custom') {
            $this->handleCustomExpiry($approval);
        }
    });
    

Config Quirks

  • Default States: The pending state is always the default. Omitting default: true in config/approval.php will not change this.

  • Enum Casting: The state column uses Laravel’s ApprovalStatus enum. Direct string assignments (e.g., state = 'approved') won’t work; use:

    $approval->setState('approved'); // Method call required.
    
  • JSON Columns: new_data and original_data are cast to AsArrayObject. Use ->toArray() to convert to native arrays:

    $data = $approval->new_data->toArray();
    
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
calliostro/spotify-bundle
calmfox/watch-sylius
damienfern/grpc-symfony-bundle
atoolo/index-bundle
atoolo/genai-bundle
coprotoai/laravel-ticket
davidjln/llm-carbon-bundle
cryonighter/valid-request-bundle
coolms/taxonomy-bundle
coolms/field-bundle
articulate-orm/symfony
aaix/laravel-tall-architect
ephoto/akeneo-connector
emmanuelballery/eb-plantumlbundle
emielburgman/symfony-visitor-beacon
emielburgman/symfony-visit-storage
emielburgman/symfony-security-headers
emielburgman/symfony-log-viewer
emarref/xdebug-bundle
emarref/pubnub-bundle