Laravel Actions

repository·main·Indexed 25 days ago

https://github.com/lorisleiva/laravel-actions

A package for organizing application logic into single-purpose PHP classes using the AsAction trait. These classes can be executed as objects, controllers, listeners, jobs, or Artisan commands, reducing boilerplate code across Laravel components. Features include built-in support for authorization, validation, response adapters, and advanced queue orchestration via JobDecorator.

Tokens
10.7K
Snippets
34
Records
60
Agent score
83%

What's inside laravel-actions

  1. Best practices for Action implementation

    main

    To ensure your actions are reusable and testable, follow these patterns:

    1. Logic Placement: Keep all core business logic inside the handle() method.
    2. Separation of Concerns: The handle() method should contain only domain logic. Do not include transport concerns (like HTTP requests, CLI commands, or specific queue configurations) inside handle().
    3. Invocation Preference: Prefer Action::run(...) for better readability in your application code.
    4. Dependency Injection: You can inject actions directly into the constructors of other services to use them via their handle() method.
    // Recommended pattern for a simple action
    final class PublishArticle
    {
        use AsAction;
    
        public function handle(int $articleId): bool
        {
            // Domain logic goes here...
            return true;
        }
    }
    
    // Usage
    $published = PublishArticle::run(42);
  2. Create and define a Laravel Action

    main

    Generate an action class using the Artisan command php artisan make:action <Name>. To enable action functionality, use the AsAction trait within your class. You can define specialized methods like asController, asListener, asJob, or asCommand to allow the action to behave differently depending on how it is invoked. The core logic should typically reside in a handle method.

    class PublishANewArticle
    {
        use AsAction;
    
        public function handle(User $author, string $title, string $body): Article
        {
            return $author->articles()->create([
                'title' => $title,
                'body' => $body,
            ]);
        }
    
        public function asController(Request $request): ArticleResource
        {
            $article = $this->handle(
                $request->user(),
                $request->get('title'),
                $request->get('body'),
            );
    
            return new ArticleResource($article);
        }
    
        public function asListener(NewProductReleased $event): void
        {
            $this->handle(
                $event->product->manager,
                $event->product->name . ' Released!',
                $event->product->description,
            );
        }
    }
  3. Create a Base Action

    main

    To create an action, define a class that uses the Lorisleiva\Actions\Concerns\AsAction trait. Implement the core business logic within the handle(...) method. Keep transport-specific logic (like HTTP responses or CLI output) in adapter methods instead of handle().

    <?php
    
    namespace App\Actions;
    
    use Lorisleiva\Actions\Concerns\AsAction;
    
    class PublishArticle
    {
        use AsAction;
    
        public function handle(int $articleId): bool
        {
            return true;
        }
    }
  4. Use the `WithAttributes` trait for attribute-based input

    main

    Use the WithAttributes trait when an action needs to store and validate input via internal attributes instead of passing them directly as method arguments to handle(). This allows you to manage state within the action instance using methods like fill(), set(), and validateAttributes().

    class CreateArticle
    {
        use AsAction;
        use WithAttributes;
    
        public function rules(): array
        {
            return [
                'title' => ['required', 'string', 'min:8'],
                'body' => ['required', 'string'],
            ];
        }
    
        public function handle(array $attributes): Article
        {
            return Article::create($attributes);
        }
    }
    
    $action = CreateArticle::make()->fill([
        'title' => 'My first post',
        'body' => 'Hello world',
    ]);
    
    $validated = $action->validateAttributes();
    $article = $action->handle($validated);
  5. Recommended testing pattern for Actions

    main

    When testing orchestration and business logic:

    1. Test handle() directly: Test the core business rules inside the action itself.
    2. Test entrypoints for orchestration: When testing controllers, listeners, or other actions that call your target action, use fakes to verify the wiring.
    3. Fake only at the boundary: Avoid over-mocking; only fake the action that is at the boundary of the unit you are currently testing.
  6. Register an action as an event listener

    main

    Laravel Actions do not automatically register themselves as listeners for all events. You must explicitly map the event to the action in your EventServiceProvider (or your application's equivalent event registration mechanism).

    // app/Providers/EventServiceProvider.php
    protected $listen = [
        TaxiRequested::class => [
            SendOfferToNearbyDrivers::class,
        ],
    ];
  7. Expose an Action as an Artisan Command

    main

    To expose an action as an Artisan command, use the asCommand(Command $command) method. This method acts as the entrypoint when the action is executed via the CLI.

    Recommended Pattern:

    1. Define command metadata using properties like $commandSignature and $commandDescription.
    2. Implement asCommand(Command $command) to handle console-specific I/O (e.g., reading arguments from the $command object and printing output).
    3. Keep your core business logic inside the handle(...) method to ensure the action remains decoupled from the console environment.
    use Illuminate\Console\Command;
    
    class UpdateUserRole
    {
        use AsAction;
    
        public string $commandSignature = 'users:update-role {user_id} {role}';
    
        public function handle(User $user, string $newRole): void
        {
            $user->update(['role' => $newRole]);
        }
    
        public function asCommand(Command $command): void
        {
            $this->handle(
                User::findOrFail($command->argument('user_id')),
                $command->argument('role')
            );
    
            $command->info('Done!');
        }
    }
  8. Use an action as an event listener with `asListener`

    main

    To use an action as an event listener, implement the asListener method within your action class. This method acts as an adapter that extracts the necessary data from the event payload and passes it to the core handle method. This pattern keeps your business logic decoupled from the event structure.

    Recommended Pattern:

    1. Define your core logic in the handle(...) method.
    2. Implement asListener(Event $event) to map event properties to handle arguments.
    3. Register the action in your EventServiceProvider.
    class SendOfferToNearbyDrivers
    {
        use AsAction;
    
        public function handle(Address $source, Address $destination): void
        {
            // Core business logic goes here
        }
    
        public function asListener(TaxiRequested $event): void
        {
            // Adapt the event payload to the handle method
            $this->handle($event->source, $event->destination);
        }
    }
  9. Use an Action as an Invokable Controller

    main

    To expose an action through HTTP routes, use the AsController trait. The recommended pattern is to keep domain logic in handle(...) and use asController(...) to manage HTTP adaptation (like extracting request data and returning responses).

    If you need to implement your own __invoke method, you must alias the trait's implementation to avoid breaking Laravel's invokable controller registration.

    class MyAction
    {
        use AsAction {
            __invoke as protected invokeFromLaravelActions;
        }
    
        public function __invoke()
        {
            // Custom behavior...
        }
    }