Back to all articles
Laravel Insights Jan 26, 2026 โˆ™ 1 min read

A Complete Guide to Laravel Actions for Cleaner Code

Streamline Your Laravel Codebase with Actions: A Comprehensive Guide with Real-World Examples

A flowchart diagram showing a complex process being broken down into simple, single-step action blocks.

A Guide to Refactoring with Laravel Actions

As Laravel applications scale, business logic can quickly clutter controllers and models, making them difficult to maintain and test. The Action pattern offers a structured solution by encapsulating specific tasks into single-responsibility classes. This approach streamlines your codebase, enhances reusability, and clarifies the core operations of your application.

This guide provides a complete overview of Laravel Actions. We will explore what they are, how to implement them, and when they are most effective. With practical, real-world examples, you will learn how to refactor complex logic into clean, testable, and reusable Action classes.

What Are Laravel Actions?

Laravel Actions are dedicated classes that each perform a single, specific business task. Think of an action as a self-contained unit of logic. Instead of placing the code for creating a user, sending a notification, or processing a payment inside a controller, you extract it into an Action class like CreateUser, SendWelcomeNotification, or ProcessPayment.

This pattern aligns with the Single Responsibility Principle (SRP), which states that a class should have only one reason to change. By isolating business logic, Actions become highly reusable and can be invoked from controllers, queued jobs, event listeners, or even console commands without duplicating code.

The primary benefit is a shift in focus from "Where does this code go?" to "What does my application do?". This leads to a more organized and intuitive architecture.

Getting Started with Laravel Actions

While Laravel does not have a built-in make:action command, the community has widely adopted this pattern. A popular and powerful tool is the lorisleiva/laravel-actions package, which we will use in our examples. It supercharges the pattern by allowing an Action class to be used interchangeably as a controller, job, listener, and more.

First, install the package via Composer:

composer require lorisleiva/laravel-actions

Next, create a dedicated directory for your actions at app/Actions. You can further organize this directory by domain, such as app/Actions/Users or app/Actions/Billing.

Creating Your First Action

Let's create a simple Action to handle user registration. This task typically involves validating input, creating a new user record, and perhaps sending a welcome email.

app/Actions/RegisterUser.php

<?php

namespace App\Actions;

use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Lorisleiva\Actions\Concerns\AsAction;

class RegisterUser
{
    use AsAction;

    public function handle(array $data): User
    {
        // 1. Validate the incoming data
        $this->validate($data);

        // 2. Create the user
        $user = User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
        ]);

        // 3. Optional: Dispatch a job to send a welcome email
        // SendWelcomeEmail::dispatch($user);

        return $user;
    }

    private function validate(array $data): void
    {
        $validator = Validator::make($data, [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ]);

        if ($validator->fails()) {
            throw new ValidationException($validator);
        }
    }
}

Key Points:

  • The AsAction trait from the package provides all the necessary functionality.
  • The handle() method contains the core business logic.
  • Validation is performed directly within the Action, keeping the controller clean. If validation fails, a ValidationException is thrown, which Laravel handles automatically.

Using Actions in Different Contexts

The true power of this pattern is its versatility. Let's see how to use our RegisterUser action.

1. As a Standard Class in a Controller

You can inject the Action into a controller and call its handle() method. This is the most straightforward approach.

app/Http/Controllers/Auth/RegisteredUserController.php

<?php

namespace App\Http\Controllers\Auth;

use App\Actions\RegisterUser;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class RegisteredUserController extends Controller
{
    public function store(Request $request, RegisterUser $registerUser)
    {
        $user = $registerUser->handle($request->all());

        Auth::login($user);

        return redirect()->route('dashboard');
    }
}

The controller's responsibility is now limited to handling the HTTP request and response. All business logic resides in the RegisterUser action.

2. As a Controller

The laravel-actions package allows an Action to function directly as a controller. Simply add a route pointing to the Action class.

First, add the AsController trait to your action.
app/Actions/RegisterUser.php

use Lorisleiva\Actions\Concerns\AsAction;
use Lorisleiva\Actions\Concerns\AsController; // Add this trait

class RegisterUser
{
    use AsAction;
    use AsController; // And use it

