lorisleiva/laravel-actions
Laravel Actions organizes your app around single-purpose “action” classes. Write the core logic once in handle(), then run it as a controller, job, listener, command, or plain object. Keep business logic reusable, testable, and consistent across entry points.
Installation:
composer require lorisleiva/laravel-actions
No additional configuration is required.
Create Your First Action:
php artisan make:action PublishArticle
This generates a class with the AsAction trait.
Define Core Logic:
Implement the handle() method to encapsulate the core business logic:
use Lorisleiva\Actions\AsAction;
class PublishArticle
{
use AsAction;
public function handle(string $title, string $body): Article
{
return Article::create([
'title' => $title,
'body' => $body,
]);
}
}
First Use Case: Run the action directly as an object:
$article = PublishArticle::run('My Title', 'My Content');
php artisan make:action for scaffolding.AsAction Trait: Understand the asX() methods (e.g., asController, asJob).Single Responsibility Principle (SRP):
Each action class handles one specific task (e.g., PublishArticle, SendWelcomeEmail).
Example:
class SendWelcomeEmail
{
use AsAction;
public function handle(User $user): void
{
Mail::to($user->email)->send(new WelcomeEmail($user));
}
}
Reusable Logic: Extract shared logic into helper methods or services. Actions can call other actions:
public function handle(User $user, string $content): void
{
$article = $this->createArticle($user, $content);
$this->notifyTeam($article);
}
protected function createArticle(User $user, string $content): Article
{
return Article::create(['user_id' => $user->id, 'body' => $content]);
}
Integration Patterns:
Route::post('/articles', PublishArticle::class);
asJob for async tasks:
public function asJob(): void
{
$this->handle(...);
}
Dispatch with:
PublishArticle::dispatch($title, $body);
Event::listen(NewArticlePublished::class, PublishArticle::class);
public function asCommand(): int
{
$this->handle(...);
return 0;
}
Register in app/Console/Kernel.php:
protected $commands = [
PublishArticle::class,
];
Validation:
Use Laravel’s validation directly in handle() or leverage asController for FormRequest integration:
public function asController(Request $request): ArticleResource
{
$validated = $request->validate([
'title' => 'required|string',
'body' => 'required|string',
]);
return new ArticleResource($this->handle(...$validated));
}
Testing: Mock actions easily:
$action = Mockery::mock(PublishArticle::class);
$action->shouldReceive('handle')->once()->andReturn($article);
Dependency Injection: Inject services via constructor:
class PublishArticle
{
public function __construct(protected Notifier $notifier) {}
public function handle(User $user, string $content): void
{
$article = Article::create(['user_id' => $user->id, 'body' => $content]);
$this->notifier->notify($article);
}
}
Conditional Execution:
Use shouldRun() for guards:
public function shouldRun(): bool
{
return Auth::check() && Auth::user()->can('publish');
}
Custom Decorators:
Extend default decorators (e.g., JobDecorator) for custom behavior:
class CustomJobDecorator extends JobDecorator
{
public function __construct(Closure $action)
{
parent::__construct($action);
}
public function handle(): void
{
Log::info('Custom job started');
parent::handle();
}
}
Register in config/laravel-actions.php:
'decorators' => [
'job' => CustomJobDecorator::class,
],
API Resources:
Return API resources in asController:
public function asController(Request $request): ArticleResource
{
return new ArticleResource($this->handle(...));
}
Inertia.js: Return Inertia responses:
public function asController(Request $request): Inertia\Response
{
return Inertia::render('Articles/Show', [
'article' => $this->handle(...),
]);
}
Method Naming Conflicts:
Avoid naming custom methods handle or asX (e.g., asController) to prevent overrides.
Fix: Use descriptive names like execute() or perform().
Middleware in Controllers:
Middleware registered on routes won’t apply to actions used as objects or jobs.
Fix: Use shouldRun() or inject middleware logic into the action.
Job Failures:
By default, failed jobs throw exceptions. Use jobFailed() to handle failures gracefully:
public function jobFailed(Throwable $e): void
{
Sentry::captureException($e);
}
Circular Dependencies: Actions calling other actions may create tight coupling. Prefer dependency injection over chaining:
// Avoid:
$this->notifyTeam($this->publishArticle($user));
// Prefer:
$this->notifier->notify($this->articleService->publish($user));
Laravel Version Mismatches: Ensure compatibility (e.g., Laravel 11+ requires v2.8.0+). Check the changelog.
Log Action Execution:
Add logging in handle() or decorators:
Log::debug('Action executed with args:', [$title, $body]);
Backtrace Limits:
Increase xdebug.max_nesting_level if stack traces are truncated:
xdebug.max_nesting_level=256
IDE Autocomplete: Enable PHPDoc annotations for better IDE support:
/**
* @param User $user
* @param string $title
* @return Article
*/
public function handle(User $user, string $title): Article
Testing Actions:
Use run() in tests to bypass decorators:
$action = new PublishArticle();
$result = $action->run($user, $title); // Skips asJob/asController logic
Custom Decorators:
Override defaults in config/laravel-actions.php:
'decorators' => [
'job' => App\Decorators\CustomJobDecorator::class,
],
Job Queue Connections:
Specify the queue connection in asJob:
public function asJob(): void
{
$this->onQueue('high_priority');
$this->handle(...);
}
Event Listeners:
Ensure the asListener method signature matches the event’s payload:
public function asListener(NewArticleEvent $event): void
{
$this->handle($event->user, $event->content);
}
Custom Action Traits:
Extend AsAction for shared behavior:
trait LoggableAction
{
public function handle(...$args)
{
Log::info('Action started', $args);
$result = $this->perform(...$args);
Log::info('Action completed', ['result' => $result]);
return $result;
}
abstract protected function perform(...$args);
}
Dynamic Method Resolution:
Use method_exists() to check for asX methods dynamically:
if (method_exists($action, 'asJob
How can I help you explore Laravel packages today?