Try Live
Add Docs
Rankings
Pricing
Docs
Install
Install
Docs
Pricing
More...
More...
Try Live
Rankings
Enterprise
Create API Key
Add Docs
Laravel Queueable Action
https://github.com/spatie/laravel-queueable-action
Admin
Laravel Queueable Action is a package that adds easy support for making Laravel actions queueable,
...
Tokens:
6,202
Snippets:
48
Trust Score:
8.5
Update:
2 weeks ago
Context
Skills
Chat
Benchmark
93
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# Laravel Queueable Action Laravel Queueable Action is a Spatie package that provides an elegant way to structure business logic in Laravel applications using the Action pattern. Actions are reusable classes that encapsulate specific business operations, and this package adds seamless support for making them queueable, allowing developers to execute actions either synchronously or asynchronously on Laravel's queue system with a simple fluent API. The package leverages Laravel's dependency injection container, enabling actions to receive constructor-injected dependencies while still being dispatchable to queues. It supports all standard Laravel queue features including queue selection, job chaining, batching, middleware, custom tags for Horizon, backoff strategies, and retry configurations. Actions can use either an `execute()` method or PHP's magic `__invoke()` method, providing flexibility in how business logic is organized. ## Installation Install the package via Composer. ```bash composer require spatie/laravel-queueable-action ``` ## Publishing Configuration Optionally publish the configuration file to customize the job class. ```bash php artisan vendor:publish --provider="Spatie\QueueableAction\QueueableActionServiceProvider" --tag="config" ``` ```php // config/queuableaction.php return [ /* * The job class that will be dispatched. * If you would like to change it and use your own job class, * it must extend the \Spatie\QueueableAction\ActionJob class. */ 'job_class' => \Spatie\QueueableAction\ActionJob::class, ]; ``` ## Generating Action Classes with Artisan Command The package provides an Artisan command to generate action classes. By default, it creates a queueable action; use the `--sync` flag for synchronous actions. ```bash # Create a queueable action class php artisan make:action SendWelcomeEmail # Create a synchronous action class php artisan make:action ProcessPayment --sync ``` ```php // Generated queueable action: app/Actions/SendWelcomeEmail.php <?php namespace App\Actions; use Spatie\QueueableAction\QueueableAction; class SendWelcomeEmail { use QueueableAction; public function __construct() { // Prepare the action for execution, leveraging constructor injection. } public function execute() { // The business logic goes here. } } ``` ## QueueableAction Trait The core trait that enables queueable functionality on action classes. It provides the `onQueue()` method for dispatching actions to Laravel's queue system. ```php <?php namespace App\Actions; use App\Models\User; use App\Services\EmailService; use Spatie\QueueableAction\QueueableAction; class SendWelcomeEmail { use QueueableAction; protected EmailService $emailService; // Constructor dependencies are resolved from the container public function __construct(EmailService $emailService) { $this->emailService = $emailService; } // Parameters passed at execution time public function execute(User $user, string $subject = 'Welcome!') { $this->emailService->send($user->email, $subject, 'emails.welcome', [ 'name' => $user->name, ]); } } // Usage in a controller class UserController { public function store(Request $request, SendWelcomeEmail $action) { $user = User::create($request->validated()); // Execute on the queue (asynchronous) $action->onQueue()->execute($user, 'Welcome to our platform!'); // Or execute immediately (synchronous) $action->execute($user, 'Welcome to our platform!'); return response()->json(['user' => $user]); } } ``` ## Specifying Queue Name with onQueue() Dispatch actions to specific queues by passing the queue name to `onQueue()` or by setting a `$queue` property on the action class. ```php <?php namespace App\Actions; use App\Models\Order; use Spatie\QueueableAction\QueueableAction; class ProcessOrder { use QueueableAction; // Default queue for this action public string $queue = 'orders'; public function execute(Order $order) { $order->process(); $order->update(['status' => 'processed']); } } // Usage $action = app(ProcessOrder::class); // Uses the default 'orders' queue defined in the class $action->onQueue()->execute($order); // Override with a specific queue name $action->onQueue('high-priority')->execute($order); // Execute immediately without queuing $action->execute($order); ``` ## Using __invoke() Method Actions can use PHP's magic `__invoke()` method instead of `execute()`. The package automatically detects and uses the appropriate method. ```php <?php namespace App\Actions; use App\Models\Product; use App\Services\InventoryService; use Spatie\QueueableAction\QueueableAction; class UpdateInventory { use QueueableAction; protected InventoryService $inventory; public function __construct(InventoryService $inventory) { $this->inventory = $inventory; } public function __invoke(Product $product, int $quantity) { $this->inventory->adjust($product->id, $quantity); $product->update(['stock' => $product->stock + $quantity]); } } // Usage - still use execute() to dispatch, even with __invoke() $action = app(UpdateInventory::class); $action->onQueue()->execute($product, 50); ``` ## ActionJob Class for Chaining Actions Wrap actions in `ActionJob` to chain multiple actions together. The job accepts an action class or instance and an array of parameters. ```php <?php use App\Actions\ProcessOrder; use App\Actions\SendOrderConfirmation; use App\Actions\UpdateInventory; use App\Actions\NotifyWarehouse; use Spatie\QueueableAction\ActionJob; $order = Order::find(1); // Chain multiple actions that execute sequentially app(ProcessOrder::class) ->onQueue() ->execute($order) ->chain([ // ActionJob accepts the action class and an array of parameters new ActionJob(SendOrderConfirmation::class, [$order]), new ActionJob(UpdateInventory::class, [$order->product, -$order->quantity]), new ActionJob(NotifyWarehouse::class, [$order]), ]); // Chain with shared arguments across actions $args = [$order->id, $order->customer_id]; app(ProcessOrder::class) ->onQueue() ->execute($order) ->chain([ new ActionJob(SendOrderConfirmation::class, $args), new ActionJob(UpdateInventory::class, $args), ]); ``` ## Batching Actions with Laravel Bus Use Laravel's Bus facade to dispatch multiple actions as a batch for parallel processing. ```php <?php use App\Actions\ProcessImage; use App\Actions\GenerateThumbnail; use Illuminate\Support\Facades\Bus; use Spatie\QueueableAction\ActionJob; $images = Image::whereNull('processed_at')->get(); // Dispatch actions as a batch Bus::batch( $images->map(fn ($image) => new ActionJob(ProcessImage::class, [$image])) )->then(function ($batch) { // All jobs completed successfully Log::info("Processed {$batch->totalJobs} images"); })->catch(function ($batch, $e) { // First batch job failure detected Log::error("Image processing failed: {$e->getMessage()}"); })->finally(function ($batch) { // Batch has finished executing Notification::send($admin, new BatchCompleted($batch)); })->dispatch(); // Batch multiple different actions together Bus::batch([ new ActionJob(ProcessImage::class, [$image1]), new ActionJob(GenerateThumbnail::class, [$image1, 'small']), new ActionJob(GenerateThumbnail::class, [$image1, 'medium']), ])->dispatch(); ``` ## Custom Tags for Horizon Override the `tags()` method to customize how actions appear in Laravel Horizon for monitoring and filtering. ```php <?php namespace App\Actions; use App\Models\User; use Spatie\QueueableAction\QueueableAction; class SyncUserData { use QueueableAction; protected ?User $user = null; public function execute(User $user) { $this->user = $user; // Sync user data with external service } public function tags(): array { return [ 'sync', 'user-data', 'user:' . ($this->user?->id ?? 'unknown'), ]; } } // In Horizon, jobs will be tagged with: ['sync', 'user-data', 'user:123'] $action = app(SyncUserData::class); $action->onQueue()->execute($user); ``` ## Job Middleware Override the `middleware()` method to add Laravel job middleware for rate limiting, preventing overlaps, or other cross-cutting concerns. ```php <?php namespace App\Actions; use App\Models\User; use Illuminate\Queue\Middleware\RateLimited; use Illuminate\Queue\Middleware\WithoutOverlapping; use Spatie\QueueableAction\QueueableAction; class SendNotification { use QueueableAction; public function execute(User $user, string $message) { $user->notify(new GenericNotification($message)); } public function middleware(): array { return [ new RateLimited('notifications'), new WithoutOverlapping('user-notification'), ]; } } // Configure the rate limiter in AppServiceProvider use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Support\Facades\RateLimiter; RateLimiter::for('notifications', function ($job) { return Limit::perMinute(10); }); // Usage $action = app(SendNotification::class); $action->onQueue()->execute($user, 'Your order has shipped!'); ``` ## Backoff Strategy Configuration Configure retry delays using the `$backoff` property or `backoff()` method for exponential or custom backoff strategies. ```php <?php namespace App\Actions; use App\Services\ExternalPaymentGateway; use Spatie\QueueableAction\QueueableAction; class ProcessPayment { use QueueableAction; // Simple fixed backoff: retry after 5 seconds public int $backoff = 5; public function execute(Order $order) { app(ExternalPaymentGateway::class)->charge($order); } } class ProcessPaymentWithExponentialBackoff { use QueueableAction; public function execute(Order $order) { app(ExternalPaymentGateway::class)->charge($order); } // Exponential backoff: 1s, 5s, then 10s between retries public function backoff(): array { return [1, 5, 10]; } } class ProcessPaymentWithDynamicBackoff { use QueueableAction; protected int $attemptCount = 0; public function execute(Order $order) { $this->attemptCount++; app(ExternalPaymentGateway::class)->charge($order); } // Dynamic backoff based on attempt count public function backoff(): int { return min($this->attemptCount * 2, 30); } } ``` ## Retry Until Configuration Specify a deadline after which the job should stop retrying using the `retryUntil()` method. ```php <?php namespace App\Actions; use DateTime; use Spatie\QueueableAction\QueueableAction; class SendTimeSensitiveAlert { use QueueableAction; public int $tries = 10; public function execute(User $user, string $alertMessage) { $user->notify(new UrgentAlert($alertMessage)); } // Stop retrying after 30 minutes public function retryUntil(): DateTime { return now()->addMinutes(30); } } // The action will retry up to 10 times, but only within a 30-minute window $action = app(SendTimeSensitiveAlert::class); $action->onQueue('alerts')->execute($user, 'Server is down!'); ``` ## Handling Failed Jobs Define a `failed()` method on your action to handle exceptions when a job fails after all retry attempts. ```php <?php namespace App\Actions; use App\Models\Order; use App\Notifications\OrderProcessingFailed; use Spatie\QueueableAction\QueueableAction; use Throwable; class ProcessOrder { use QueueableAction; protected Order $order; public function execute(Order $order) { $this->order = $order; // Business logic that might fail $this->processPayment(); $this->reserveInventory(); $this->sendConfirmation(); } public function failed(Throwable $exception): void { // Log the failure Log::error('Order processing failed', [ 'order_id' => $this->order->id, 'error' => $exception->getMessage(), ]); // Update order status $this->order->update(['status' => 'failed']); // Notify administrators Notification::route('slack', config('services.slack.webhook')) ->notify(new OrderProcessingFailed($this->order, $exception)); } } ``` ## Queue Properties Configuration Configure standard Laravel queue properties directly on your action class. ```php <?php namespace App\Actions; use Spatie\QueueableAction\QueueableAction; class HeavyDataProcessing { use QueueableAction; // Queue connection to use public string $connection = 'redis'; // Queue name public string $queue = 'heavy-processing'; // Maximum number of attempts public int $tries = 3; // Timeout in seconds public int $timeout = 120; // Maximum exceptions before marking as failed public int $maxExceptions = 2; // Delay before first attempt (in seconds) public int $delay = 10; public function execute(Dataset $dataset) { foreach ($dataset->records as $record) { $this->processRecord($record); } } } // All properties are automatically applied when queued $action = app(HeavyDataProcessing::class); $action->onQueue()->execute($dataset); ``` ## Custom ActionJob Class Extend `ActionJob` to create custom job classes with additional functionality. ```php <?php namespace App\Jobs; use Spatie\QueueableAction\ActionJob; class CustomActionJob extends ActionJob { // Add custom properties public bool $deleteWhenMissingModels = true; public function middleware(): array { return [ new \Illuminate\Queue\Middleware\WithoutOverlapping($this->actionClass), ]; } public function tags(): array { return array_merge(parent::tags(), ['custom-job']); } } // Configure in config/queuableaction.php return [ 'job_class' => \App\Jobs\CustomActionJob::class, ]; // Now all queued actions will use your custom job class $action = app(SendEmail::class); $action->onQueue()->execute($user); // Uses CustomActionJob ``` ## QueueableActionFake for Testing The package provides `QueueableActionFake` with PHPUnit assertions for testing queued actions. ```php <?php namespace Tests\Feature; use App\Actions\SendWelcomeEmail; use App\Actions\ProcessOrder; use App\Actions\NotifyWarehouse; use Illuminate\Support\Facades\Queue; use Spatie\QueueableAction\ActionJob; use Spatie\QueueableAction\Testing\QueueableActionFake; use Tests\TestCase; class OrderTest extends TestCase { public function test_order_queues_welcome_email(): void { Queue::fake(); $user = User::factory()->create(); $action = app(SendWelcomeEmail::class); $action->onQueue()->execute($user); // Assert the action was pushed to the queue QueueableActionFake::assertPushed(SendWelcomeEmail::class); } public function test_action_queued_multiple_times(): void { Queue::fake(); $action = app(SendWelcomeEmail::class); $action->onQueue()->execute($user1); $action->onQueue()->execute($user2); $action->onQueue()->execute($user3); // Assert the action was pushed exactly 3 times QueueableActionFake::assertPushedTimes(SendWelcomeEmail::class, 3); } public function test_action_not_queued_on_validation_failure(): void { Queue::fake(); // Simulate validation failure scenario $this->postJson('/orders', ['invalid' => 'data']); // Assert the action was NOT pushed QueueableActionFake::assertNotPushed(ProcessOrder::class); } public function test_action_queued_with_chain(): void { Queue::fake(); $order = Order::factory()->create(); app(ProcessOrder::class) ->onQueue() ->execute($order) ->chain([ new ActionJob(NotifyWarehouse::class, [$order]), new ActionJob(SendWelcomeEmail::class, [$order->user]), ]); // Assert the action was pushed with the expected chain QueueableActionFake::assertPushedWithChain( ProcessOrder::class, [NotifyWarehouse::class, SendWelcomeEmail::class] ); } public function test_action_queued_without_chain(): void { Queue::fake(); app(ProcessOrder::class)->onQueue()->execute($order); // Assert the action was pushed without any chained jobs QueueableActionFake::assertPushedWithoutChain(ProcessOrder::class); } } ``` ## Complete Action Example with All Features A comprehensive example demonstrating all available features in a single action class. ```php <?php namespace App\Actions; use App\Models\Order; use App\Services\PaymentGateway; use App\Services\InventoryService; use App\Notifications\OrderFailed; use DateTime; use Illuminate\Queue\Middleware\RateLimited; use Illuminate\Queue\Middleware\WithoutOverlapping; use Spatie\QueueableAction\QueueableAction; use Throwable; class ProcessOrderComplete { use QueueableAction; // Queue configuration properties public string $connection = 'redis'; public string $queue = 'orders'; public int $tries = 5; public int $timeout = 60; public int $maxExceptions = 3; // Injected dependencies protected PaymentGateway $paymentGateway; protected InventoryService $inventory; protected ?Order $order = null; public function __construct( PaymentGateway $paymentGateway, InventoryService $inventory ) { $this->paymentGateway = $paymentGateway; $this->inventory = $inventory; } public function execute(Order $order): void { $this->order = $order; // Process payment $this->paymentGateway->charge( $order->customer, $order->total_amount ); // Update inventory foreach ($order->items as $item) { $this->inventory->decrement($item->product_id, $item->quantity); } // Mark order as complete $order->update([ 'status' => 'completed', 'processed_at' => now(), ]); } public function middleware(): array { return [ new RateLimited('orders'), (new WithoutOverlapping($this->order?->id ?? 'default')) ->expireAfter(60), ]; } public function backoff(): array { return [5, 15, 30, 60, 120]; // Exponential backoff } public function retryUntil(): DateTime { return now()->addHours(2); } public function tags(): array { return [ 'order-processing', 'order:' . ($this->order?->id ?? 'unknown'), 'customer:' . ($this->order?->customer_id ?? 'unknown'), ]; } public function failed(Throwable $exception): void { if ($this->order) { $this->order->update(['status' => 'failed']); $this->order->customer->notify( new OrderFailed($this->order, $exception->getMessage()) ); } Log::error('Order processing failed', [ 'order_id' => $this->order?->id, 'exception' => $exception->getMessage(), 'trace' => $exception->getTraceAsString(), ]); } } // Usage in a controller class OrderController extends Controller { public function store( OrderRequest $request, ProcessOrderComplete $processOrder ) { $order = Order::create($request->validated()); // Queue the action with chaining $processOrder ->onQueue('high-priority') // Override default queue ->execute($order) ->chain([ new ActionJob(SendOrderConfirmation::class, [$order]), new ActionJob(NotifyWarehouse::class, [$order]), ]); return response()->json([ 'message' => 'Order received and processing', 'order' => $order, ], 202); } } ``` ## Summary Laravel Queueable Action provides a clean, maintainable pattern for organizing business logic in Laravel applications. By encapsulating operations in dedicated action classes with full support for Laravel's queue system, developers can build applications that are both testable and scalable. The package seamlessly integrates with Laravel's dependency injection, allowing actions to receive services through constructor injection while accepting runtime parameters through the `execute()` method. The main use cases include processing orders, sending notifications, syncing data with external services, handling file uploads, and any operation that benefits from being run asynchronously. Integration is straightforward: add the `QueueableAction` trait to your class, define your business logic in an `execute()` or `__invoke()` method, and use `onQueue()` to dispatch to Laravel's queue system. The package works with all Laravel queue drivers (Redis, SQS, database, etc.) and integrates seamlessly with Laravel Horizon for monitoring, making it an excellent choice for production applications requiring reliable background job processing.