lorisleiva/laravel-actions
Unify your Laravel app logic into single-purpose “Action” classes that can run as controllers, jobs, listeners, or commands. Keep business logic in one place, reduce duplication, and generate actions via artisan with flexible asX entrypoints.
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 pre-loaded.
Define Core Logic:
Implement the handle() method to encapsulate your business logic:
use Lorisleiva\Actions\Concerns\AsAction;
class PublishArticle
{
use AsAction;
public function handle(string $title, string $body): void
{
// Your core logic here
Article::create(['title' => $title, 'body' => $body]);
}
}
First Use Case: Run the action directly as an object:
PublishArticle::run('My Title', 'My Content');
app/Actions/ – Default directory for organizing actions (create this folder if it doesn’t exist).AsAction Trait – Understand the handle() method and asX() methods (e.g., asController, asJob).Single Responsibility Principle (SRP):
Each action class handles one specific task (e.g., PublishArticle, SendWelcomeEmail). Avoid mixing concerns like validation, persistence, and notifications in a single action.
Modular Registration: Register actions in routes, events, or commands without boilerplate:
// Routes
Route::post('/articles', PublishArticle::class);
// Events
Event::listen(NewArticleCreated::class, PublishArticle::class);
// Commands
Artisan::register(PublishArticle::class);
Dependency Injection:
Inject dependencies directly into handle() or use Laravel’s container:
public function handle(ArticleRepository $repository, string $title)
{
$repository->create($title);
}
Replace traditional controllers with actions:
class PublishArticle
{
use AsAction;
public function handle(Request $request): ArticleResource
{
$validated = $request->validate([...]);
return new ArticleResource(Article::create($validated));
}
public function asController(Request $request): ArticleResource
{
return $this->handle($request);
}
}
Route:
Route::post('/articles', PublishArticle::class);
Run actions asynchronously:
class PublishArticle
{
use AsAction;
public function handle(string $title): void
{
// Async logic
}
public function asJob(): void
{
$this->handle('Async Title');
}
}
Dispatch:
PublishArticle::dispatch('Async Title');
Attach actions to events:
class PublishArticle
{
use AsAction;
public function handle(User $user): void
{
$user->articles()->create([...]);
}
public function asListener(NewUserRegistered $event): void
{
$this->handle($event->user);
}
}
Event Listener:
Event::listen(NewUserRegistered::class, PublishArticle::class);
Convert actions into Artisan commands:
class PublishArticle
{
use AsAction;
public function handle(string $title): void
{
// Command logic
}
public function asCommand(): int
{
$this->handle('Command Title');
return self::SUCCESS;
}
}
Registration:
Artisan::register(PublishArticle::class);
Usage:
php artisan publish-article
Use Laravel’s validation directly in handle() or leverage asFormRequest:
use Lorisleiva\Actions\Concerns\ValidatesRequests;
class PublishArticle
{
use AsAction, ValidatesRequests;
protected $rules = [
'title' => 'required|string|max:255',
'body' => 'required|string',
];
public function handle(array $data): Article
{
return Article::create($this->validated());
}
}
Mock actions easily:
public function test_publish_article()
{
$action = PublishArticle::fake();
$action->shouldRun();
$this->assertDatabaseHas('articles', [...]);
}
Use shouldRun() to conditionally execute actions:
class PublishArticle
{
use AsAction;
public function shouldRun(): bool
{
return Auth::check() && Auth::user()->can('publish');
}
public function handle(): void
{
// Logic
}
}
Extend built-in decorators (e.g., for jobs or commands):
use Lorisleiva\Actions\Decorators\JobDecorator;
class CustomJobDecorator extends JobDecorator
{
public function __construct($action)
{
parent::__construct($action);
$this->onFailure(fn ($e) => Log::error($e));
}
}
Configure in AppServiceProvider:
Actions::macro('job', fn ($action) => new CustomJobDecorator($action));
Return API resources directly from asController:
public function asController(Request $request): ArticleResource
{
$article = $this->handle($request->user(), $request->input());
return new ArticleResource($article);
}
Method Naming Conflicts:
Avoid naming methods handle() or asX() in your action class if they conflict with the trait’s methods. Use namespaces or prefixes if needed.
Middleware in Controllers:
If using asController, ensure middleware is applied at the route level (not in the action). The action itself doesn’t inherit middleware from the trait.
Job Failures:
By default, job failures are logged but not re-queued. Use onFailure() in custom decorators to handle retries:
$action->asJob()->onFailure(fn ($e) => $this->release($e));
Circular Dependencies:
Avoid circular references between actions (e.g., ActionA calling ActionB, which calls ActionA). Use dependency injection carefully.
Laravel Version Compatibility:
v2.7.x or earlier.v2.6.0.Action Not Running?:
Verify the handle() method is called by adding a dd() or Log::debug() statement. Check if the asX() method is properly defined (e.g., asController, asJob).
Middleware Issues:
If a controller action fails silently, ensure middleware is registered in RouteServiceProvider or the route definition:
Route::post('/articles', PublishArticle::class)->middleware('auth');
Job Not Dispatching: Confirm the action is registered as a job:
PublishArticle::dispatch()->asJob();
Or use the asJob() method:
PublishArticle::run()->asJob();
Validation Errors:
If validation fails in handle(), ensure ValidatesRequests is used or manually validate inputs:
$validated = $request->validate([...]);
$this->handle($validated);
IDE Autocomplete: Enable IDE helpers for better autocompletion:
composer require --dev barryvdh/laravel-ide-helper
php artisan ide-helper:generate
Custom Directories: Change the default action directory by publishing the config:
php artisan vendor:publish --provider="Lorisleiva\Actions\ActionsServiceProvider"
Update config/actions.php:
'path' => app_path('Actions/Custom'),
Macros:
Extend the Actions facade to add custom macros:
Actions::macro('retry', fn ($action, $times = 3) => {
for ($i = 0; $i < $times; $i++) {
try {
$action->run();
break;
} catch (Exception $e) {
if ($i === $times - 1) throw $e;
sleep(1);
}
}
});
Usage:
How can I help you explore Laravel packages today?