Refactoring to Action Classes in Laravel

· 3 min read

As Laravel applications grow, we often face the "Fat Controller" or "God Service" problem. Controllers become cluttered with validation, business logic, and response formatting. Service classes grow indefinitely, handling loosely related tasks like createUser, updateUser, notifyUser, exportUser, etc.

Action Classes (or Single Action Controllers) are a powerful pattern to solve this. An Action Class does exactly one thing.

What is an Action Class?

It's a simple PHP class that performs a single task. It typically has one public method, often named execute, handle, or __invoke.

The Problem: Bloated Controller

// UserController.php

public function store(Request $request)
{
    $data = $request->validate([...]);
    
    // Business logic mixed with controller logic
    $user = User::create([
        'email' => $data['email'],
        'password' => Hash::make($data['password']),
    ]);
    
    if ($request->has('newsletter')) {
        Newsletter::subscribe($user->email);
    }
    
    Mail::to($user)->send(new WelcomeEmail());
    
    return redirect()->route('dashboard');
}

This is hard to test and reuse (e.g., if you want to create a user from an Artisan command).

The Solution: CreateUserAction

Let's extract the business logic into a dedicated class.

// app/Actions/User/CreateUserAction.php

namespace App\Actions\User;

use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeEmail;

class CreateUserAction
{
    public function execute(array $data, bool $subscribeToNewsletter = false): User
    {
        $user = User::create([
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
            'name' => $data['name'],
        ]);

        if ($subscribeToNewsletter) {
            // Suppose we have a separate action or service for this
            Newsletter::subscribe($user->email);
        }

        Mail::to($user)->send(new WelcomeEmail());

        return $user;
    }
}

Refactored Controller

Now the controller is purely an HTTP interface.

// UserController.php

public function store(Request $request, CreateUserAction $createUser)
{
    $validated = $request->validate([
        'name' => 'required',
        'email' => 'required|email|unique:users',
        'password' => 'required|min:8',
        'newsletter' => 'boolean'
    ]);

    $user = $createUser->execute(
        $validated, 
        $request->boolean('newsletter')
    );

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

Benefits of Actions

  1. Reusability: You can call CreateUserAction from a Controller, an Artisan Command, a Job, or a Seeder.
  2. Testability: You can write unit tests for the Action without worrying about HTTP requests, sessions, or redirects.
  3. Readability: The code describes what it does. Scannability is improved.
  4. Composition: Actions can use other Actions. e.g., CreateUserAction might call SubscribeToNewsletterAction.

Handling "Format Agnostic" Logic

Some developers prefer to make the Action an "invokable" controller.

// app/Actions/User/CreateUser.php

class CreateUser
{
    public function __invoke(UserData $data) 
    {
        // ...
    }
}

While convenient, I prefer separating the HTTP layer (Controller) from the Domain layer (Action). Keep your Actions framework-agnostic where possible (not returning redirects or JSON responses).

When not to use it?

Don't over-engineer simple CRUD. If your controller just does Post::create($request->all()), you probably don't need an action class yet. Introduce patterns when the pain of complexity is felt.

Comments