    // ... handle method remains the same
}

Now, define the route in routes/web.php:

use App\Actions\RegisterUser;

Route::post('/register', RegisterUser::class);

When a POST request hits /register, Laravel will automatically resolve the RegisterUser action from the container and execute its handle() method.

3. As a Queued Job

For long-running tasks, you can run an Action as a background job. This is ideal for tasks like generating reports, processing large files, or sending bulk notifications.

Let's create an action to publish a blog post, which also notifies subscribers.

app/Actions/PublishPost.php

<?php

namespace App\Actions;

use App\Models\Post;
use Lorisleiva\Actions\Concerns\AsAction;
use Lorisleiva\Actions\Concerns\AsJob; // Add this trait

class PublishPost
{
    use AsAction;
    use AsJob; // And use it

    public function handle(Post $post)
    {
        if ($post->isPublished()) {
            // Avoid re-publishing
            return;
        }

        $post->update(['published_at' => now()]);

        // This could be another action or a Mailable
        // NotifySubscribers::dispatch($post);
    }
}

Now, you can dispatch this action just like a regular job:

use App\Actions\PublishPost;
use App\Models\Post;

$post = Post::find(1);

// Dispatch the action to the queue
PublishPost::dispatch($post);

// Or dispatch it synchronously
PublishPost::dispatchSync($post);

Real-World Example: A Multi-Step Workflow

Actions excel at composing complex workflows from smaller, reusable units. Imagine a workflow for when a user purchases a subscription. The process involves:

  1. Processing the payment.
  2. Creating the subscription record.
  3. Assigning a role to the user.
  4. Sending an invoice.

Instead of one massive class, we can create a primary "workflow" Action that orchestrates other, more focused Actions.

app/Actions/PurchaseSubscription.php

<?php

namespace App\Actions;

use App\Models\Plan;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Lorisleiva\Actions\Concerns\AsAction;

class PurchaseSubscription
{
    use AsAction;

    public function handle(User $user, Plan $plan, string $paymentToken)
    {
        return DB::transaction(function () use ($user, $plan, $paymentToken) {
            // Action 1: Process the payment
            $charge = ProcessPayment::run($user, $plan->price, $paymentToken);

            // Action 2: Create the subscription record
            $subscription = CreateSubscription::run($user, $plan);

            // Action 3: Assign the new role to the user
            AssignRoleToUser::run($user, 'subscriber');

            // Action 4: Send the invoice via email
            SendInvoice::dispatch($user, $charge);

            return $subscription;
        });
    }
}

This approach makes the entire workflow readable and easy to manage. Each step is an isolated, testable Action. If you need to change how invoices are sent, you only need to modify the SendInvoice Action without touching the main purchase logic.

Advantages and Best Practices

Key Advantages

  1. Clean Controllers: Keeps controllers slim by delegating business logic.
  2. Reusability: A single Action can be used in controllers, jobs, tests, and console commands.
  3. Testability: Isolated logic is much easier to unit test. You can test an Action without needing to simulate an HTTP request.
  4. Readability: Descriptive class names like DeactivateUserAccount make the code self-documenting.
  5. Maintainability: When business rules change, you know exactly which file to edit.

Best Practices

  • Keep Actions Focused: An Action should do one thing well. If it becomes too complex, break it down into smaller, composable Actions.
  • Use Descriptive Naming: The class name should clearly state what the Action does.
  • Leverage Dependency Injection: Inject any dependencies (like repositories or other services) into the constructor or handle method.
  • Use Database Transactions: For multi-step Actions that modify the database, wrap the logic in a DB::transaction() to ensure data integrity.

Conclusion

Laravel Actions provide a powerful pattern for organizing business logic in a clean, reusable, and testable way. By extracting tasks into dedicated classes, you can significantly improve the architecture of your application, making it easier to scale and maintain. Whether you are building a new feature or refactoring a "fat" controller, consider using Actions to streamline your code and clarify its intent.


Related articles

Continue exploring Laravel insights and practical delivery strategies.

Laravel consulting

Need senior Laravel help for this topic?

Let's adapt these practices to your product and deliver the next milestone.