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

kirschbaum-development/laravel-actions

A Laravel package for organizing reusable action classes that encapsulate business logic, keeping controllers, jobs, and listeners thin and testable. It offers a structured way to define and invoke actions throughout your application.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and discover.

composer require kirschbaum-development/laravel-actions

With zero dependents and a single-digit star count, do not assume the README is a complete specification. Budget 20 minutes to read the source before writing production code:

  1. README.md — the intended happy path (usually 1–2 examples).
  2. src/ — the base Action class and any service provider. This is the entire public API; it is small enough to read in one sitting, and it is the only authoritative answer on method names, hooks, and dispatch behaviour.
  3. composer.json — check require constraints against your Laravel/PHP version, and confirm whether the provider is auto-discovered (extra.laravel.providers). If it is absent, register the provider manually in bootstrap/providers.php.
  4. CHANGELOG.md / tags — the latest release is v0.2.3 (static analysis raised, parameter and return types tightened). There is no ecosystem of blog posts or Stack Overflow answers to fall back on, so the changelog is your only signal.

First use case: evacuate a fat controller.

Pick one controller method with business logic and side effects (a checkout, a webhook handler, an invite flow). Move the body into an action class and have the controller delegate:

// app/Actions/Orders/PlaceOrder.php
namespace App\Actions\Orders;

use App\Models\Order;

class PlaceOrder extends \KirschbaumDevelopment\Actions\Action // verify exact base FQCN in src/
{
    public function handle(array $payload, int $userId): Order
    {
        $order = Order::create([...$payload, 'user_id' => $userId]);

        // side effects, events, notifications

        return $order;
    }
}
// app/Http/Controllers/OrderController.php
public function store(StoreOrderRequest $request, PlaceOrder $placeOrder)
{
    $order = $placeOrder->handle($request->validated(), $request->user()->id);

    return redirect()->route('orders.show', $order);
}

Resolve actions from the container rather than new-ing them — that is what makes constructor injection of repositories, gateways, and clients work, and it is the single habit that determines whether the package helps or hurts you.

Implementation Patterns

One action, one verb, one reason to change. Name them PlaceOrder, CancelSubscription, SyncStripeCustomer. Put them in app/Actions/{Domain}/ so discovery stays predictable. If the class name needs the word "And", it is two actions.

Thin controller, thick action. The rule of thumb: a controller method should be 1–5 lines plus the response. Everything else — validation of business invariants, DB writes, event dispatch, external calls — lives in handle().

Wrap multi-write operations in a transaction inside the action, not the controller. This keeps the guarantee attached to the use case, so the queued path and the synchronous path behave identically:

public function handle(array $payload): Order
{
    return DB::transaction(function () use ($payload) {
        $order = Order::create($payload);
        $this->reserveStock($order);

        return $order;
    });
}

Return domain objects, not arrays. Returning the Order, a DTO, or void gives callers something to assert on and keeps tests readable. Returning ['success' => true] is the first step toward re-implementing exceptions badly. This is also where v0.2.3 helps: the tightened return-type and parameter declarations on the base class mean your overrides are now checked against a concrete signature rather than mixed, so a typo in a return type fails at parse time instead of surfacing as a TypeError three callers deep.

Queued execution. If the package exposes a job/listener adapter (check src/ for dispatch, asJob, ShouldQueue handling), use one action for both the sync and queued paths — but only if handle() is serialisation-safe. See Gotchas. If it does not provide an adapter, compose with Laravel's native queue instead of fighting the package:

// app/Jobs/PlaceOrderJob.php
public function handle(PlaceOrder $action): void
{
    $action->handle($this->payload);
}

Actions compose, but depth must stay shallow. An action calling two or three sibling actions is fine. An action calling an action that calls an action is a stack trace you will read at 2am. Cap it at one level of nesting; anything deeper wants an explicit orchestrator or a domain service.

Test the action, not the route. The main payoff of the pattern is that your business logic becomes unit-testable without HTTP:

it('places an order', function () {
    $order = app(PlaceOrder::class)->handle(['total' => 1000], $this->user->id);

    expect($order)->toBeInstanceOf(Order::class);
    Event::assertDispatched(OrderPlaced::class);
});

Keep one or two feature tests over the controller to prove the wiring, and move the rest of the matrix into action tests. Use Bus::fake() / Queue::fake() around actions to assert dispatch without execution.

Invokable actions as single-purpose routes. For trivial endpoints (health probes, one-shot webhooks) an action as the route handler via __invoke avoids a controller that exists only to be a pass-through.

