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 new model changes for review before they’re persisted. Add approvals to your workflow, store pending records in the database, and promote them once approved. Supports PHP 8.3+ and Laravel 12.4+/13.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation:

    composer require cjmellor/approval
    php artisan vendor:publish --tag="approval-migrations"
    php artisan migrate
    
  2. Apply to a Model: Add the MustBeApproved trait to your Eloquent model (e.g., Post):

    use Cjmellor\Approval\Concerns\MustBeApproved;
    
    class Post extends Model
    {
        use MustBeApproved;
    }
    
  3. First Use Case: Create/update a Post instance. The package automatically:

    • Stores the original and new data in the approvals table.
    • Sets the state to pending (default).
    • Tracks the creator (creator_id/creator_type).

    Example:

    Post::create(['title' => 'Draft Post', 'user_id' => 1]);
    

Key Configuration

  • Foreign Key: Override getApprovalForeignKeyName() if your model uses a non-standard key (e.g., author_id).
  • Custom States: Extend config/approval.php to add states like in_review or needs_info.

Implementation Patterns

Core Workflows

1. Approval Pipeline

  • Trigger: Automatically invoked on create()/update() for models with MustBeApproved.
  • Data Capture: Stores new_data (JSON) and original_data (JSON) in the approvals table.
  • State Management: Defaults to pending; transition via approve(), reject(), or postpone().

Example Workflow:

// Admin approves a pending post
$approval = Approval::pending()->first();
$approval->approve(); // Fires `ModelApproved` event

// Reject with custom message
$approval->reject()->setCustomState('spam');

2. Conditional Approvals

  • Use approveIf(), rejectUnless(), etc., for dynamic state changes:
    $approval->approveIf($post->isFeatured());
    $approval->rejectUnless(auth()->can('admin'));
    

3. Expiration Handling

  • Set time-based actions (e.g., auto-reject after 48 hours):
    $approval->expiresIn(hours: 48)->thenReject();
    
  • Process expired approvals via scheduler:
    // routes/console.php
    Schedule::command('approval:process-expired')->hourly();
    

4. Rollbacks

  • Revert changes and reset state:
    $approval->rollback(); // Reverts to `original_data`, state = `pending`
    $approval->rollback(bypass: false); // Forces re-approval
    

5. Querying Approvals

  • Filter by state, requestor, or expiration:
    // Pending approvals by a user
    Approval::pending()->requestedBy(auth()->user())->get();
    
    // Expired but unprocessed
    Approval::expired()->notExpired()->get();
    

Integration Tips

  • Events: Listen for ModelApproved, ModelRejected, or ApprovalExpired to trigger notifications/emails.
  • Bypassing Approval: Use withoutApproval() for admin-only updates or bulk operations:
    Post::withoutApproval()->update(['status' => 'published']);
    
  • Partial Approvals: Restrict approvals to specific attributes via approvalAttributes:
    class Post extends Model
    {
        use MustBeApproved;
        protected array $approvalAttributes = ['title', 'content'];
    }
    

Gotchas and Tips

Pitfalls

  1. Foreign Key Omission:

    • If creating a model without the foreign key (e.g., user_id), the approvals table will lack the foreign_key column, breaking requestor tracking.
    • Fix: Always include the foreign key in create()/update() calls.
  2. Missing creator_id:

    • The creator_id defaults to the authenticated user. If no user is logged in, it may store null, complicating auditing.
    • Fix: Set a default creator manually:
      $approval->creator_id = $fallbackUserId;
      
  3. JSON Column Limits:

    • Large model attributes may exceed MySQL’s JSON column limits (~64KB). Test with complex models.
    • Fix: Use approvalAttributes to limit stored data or consider splitting attributes.
  4. Event Ordering:

    • Events like ApprovalCreated fire after the approval record is saved. Avoid relying on event data for immediate validation.
    • Fix: Validate data before triggering approvals.
  5. Expiration Edge Cases:

    • Approvals with expires_at in the past may not trigger ApprovalExpired if the scheduler hasn’t run.
    • Fix: Run php artisan approval:process-expired manually during testing.

Debugging Tips

  • Inspect Approval Data:
    $approval->new_data->toArray(); // View JSON payload
    $approval->original_data->diff($approval->new_data); // Compare changes
    
  • Check State Transitions: Use dd($approval->fresh()->state) to verify state changes after updates.
  • Query Performance: Avoid whereHas('approval') on large datasets. Use direct Approval queries instead:
    // Slow
    Post::whereHas('approval')->get();
    
    // Fast
    Approval::where('approvalable_type', Post::class)->get();
    

Extension Points

  1. Custom States:

    • Add states to config/approval.php and use them via setState():
      $approval->setState('needs_info');
      
    • Note: Custom states don’t get built-in scopes (e.g., whereState('needs_info') works, but needsInfo() doesn’t).
  2. Custom Expiration Actions:

    • Extend the thenCustom() behavior by listening for ApprovalExpired:
      event(new ApprovalExpired($approval));
      
    • Implement logic in an event listener.
  3. Rollback Logic:

    • Override the rollback behavior by extending the Approval model:
      class CustomApproval extends \Cjmellor\Approval\Models\Approval
      {
          public function rollback(bool $bypass = true): self
          {
              // Custom logic (e.g., log rollback reason)
              return parent::rollback($bypass);
          }
      }
      
  4. Bulk Operations:

    • Use withoutApproval() for bulk updates to avoid flooding the approvals table:
      Post::withoutApproval()->update(['status' => 'archived']);
      

Configuration Quirks

  • Default States: The pending state is marked as default in config. Remove this if you want all new approvals to start in a custom state.
  • Polymorphic Relations: The approvals table uses approvalable_type/approvalable_id. Ensure your models are properly namespaced (e.g., App\Models\Post).
  • Time Zones: Expiration times (expires_at) use the server’s timezone. Set config('app.timezone') explicitly if needed.

Performance Considerations

  • Indexing: Add indexes to approvalable_type, approvalable_id, and state for large datasets:
    Schema::table('approvals', function (Blueprint $table) {
        $table->index(['approvalable_type', 'approvalable_id']);
        $table->index('state');
    });
    
  • Batch Processing: For high-volume approvals, process expired items in batches:
    Approval::expired()->limit(100)->get()->each(fn ($a) => $a->reject());
    
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.
hamzi/corewatch
minionfactory/raw-hydrator
hexters/coinpayment
rjcodes/rjcms
act-training/laravel-permissions-manager
alimarchal/laravel-chart-of-accounts
babenkoivan/elastic-scout-driver
mkwebdesign/filament-watchdog-v5
renatomarinho/laravel-page-speed
zedmagdy/filament-business-hours
renatovdemoura/blade-elements-ui
devgeek/beacon-admin
benjamin-rqt/data-watcher-bundle
atriumphp/atrium
sandermuller/package-boost-laravel
sandermuller/boost-skills
redaxo/core
yusufgenc/filament-api-forge
l3aro/rating-star-for-filament
leek/filament-subtenant-scope