laravel-actions: one class per task, runnable as controller, job, listener or command
⚡️ Laravel components that take care of one specific task
At a glance
- What is it?
- Laravel Actions moves application logic into classes that expose asController, asJob, asListener and asCommand methods, so the same task can be invoked from an HTTP route, a queue worker or the console. The idea is clean; the trade-offs are in how much of Laravel's wiring you give up.
- Who is it for?
- Use laravel-actions when the same piece of work genuinely needs to be reachable from more than one entry point, and when your team is comfortable reading a class that implements several asX methods. Skip it if your application is mostly thin CRUD controllers, or if you rely on Laravel's own scaffolding and route-level middleware conventions and do not want a package mediating between your class and the framework.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 36 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What problem laravel-actions is actually solving
The README states the motivation directly: instead of asking "What controllers do I need?", "should I make a FormRequest for this?", "should this run asynchronously in a job instead?", the package wants you asking "What does my application actually do?". That reframing is the whole product. In a conventional Laravel codebase, one unit of business logic often ends up spread across a controller method, a queued job class, an event listener and an artisan command, each with its own constructor signature and its own way of receiving input. The logic itself gets duplicated or extracted into a service class that the four wrappers then call.
Laravel Actions proposes collapsing the wrappers. You write one class with a handle method holding the logic, plus optional asController, asJob, asListener and asCommand methods that adapt that logic to each context. The package's own phrasing is that it lets you "create a PHP class that handles a specific task and run that class as anything you want". The audience is Laravel developers on medium to large applications where the same operation is triggered from a web request, a queued job and a console command, and where keeping those three in sync has become maintenance work.
The framing has a cost worth naming early: a class that can be a controller and a job and a listener is a class with three reasons to change. The package bets that the shared handle method outweighs that.
How the asX methods and the run method fit together
The mechanism is method naming plus a trait. A class uses AsAction and defines handle with whatever signature the task needs. The asX methods are the adapters: asController takes a Request and returns something a route can send back, asListener takes an event object, asJob and asCommand follow the same pattern. The README's example action, PublishANewArticle, has a handle that creates an article from an author, title and body, an asController that pulls those three values off the request and returns an ArticleResource, and an asListener that pulls them off a NewProductReleased event.
The same class is then reachable four ways. As an object, PublishANewArticle::run($author, 'My title', 'My content') calls handle directly. As a controller, the class name goes straight into a route definition. As a listener, the class name goes into Event::listen. The README says jobs and commands are supported on the same pattern and points to laravelactions.com for the full surface, so the exact signatures for asJob and asCommand are not in the README itself.
The design decision that matters is that the class, not the framework, owns the input translation. Laravel normally hands a controller a Request and a listener an event; here one class has to know how to read both. That is what makes the reuse possible, and it is also where the class starts accumulating knowledge of contexts it may never run in.
Installing laravel-actions and running a first action
Installation is a single Composer command, per the README. There is no published config file or migration step mentioned in the README, so nothing needs to be published before use.
composer require lorisleiva/laravel-actionsOnce installed, the package registers a make:action artisan command. The README gives this as the way to create your first action:
php artisan make:action PublishANewArticleYou should end up with a class file that uses the AsAction trait. Fill in handle with the task itself, then add the asX method for the context you want to run it in. The README's controller adapter looks like this, reading the author from the authenticated user and the remaining fields from the request:
public function asController(Request $request): ArticleResource
{
$article = $this->handle(
$request->user(),
$request->get('title'),
$request->get('body'),
);
return new ArticleResource($article);
}Register it as an invokable controller in a routes file. The class name is the handler; there is no controller method to name:
Route::post('articles', PublishANewArticle::class)->middleware('auth');A POST to articles with a title and body and an authenticated user should create the article and return the resource. The same class can be called without HTTP at all, which is the point of the package:
PublishANewArticle::run($author, 'My title', 'My content');The listener path and where the abstraction gets thin
The event side is where the trade-off is easiest to see. Registering an action as a listener is one line, and the README gives it exactly:
Event::listen(NewProductReleased::class, PublishANewArticle::class);Dispatching the event then routes into the action's asListener method, which in the README's example ignores the request entirely and reads from the event object instead. That is the payoff: one handle, two callers, no duplicated article-creation logic.
The limitation is that the class now carries an asListener method that only makes sense if the event is dispatched, and an asController method that only makes sense if a route points at it. Nothing in the README describes what happens when an action is registered as a listener but has no asListener method, or when a route points at a class whose asController signature does not match what the route passes. Those are the failure modes you would want documented before putting this at the centre of an application, and the README is silent on them. The documentation site is the place to check.
There is a second, quieter cost. Laravel developers read controllers by looking at the route file and the controller together, and read jobs by looking at the dispatch site. With actions, the route file, the event registration and the dispatch site all point at the same class name, so grepping the class name is how you find every entry point. That is a real improvement for discovery and a real change in habit.
laravel-actions compared with a service class
The most common alternative in Laravel codebases is a plain service class injected into a controller, a job and a listener, with the controller and job doing their own input translation. The difference is where the adapters live. With a service class, the controller method and the job's handle method both exist as separate files, and each one is a small wrapper around the service call. With laravel-actions, those wrappers become asController and asJob methods on the same class, and the class is addressable by name from a route definition and from Event::listen without an intermediate registration.
That is not automatically better. A service class is a normal PHP object with a constructor, so you can inject it, mock it, and type-hint it anywhere. An action is reached through static entry points like run and through framework registration, which changes how you substitute it in tests. The README mentions mocking actions in tests as a supported feature but does not show the API, so if test substitution is central to your workflow, read laravelactions.com before committing.
The other comparison worth making is against Laravel's own scaffolding. Controllers, jobs and listeners are generated for you, documented by the framework, and understood by anyone who has worked in Laravel. laravel-actions replaces that with a package convention. The gain is coherence across entry points; the loss is that a new hire has to learn one more convention before reading your application.
Maintenance, versioning and the MIT licence
The repository is not archived, and the last push was on 2026-08-25, which is recent enough that the project is being worked on. The release history supports that: v2.12.0 landed on 2026-08-25, v2.11.0 on 2026-08-17, and v2.10.2 on 2026-07-26. Those are close together, which is worth noting if you pin versions, because upgrading across several minor releases at once is a different exercise from tracking them.
The package is MIT licensed, which is permissive and places few obligations on how you use it in a commercial application. This is not legal advice; read LICENSE.md in the repository if the licence terms matter to your organisation.
The upgrade cost is mostly the cost of the Laravel version underneath. A package that integrates at the level of routing, events, queues and the console touches several framework subsystems, so a major Laravel upgrade is the moment to expect friction. The repository ships phpstan.neon.dist, phpunit.xml.dist and a test suite under tests/, which is a signal that static analysis and tests are part of the workflow, but the README does not state a supported Laravel version range, so check composer.json before upgrading Laravel.
Editorial conclusion
Use laravel-actions when the same piece of work genuinely needs to be reachable from more than one entry point, and when your team is comfortable reading a class that implements several asX methods. Skip it if your application is mostly thin CRUD controllers, or if you rely on Laravel's own scaffolding and route-level middleware conventions and do not want a package mediating between your class and the framework. Before adopting, verify three things in a scratch project: that php artisan make:action registers the command after the package is installed, that Route::post('articles', PublishANewArticle::class) resolves your action as an invokable controller, and that Event::listen(NewProductReleased::class, PublishANewArticle::class) reaches the asListener method. The README documents none of the failure behaviour around those three, so confirm them yourself rather than assuming.
Frequently asked questions
What is laravel-actions?
It is a Laravel package that introduces classes taking care of one specific task, which you can run as an object, a controller, a job, a listener or a command. The README describes the goal as organising application logic around what the application actually does rather than around the framework constructs it runs in.
How does laravel-actions compare with running the same logic as a job?
With laravel-actions the logic lives in a handle method and the job becomes an asJob adapter on the same class, so the queued path and the synchronous path share one implementation. The README lists jobs among the supported contexts and points to laravelactions.com for the details of that adapter.
Should I use laravel-actions or a service class?
A service class is a normal injectable object that controllers, jobs and listeners each wrap separately; an action folds those wrappers into asX methods on one class that is addressable by name from a route definition and from Event::listen. The README does not compare the two, so the decision rests on whether your task is genuinely reachable from several entry points.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/lorisleiva-laravel-actions)