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 Action Laravel Package

cleaniquecoders/laravel-action

A lightweight Laravel package for organizing business logic into reusable, testable action classes. Run actions through a clean pipeline with built-in validation, authorization, and hooks, keeping controllers thin and complex workflows easy to reason about and maintain.

View on GitHub
Deep Wiki
Context7

Getting Started

Requirements: Laravel 12 or 13, PHP 8.3+ (CI runs 8.3/8.4), PHPUnit 12 or Pest 4 for the package's own suite. No migration or config publishing is required to start — the value lands the moment you create your first action class.

Install and go:

composer require cleaniquecoders/laravel-action

Read these first, in order:

  1. README.md — the canonical list of artisan commands and the base-class contract.
  2. src/ — the action base class plus any Console/Commands directory. This tells you the exact method name (handle vs execute vs __invoke) and whether a static entrypoint (Action::run(...), Action::make(...)) exists.
  3. composer.json — confirm the illuminate/* constraint is ^12.0|^13.0 before upgrading an app on Laravel 13. This release is the one that adds it.
  4. phpunit.xml.dist — it was rewritten for PHPUnit 12 in this release; copy its schema if you are scaffolding your own package tests.

First use case: move a fat controller method into a single-purpose class and delegate to it.

// app/Actions/CreateInvoice.php
class CreateInvoice extends Action
{
    public function handle(Order $order, array $lines): Invoice { /* ... */ }
}

// Controller
public function store(StoreInvoiceRequest $request, Order $order)
{
    $invoice = CreateInvoice::run($order, $request->validated('lines'));

    return InvoiceResource::make($invoice);
}

If that reads cleanly to your team, the package is earning its keep. If you need more than a naming convention and a make command, check the source before committing — this is a thin, opinionated package, not a framework.

Implementation Patterns

Thin controllers, fat actions. The single highest-value pattern. Controllers do three things: authorize, validate, delegate. Everything else lives in app/Actions. Keep action names verb-first (CreateInvoice, SyncInventory, SendWelcomeEmail) — they read as documentation at the call site.

One action, one public method. Resist adding handle + handleRefund to the same class. When an action grows a second entrypoint, split it. This is what makes the pattern testable and greppable.

Queue them. An action is a natural job body. Either implement ShouldQueue on an invokable action or wrap the call:

class SendWelcomeEmail extends Action implements ShouldQueue
{
    use Queueable;

    public function handle(User $user): void
    {
        Mail::to($user)->send(new WelcomeMail($user));
    }
}

SendWelcomeEmail::dispatch($user);

Constructor injection for collaborators, method injection for the payload. Resolve services (Mailer, StripeClient, repositories) via the constructor so the container wires them; pass request data as handle() arguments. Mixing the two — putting the Request in the constructor — breaks queued dispatch and makes tests awkward.

Compose rather than inherit. For multi-step workflows, have a coordinating action call sub-actions. It stays readable in a stack trace, which inheritance-based approaches do not.

Wrap writes in a transaction at the action boundary, not in the controller. An action is the unit of work; the transaction belongs where the work is defined.

public function handle(Order $order): Invoice
{
    return DB::transaction(function () use ($order) { /* ... */ });
}

Test actions in isolation, no HTTP layer. This is where the pattern pays for itself — an action test needs no route, no middleware, no session.

// Pest 4
it('creates an invoice', function () {
    $invoice = CreateInvoice::run(Order::factory()->create(), [/* lines */]);

    expect($invoice)->toBeInstanceOf(Invoice::class)
        ->and($invoice->lines)->toHaveCount(2);
});

// PHPUnit 12 — attributes, not annotations
#[Test]
public function it_creates_an_invoice(): void { /* ... */ }

Return values, don't mutate by reference. Actions that return the created/updated model compose better and are far easier to assert against.

Gotchas and Tips

Laravel 13 support is brand new and not in the CI matrix. The release adds ^13.0 to the constraints, but the standardized workflow still runs Laravel 12 with PHP 8.4/8.3. That means the L13 path is declared, not proven by the matrix. If you're on Laravel 13, run your own suite against it before trusting it in production; if you're on 12, nothing here changes for you.

PHPUnit 12 will break annotation-style tests. If you are upgrading your own suite alongside this package: @test, @dataProvider, and @covers docblocks are gone or deprecated in favour of #[Test], #[DataProvider], and #[CoversClass]. The rewritten phpunit.xml.dist in this release also drops older schema attributes — diff it against yours rather than eyeballing it, because an invalid schema is a silent-ish failure that shows up as ignored config.

Pest 4 is a major. If your test suite is on Pest 3, don't upgrade Pest to match this package unless you're ready to walk the upgrade guide — plugin versions (pest-plugin-laravel) must move in lockstep, and expectation APIs shifted. The package supporting Pest 4 does not obligate your app to.

Container resolution vs. new. Calling new CreateInvoice() skips constructor injection and silently gives you null dependencies or a fatal. Always use the package's static entrypoint or app(CreateInvoice::class). If you see "too few arguments" in a queued action, this is why.

Queued actions serialize their constructor. Anything you put in the constructor must be serializable — no closures, no Request, no freshly-uploaded UploadedFile. Pass IDs or DTOs and re-resolve inside handle().

Actions are not a validation layer. Keep FormRequest (or an explicit validator call) at the edge. Putting validation inside the action means every caller — commands, jobs, other actions — has to satisfy a contract that was designed for HTTP.

Don't action-ize everything. A three-line query belongs in the controller or a scope. The pattern earns its overhead on orchestration, external I/O, and logic you want to reuse or queue. Applying it uniformly produces a directory of one-method classes that obscures rather than clarifies.

Debugging tip: because actions are just classes, ray()/dump() inside handle() is your fastest path. But wrap long workflows in a transaction first — otherwise a thrown exception mid-way leaves partial state and makes the failure look unrelated to its cause.

Extension points to check in src/: whether the base class offers a before/after hook, a pipe()-style composition helper, or a asController() adapter. If those exist, they're the intended extension surface — don't subclass and override handle() internals to get the same behaviour.

Dev-dependency churn is not runtime churn. The larastan/phpstan/collision bumps in this release affect the package's own CI, not your application. Don't read the changelog as a signal that anything in your vendor/ behaviour changed; the only runtime-relevant line is the illuminate/* constraint.

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.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel