Open source March 2025
Laractions

Laractions

Laractions

Laractions gives every meaningful operation in your app its own class: one handle() method, one job, called the same way from a controller, a job, a console command or a test.

Installation

composer require edulazaro/laractions

The service provider auto-registers and there is nothing to publish to start writing actions. Tracing is the one feature that needs a migration; see below.

Writing an action

php artisan make:action SendWelcomeEmail

The generator drops the Action suffix on purpose: the App\Actions namespace already says what the class is.

namespace App\Actions;

use EduLazaro\Laractions\Action;

class SendWelcomeEmail extends Action
{
    public function handle(string $email, string $subject): void
    {
        // send the mail...
    }
}

Calling it

You never new an action. create() resolves the instance through the container, so constructor dependencies are injected; run() executes handle().

SendWelcomeEmail::create()->run('user@example.com', 'Welcome');

create() takes the constructor arguments, run() takes the handle() arguments.

Passing arguments

run() maps to handle() by reflection, so the same handle(string $email, string $subject) accepts all three forms:

$action->run('user@example.com', 'Welcome');                           // positional
$action->run(email: 'user@example.com', subject: 'Welcome');           // named
$action->run(['email' => 'user@example.com', 'subject' => 'Welcome']); // keyed by parameter name

One rule worth knowing so it never surprises you: when handle() declares a single array parameter, an array passed to run() goes through whole as that argument rather than being spread. A concrete typed parameter such as handle(File $file) still receives the value itself. It mirrors how PHP hands an array to fn(array $x) versus fn(File $x).

Model-scoped actions

An action often belongs to a record. Generate one bound to a model and it gains a typed property for it.

php artisan make:action SendWelcomeEmail --model=User
namespace App\Actions\User;

class SendWelcomeEmail extends Action
{
    protected User $user;

    public function handle(): void
    {
        Mail::to($this->user->email)->send(new WelcomeEmail());
    }
}

Add HasActions to the model and register the actions it owns, keyed by a short name.

class User extends Model
{
    use HasActions;

    protected array $actions = ['send_welcome' => SendWelcomeEmail::class];
}

$user->action('send_welcome')->run();
$user->action(SendWelcomeEmail::class)->run();  // or by class, no registration

The binding walks the model's class and its parents looking for a property whose name matches the model, which is why a plain protected User $user; is all the wiring needed.

Validation

A $rules array on the action validates the resolved arguments before handle() runs, throwing a ValidationException on failure. An action guards its own inputs even when called from a command or a test that never touched an HTTP request.

Actors and tracing

Any model becomes an actor with the IsActor trait:

$user->act(SomeAction::class)->on($target);

trace() writes an action_traces row with the actor, the target and the params. This is the only part of the package that needs a migration, and it is not loaded automatically:

php artisan vendor:publish --tag=laractions-migrations

Queueing an action

Call dispatch() instead of run() and the action is wrapped in a job, with queue(), delay() and retry() to configure it.

SendWelcomeEmail::create()
    ->queue('emails')
    ->dispatch('user@example.com', 'Welcome');

built and maintained by Edu Lazaro · MIT license