laravel-actions
Installation
SKILL.md
Laravel Actions
An action is one domain operation in one class with one public method, __invoke(). All business logic lives in actions. Everything that triggers an operation — a controller, job, listener, command or the scheduler — gathers its input and calls one action, nothing more.
Rules
- One operation per class. Named
{Verb}{Subject}Action(CancelOrderAction,GetInvoiceTotalsAction), underapp/Actions/{Entity or Subsystem}/. Never group by verb. - Typed input. Take the models the operation acts on, plus a DTO, enum or value object for everything else. Never an array of request data or a loose run of primitives.
- The acting user is an argument. When the operation depends on who's doing it, the caller passes the user in. Never call
user()orauth()inside an action: it would then only work inside a request. - Return what the caller needs. The model it changed, the thing it created, or
void. Return the model you hold; don't re-query it withfresh(). - Guard first. Business-rule preconditions go in a private
guard()called before any work, usingthrow_unless/throw_ifwith a domain exception factory. A state precondition asks the state:$order->state->canTransitionTo(OrderCancelled::class), or a modelis…()helper. On an event-sourced model the aggregate guards instead, and the action doesn't (laravel-event-sourcing). - Transaction when there's more than one write. Wrap in
DB::transactionwhen the action makes several writes, or a write plus something that must roll back with it. A single write needs none. - Nothing external inside the transaction. HTTP calls, file storage and payments run before or after the closure, never inside it; a rollback can't undo them.
- Reads can be actions too. A read that computes, caches, transforms or calls a service is a
Get…ActionorFetch…Action. A filterable, paginated listing is a query class instead (see laravel-controllers). - Context in mutable private properties. When several private methods need the same input, store it in
private Order $order;, neverreadonly: the same instance is invoked again and again, for example once per item in a loop. resolve(), neverapp().