Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Actions Laravel Package

lorisleiva/laravel-actions

Laravel Actions lets you write one PHP class that handles a single task and run it as a controller, job, listener, command, or plain object—cutting boilerplate and helping you focus on what your application actually does rather than which files to create.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and generate your first action:

composer require lorisleiva/laravel-actions
php artisan make:action PublishANewArticle

The generated class lands in app/Actions and gets the AsAction trait. The mental model: one class = one task, with handle() as the reusable core and asX() methods as thin adapters that let the same class run as a controller, job, listener, or command.

class PublishANewArticle
{
    use AsAction;

    public function handle(User $author, string $title, string $body): Article
    {
        return $author->articles()->create(compact('title', 'body'));
    }

    public function asController(Request $request): ArticleResource
    {
        return new ArticleResource($this->handle(
            $request->user(),
            $request->get('title'),
            $request->get('body'),
        ));
    }
}

First real use case: take one fat controller method and move it into an action. Register it as an invokable route and you immediately gain the ability to call the same logic as an object from a console command, a queued job, or a test:

Route::post('articles', PublishANewArticle::class)->middleware('auth');

PublishANewArticle::run($author, 'My title', 'My content');

Where to look first: laravelactions.com is the authoritative docs source (the README is deliberately thin). Read the "As Controller", "As Job" and "Testing" pages in that order — those three cover 80% of day-to-day use.

Implementation Patterns

Keep handle() pure and adapter-free. Every asX method should do three things: pull inputs off the framework-specific object, call handle(), and shape the output. Anything that needs Request, Command, or a job decorator belongs in the adapter, never in handle(). This is what makes the action portable across contexts.

Objects. MyAction::run(...$params) executes and returns the result; MyAction::make() instantiates without running; runIf() / runUnless() give you conditional execution at the call site instead of if blocks in the caller.

Controllers. Register the class directly as an invokable route. Two framework behaviours come along for free if you declare them on the action:

public function rules(): array
{
    return ['title' => 'required|string|max:255', 'body' => 'required'];
}

public function authorize(): bool
{
    return $this->user()->can('publish');
}

Validation runs before asController, and $this->validated('title') (added in 2.4.3) reads individual validated keys. Route parameters are injected into authorize() as of 2.12.0, so you can gate on a bound model. Implicit route model binding works normally in asController's signature. If you need a non-invokable multi-method route, you can point a route at an explicit public method — but see the gotchas.

Listeners. Event::listen(NewProductReleased::class, PublishANewArticle::class) routes to asListener(). Implement ShouldQueue on the action and the listener queued, since 2.1.1.

Jobs. Dispatch from anywhere with MyAction::dispatch(...), plus dispatchSync(), dispatchAfterResponse(), dispatchIf(), dispatchUnless(). The returned job decorator lets you configure queue placement fluently:

PublishANewArticle::dispatch($author, $title, $body)->onQueue('publishing');

Job-specific concerns (unique constraints, debouncing, custom decorators) can also be expressed inside the action by type-hinting a job decorator as the first argument, which is the cleanest place for retry/backoff policy that belongs to the action rather than its callers.

Commands. Implement asCommand(Command $command) and declare a signature on the action. Register in bulk with Actions::registerCommands($paths) — useful because actions living in app/Actions are outside the default command discovery path, and if your actions live outside the App namespace, the bundled Lody integration handles discovery.

Testing. The AsFake capability (included via AsAction) is the big win here:

PublishANewArticle::fake();

$this->post('/articles', [...])->assertCreated();

PublishANewArticle::assertRan(fn ($action) => $action->title === 'My title');

Pair this with ::partialMock() when you want to spy on handle() but still exercise validation and authorization, and plain ::mock() when you want to swap the whole class out. For queued assertions, Queue::fake() + assertPushed(MyAction::class) works, including with ShouldBeUnique jobs since 2.1.4.

Integration tip. Inject services via handle()'s signature — the container resolves them per call, which is what keeps the action Octane-safe (since 2.1.5 the package deliberately never caches the container or request on the instance).

Gotchas and Tips

  • Payloads must serialize. Anything passed to dispatch() goes through the queue serializer. Passing a Request, a closure, or an Eloquent collection with loaded relations you forgot about will bite you in production, not in tests. Reduce to scalars/IDs/SerializesModels-backed models before dispatch.
  • run() and dispatch() return different things. run() returns handle()'s value; dispatch() returns the job decorator. Don't write $article = MyAction::dispatch(...) and then call $article->id.
  • Validation and authorization only fire on the invokable controller path. Explicit-method routes deliberately skip them (behaviour set in 2.1.4), and handle() called directly — from a job, command, or unit test — runs none of it. If you need guaranteed validation everywhere, validate inside handle() or use the WithAttributes trait (fill($attributes) + validateAttributes()).
  • Don't let asX methods leak into routing. Only asController is invoked for invokable routes; if you're using explicit method routes, keep those method names well clear of the as* convention or you'll get confusing resolution behaviour.
  • Octane discipline. Don't stash request(), app(), or user state in properties. Fetch them inside the method. The package is Octane-ready, but your action is only as Octane-safe as you make it.
  • Debugging routing. php artisan route:list shows the action class as the handler, not a Controller@method string — that's normal, not a registration failure. If an invokable route 404s, check the class actually resolves (the package guards against non-existent classes when registering routes).
  • Fake-vs-queue interaction. MyAction::fake() intercepts object/controller/job runs of that action; Queue::fake() intercepts the queue itself. Using both and asserting on the wrong one is the most common source of "my assertion passes but the job never ran" confusion. Pick one lane per test.
  • Unique/debounced jobs need real thought. ShouldBeUnique on the action works and is testable, but uniqueness locks are keyed on uniqueId() — if your action's identity depends on arguments, override it, or two logically distinct dispatches will silently collapse into one.
  • Extension points worth knowing. Custom job decorator classes are configurable (2.5.1), any job decorator subclass can be type-hinted in asJob (2.3.0), and there's a shipped PHPStan extension (2.11.0) plus Laravel Boost documentation (2.10.1) — enable the PHPStan rules if you're using static analysis, since decorator magic is otherwise opaque to it.
  • Version floor. Current releases track Laravel 11+ and dropped Laravel 10 in 2.10.0. If you're on an older LTS, pin to a 2.9.x line rather than fighting resolution errors.
  • The honest trade-off. This package optimises for "what does my app do" over "which Laravel primitive do I use", which is excellent for domain-heavy codebases and mildly annoying for teams who expect app/Http/Controllers to contain HTTP concerns. Decide the convention early and enforce it — half-adopted, it produces actions that are really just controllers with extra indirection.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core