Integration tip — authorisation belongs with the action, not scattered. If the package ships authorize() / rules() hooks, use them and keep FormRequest for shape validation only; business-rule validation (credit limit, seat availability) goes in the action so the queue path and console path both enforce it.

Gotchas and Tips

Treat this as a vendor-internal package, not a framework. Seven stars and zero dependents means: no Stack Overflow corpus, no third-party blog posts, no community patches. It is also still on 0.x, so SemVer gives you no protection — a 0.3.0 may break you as easily as a 1.0.0 would. Pin an exact version in composer.json ("kirschbaum-development/laravel-actions": "0.2.3"), read the diff before upgrading, and be prepared to fork. A two-hour fork of a 300-line package is a cheaper insurance policy than debugging a breaking change in production.

v0.2.3 changed method signatures — re-test anything that subclasses Action. "Improved parameter and return type declarations" almost certainly means types were added to the base class. In PHP, adding a return type to a parent method is fatal for any child override that omits or widens it, and adding a parameter type is fatal for a child that declares the parameter untyped. If you took the earlier advice and built App\Actions\BaseAction extends Action, run your suite before bumping — the failures will surface as Declaration of ... must be compatible with ... at class-load time, not at call time, so a lazily-autoloaded action you forgot about can take down a route in production. The upside is real: you inherit working PHPStan/Larastan coverage for free, which removes most of the reason to avoid the base class.

Do not install it alongside lorisleiva/laravel-actions. That package owns the same conceptual namespace and, more concretely, both define an Action base class and helpers; the naming collision and the temptation to mix idioms will cost you far more than the second dependency saves. Pick one and commit.

Serialisation is the classic queued-action bug. If handle() receives an Eloquent model and the action is queued, the model is serialised by identifier and re-fetched — fine. But if you capture auth()->user() implicitly, the queue worker has no session and the action silently operates as guest. Always pass the user ID or model explicitly as an argument.

Static/magic call syntax hides from your IDE and from static analysis. If the package offers PlaceOrder::run(...) through __callStatic, PHPStan and your editor lose the signature, and constructor injection is bypassed. Prefer container resolution (app(PlaceOrder::class) / method injection). If you must keep the static sugar, add @method annotations on the class so tooling survives. Note that v0.2.3's stricter types do not fix this — a magic forwarder has no signature to check.

Constructor injection does not reach queued actions the same way. Dependencies added to the constructor are serialised into the job payload unless the package resolves them from the container at handle time. Verify this in src/ before adding a constructor dependency that holds a non-serialisable object (HTTP client, closure, service with state). When in doubt, inject into handle() parameters — the container resolves those on every execution.

Watch the config publication step. If there is a config/laravel-actions.php with an action_path, namespace, or base-class override, publish it early (php artisan vendor:publish) and commit it. Generating into a path the auto-loader does not know about produces a Class not found that looks like a package bug and is not.

Naming collision with your own code. App\Actions\Action, Action, and Kirschbaum...\Action will eventually all appear in one file. Give your base class an unambiguous name (BaseAction, DomainAction) and never let a use statement be the only thing distinguishing them.

Debugging: use Tinker first. php artisan tinker → app(PlaceOrder::class)->handle([...]) reproduces a bug without HTTP, middleware, or CSRF in the way. This is faster than any route-based reproduction and is the strongest argument for the pattern. Drop ray() or dump() inside handle() freely — you are in a plain class, no framework ceremony.

Extension point: your own base class. The highest-leverage move you can make is a single App\Actions\BaseAction that extends the package class (or wraps it) and centralises logging, transactions, timing, and handle() signature conventions. When the dependency is abandoned — a real risk at this adoption level — you swap one extends rather than 80 files. Never let KirschbaumDevelopment\...\Action appear in more than a handful of files. This is also the layer that absorbs signature churn like v0.2.3: one class to fix, not eighty.

Failing loudly beats failing silently. Wrap the package's entry point so a missing action or a bad signature throws in tests rather than returning null in production. Low-adoption packages frequently favour permissive behaviour; you want the opposite. The v0.2.3 typing work is a step in the right direction, but do not assume it covers runtime guards — a mixed return or a nullable parameter will still pass silently.

If the package turns out to be thinner than expected, that is fine. Laravel's container already resolves invokable classes and injects dependencies, so a base Action class with a single handle() method gets you 90% of this package's value in ten lines. Do not spend a sprint integrating a package whose entire contribution is a naming convention — adopt the pattern, and treat the dependency as optional.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource