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 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.

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 vendor:publish --tag="approval-config"
    php artisan migrate
    
  2. 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;
    }
    
  3. 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.
    

Implementation Patterns

Core Workflow

  1. Model Creation/Update:

    • Any model with MustBeApproved will trigger approval logic on create()/update().
    • Dirty attributes are stored in new_data and original_data columns.
  2. Approval State Management:

    • Use scopes to query approvals:
      Approval::pending()->get(); // All pending approvals
      Approval::approved()->get(); // All approved
      
    • Manually approve/reject:
      $approval = Approval::find(1);
      $approval->approve(); // Updates model and marks as approved
      $approval->reject();  // Discards changes
      
  3. Conditional Logic:

    • Approve/reject based on conditions:
      $approval->approveIf($post->isValid());
      $approval->rejectUnless($user->isAdmin());
      
  4. Rollbacks:

    • Revert changes and reset state:
      $approval->rollback(); // Reverts to `original_data`, sets state to `pending`
      
  5. Time-Based Actions:

    • Set expiration and define post-expiry behavior:
      $approval->expiresIn(hours: 24)->thenReject();
      // Schedule processing:
      Schedule::command('approval:process-expired')->everyMinute();
      

Integration Tips

  • 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']);
    

Gotchas and Tips

Pitfalls

  1. Missing Foreign Keys:

    • If a model lacks a foreign key (e.g., user_id), the approvals table will store null in creator_id. This can cause issues with requestor queries.
    • Fix: Always include the foreign key when creating models directly.
  2. Custom States in Scopes:

    • The pending() scope excludes custom states (e.g., in_review). Use whereState('pending') for backward compatibility.
    • Fix: Use explicit state queries for custom states:
      Approval::whereState('in_review')->get();
      
  3. Expiration Events:

    • The thenCustom() action does not auto-process expired approvals. You must listen for ApprovalExpired events manually:
      Event::listen(ModelRolledBack::class, function ($event) {
          // Custom logic here
      });
      
  4. Rollback Behavior:

    • By default, rollback() bypasses re-approval (bypass: true). To force re-approval:
      $approval->rollback(bypass: false);
      
  5. Attribute Whitelisting:

    • If $approvalAttributes is defined, only those attributes are tracked. Omitting it tracks all attributes.
    • Gotcha: Forgetting to define $approvalAttributes may lead to unexpected approvals for unrelated attributes.

Debugging Tips

  1. Check Approval Records:

    • Inspect the approvals table to verify stored data:
      $approval = Approval::find($id);
      dd($approval->new_data->toArray());
      
  2. Event Debugging:

    • Listen for events in AppServiceProvider@boot() to debug workflows:
      ModelApproved::listen(function ($event) {
          Log::info('Approved:', $event->approval->approvalable);
      });
      
  3. Expiration Quirks:

    • Ensure the approval:process-expired command is scheduled in App\Console\Kernel.php:
      $schedule->command('approval:process-expired')->everyMinute();
      
  4. Config Overrides:

    • Customize states in config/approval.php:
      'states' => [
          'pending' => ['name' => 'Pending', 'default' => true],
          'in_review' => ['name' => 'Under Review'],
      ],
      

Extension Points

  1. Custom States:

    • Extend the ApprovalStatus enum or use the custom_state column for ad-hoc states:
      $approval->setState('custom_state_name');
      
  2. Rollback Logic:

    • Override rollback behavior by listening to ModelRolledBack events.
  3. Expiration Actions:

    • Implement custom logic for thenCustom() via ApprovalExpired listeners.
  4. Model-Specific Logic:

    • Use model observers or accessors to add domain-specific approval rules:
      protected static function booted()
      {
          static::created(function ($model) {
              if (!$model->isValidForApproval()) {
                  $model->withoutApproval()->save();
              }
          });
      }
      
  5. Testing:

    • Mock approvals in tests:
      $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',
        ],
    ];
    
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.
davejamesmiller/laravel-breadcrumbs
artisanry/parsedown
bower-asset/punycode
bower-asset/inputmask
bower-asset/jquery
bower-asset/yii2-pjax
laravel/nova
spatie/laravel-mailcoach
spatie/laravel-superseeder
laravel/liferaft
nst/json-test-suite
danielmiessler/sec-lists
jackalope/jackalope-transport
twbs/bootstrap4
php-http/client-implementation
phpcr/phpcr-implementation
cucumber/gherkin-monorepo
haydenpierce/class-finder
psr/simple-cache-implementation
uri-template/tests