lorisleiva/laravel-actions
Organize app logic into single-purpose “Action” classes that can run as controllers, jobs, listeners, commands, and more. Define a handle method for the core task, then add asController/asJob/etc wrappers to reuse the same logic across contexts.
Installation:
composer require lorisleiva/laravel-actions
No additional configuration is required beyond this.
Generate Your First Action:
php artisan make:action ProcessPayment
This creates a new class in app/Actions/ with the AsAction trait.
Define Core Logic:
Implement the handle() method to define the core business logic:
use App\Models\User;
use App\Models\Payment;
use Lorisleiva\Actions\Concerns\AsAction;
class ProcessPayment
{
use AsAction;
public function handle(User $user, float $amount): Payment
{
return $user->payments()->create([
'amount' => $amount,
'status' => 'pending',
]);
}
}
First Use Case: Run the action directly as an object:
$payment = ProcessPayment::run($user, 99.99);
php artisan make:action (for scaffolding) and php artisan actions:list (to list all actions).app/Actions/ directory for existing actions in your project.Single Responsibility Principle:
Each action class encapsulates one specific task (e.g., ProcessPayment, SendWelcomeEmail). This replaces controllers, jobs, and listeners with focused, reusable components.
Multi-Context Execution:
Define how the action runs in different contexts using asX methods:
public function asController(Request $request): JsonResponse
{
$payment = $this->handle($request->user(), $request->amount);
return response()->json(['success' => true]);
}
public function asJob(): void
{
$this->handle(auth()->user(), 99.99);
}
Dependency Injection:
Inject dependencies directly into handle():
public function handle(User $user, PaymentGateway $gateway, float $amount)
{
$gateway->charge($user, $amount);
}
Controllers: Register actions as invokable controllers in routes:
Route::post('/pay', ProcessPayment::class)->middleware('auth');
Jobs: Dispatch actions as jobs:
ProcessPayment::dispatch($user, 99.99);
Listeners: Attach actions to events:
Event::listen(OrderPlaced::class, ProcessPayment::class);
Commands: Use actions for CLI tasks:
public function handle()
{
$this->info('Processing payment...');
ProcessPayment::run($this->user, $this->amount);
}
Testing: Mock actions in tests:
$action = Mockery::mock(ProcessPayment::class);
$action->shouldReceive('handle')->once();
Conditional Execution:
Use shouldRun() to gate logic:
public function shouldRun(): bool
{
return auth()->user()->hasActiveSubscription();
}
Validation:
Validate inputs in asController or use validate() helper:
public function asController(Request $request): JsonResponse
{
$validated = $request->validate(['amount' => 'required|numeric']);
return $this->handle($request->user(), $validated['amount']);
}
Transactions:
Wrap handle() in a transaction:
public function handle(): void
{
DB::transaction(fn() => $this->coreLogic());
}
Method Naming Conflicts:
Avoid naming handle() methods in your action that conflict with Laravel’s magic methods (e.g., __invoke). The AsAction trait relies on handle().
Middleware in Controllers:
Middleware registered on routes won’t automatically apply to asController. Manually add middleware in the route definition:
Route::post('/pay', ProcessPayment::class)->middleware('auth');
Job Failures:
If using asJob, ensure jobFailed() is defined to handle failures:
public function jobFailed(Throwable $e): void
{
Log::error("Payment failed: " . $e->getMessage());
}
Circular Dependencies:
Actions should avoid circular dependencies. If ActionA calls ActionB and vice versa, refactor to extract shared logic into a service.
IDE Autocomplete:
Some IDEs may not recognize asX methods. Add PHPDoc annotations:
/**
* @return \Illuminate\Http\JsonResponse
*/
public function asController(Request $request)
Log Inputs/Outputs:
Use Laravel’s logging in handle() to debug:
Log::debug('Processing payment for user:', ['user_id' => $user->id]);
Check Action Execution:
Use actions:list to verify registered actions:
php artisan actions:list
Test Isolation: Test actions in isolation by mocking dependencies:
$action = new ProcessPayment();
$this->mock(User::class)->shouldReceive('payments')->andReturn($mockPayments);
Custom Decorators: Override default decorators (e.g., for jobs) by binding custom classes:
$this->app->bind(
\Lorisleiva\Actions\Jobs\JobDecorator::class,
\App\Actions\Decorators\CustomJobDecorator::class
);
Dynamic Contexts:
Use asDynamic() to handle arbitrary contexts:
public function asDynamic($context): mixed
{
if ($context === 'api') {
return $this->asController(new Request());
}
}
Action Groups:
Organize actions into namespaces or groups (e.g., App\Actions\Payments\ProcessPayment).
Event Listeners:
For complex listeners, use shouldQueue() to defer processing:
public function shouldQueue(): bool
{
return true;
}
Laravel Version Compatibility:
Ensure your Laravel version matches the package’s supported versions (check composer.json). As of v2.10.0, Laravel 13 is supported.
Octane Events:
If using Octane, ensure asListener uses handle() instead of asListener for events:
public function handle(OrderPlaced $event): void
{
// Logic here
}
Windows Compatibility:
The package includes Windows-specific tests. If you encounter path issues, ensure your storage/ and bootstrap/ paths are correctly configured.
Avoid Heavy Logic in handle():
Offload complex operations to services or jobs to keep actions lightweight.
Reuse Actions: Instantiate actions once and reuse them (e.g., in service containers) to avoid overhead:
$action = app(ProcessPayment::class);
$action->handle($user, $amount);
How can I help you explore Laravel packages today?