Write Clean and Maintainable Code with the Action Pattern

author

Herminio Heredia

1 month ago

Tired of seeing bloated controllers and business logic scattered throughout your Laravel project?

We’ve all been there.

At first, everything seems fine, but as the application grows, the code turns into an unmanageable maze—classes with hundreds of lines and methods that do everything… a real disaster.

This is where the Action Pattern comes in. Like a chef organizing ingredients before cooking, this pattern lets you encapsulate your application logic into small, reusable, and easy-to-maintain units.

In this tutorial, I’ll guide you step by step so you can master the Action Pattern in Laravel 11. Not only will you learn the theory, but we’ll also build a practical example and write tests to ensure our code runs like clockwork.

What is the Action Pattern and Why Should You Use It?

Imagine you’re building an online store. You have actions like “add product to cart,” “process payment,” “send confirmation email,” etc. Each of these actions can involve several steps and components.

The Action Pattern allows you to encapsulate each of these actions in a separate class. Instead of having one massive controller to handle everything, you delegate the responsibility to these “Action” classes.

Benefits:

  • Cleaner, more organized code: Goodbye spaghetti code.
  • Greater maintainability: Actions are easy to understand, modify, and extend.
  • Reusability: You can use the same action in different parts of your application.
  • Easier testing: By isolating logic into small units, it’s easier to write unit tests.
  • Better team collaboration: Each developer can work on specific actions without stepping on each other’s toes.

Implementing the Action Pattern in Laravel: A Practical Example

We’re going to build a simple system to manage tasks. Our example will have the following actions:

  • Create a new task.
  • Mark a task as completed.
  • Delete a task.

Step 1: Create the Action Classes

First, we’ll create a app/Actions directory to store our Action classes. Inside this directory, we’ll create three files: CreateTask.php, CompleteTask.php, and DeleteTask.php.

app/Actions/CreateTask.php

<?php

namespace App\Actions;

use App\Models\Task;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;

class CreateTask
{
    /**
     * Creates a new task.
     *
     * @param  array  $input
     * @return Task
     * @throws ValidationException
     */
    public function execute(array $input): Task
    {
        $validator = Validator::make($input, [
            'title' => ['required', 'string', 'max:255'],
            'description' => ['nullable', 'string'],
        ]);

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

        return Task::create($validator->validated());
    }
}

app/Actions/CompleteTask.php

<?php

namespace App\Actions;

use App\Models\Task;

class CompleteTask
{
    /**
     * Marks a task as completed.
     *
     * @param  Task  $task
     * @return void
     */
    public function execute(Task $task): void
    {
        $task->update(['completed' => true]);
    }
}

app/Actions/DeleteTask.php

<?php

namespace App\Actions;

use App\Models\Task;

class DeleteTask
{
    /**
     * Deletes a task.
     *
     * @param  Task  $task
     * @return void
     */
    public function execute(Task $task): void
    {
        $task->delete();
    }
}

Step 2: Call the Actions from the Controller

Now, we’ll modify our TaskController to use these Action classes.

<?php

namespace App\Http\Controllers;

use App\Actions\CompleteTask;
use App\Actions\CreateTask;
use App\Actions\DeleteTask;
use App\Models\Task;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;

class TaskController extends Controller
{
    public function store(Request $request, CreateTask $createTask)
    {
        try {
            $task = $createTask->execute($request->all());
        } catch (ValidationException $e) {
            return redirect()->back()->withErrors($e->errors());
        }

        return redirect()->route('tasks.index')->with('success', 'Task successfully created.');
    }

    public function complete(Task $task, CompleteTask $completeTask)
    {
        $completeTask->execute($task);

        return redirect()->route('tasks.index')->with('success', 'Task marked as completed.');
    }

    public function destroy(Task $task, DeleteTask $deleteTask)
    {
        $deleteTask->execute($task);

        return redirect()->route('tasks.index')->with('success', 'Task successfully deleted.');
    }
}

Step 3: Write Unit Tests

To ensure our actions work properly, we’ll write some unit tests.

<?php

namespace Tests\Feature;

use App\Actions\CreateTask;
use App\Models\Task;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class CreateTaskTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function can_create_a_task()
    {
        $createTask = new CreateTask();

        $task = $createTask->execute([
            'title' => 'New task',
            'description' => 'Task description',
        ]);

        $this->assertInstanceOf(Task::class, $task);
        $this->assertEquals('New task', $task->title);
        $this->assertEquals('Task description', $task->description);
    }

    /** @test */
    public function title_is_required()
    {
        $createTask = new CreateTask();

        $this->expectException(\Illuminate\Validation\ValidationException::class);

        $createTask->execute([
            'description' => 'Task description',
        ]);
    }
}

Diving Deeper into the Action Pattern: Dependency Injection and More

The Action Pattern is not limited to simple classes with an execute method. You can leverage Laravel’s dependency injection to make your actions even more flexible and powerful.

Dependency Injection Example:

Suppose you need to send a notification to the user when a new task is created. Instead of instantiating the notification class inside the action, you can inject it in the constructor.

<?php

namespace App\Actions;

use App\Models\Task;
use App\Notifications\TaskCreated;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;

class CreateTask
{
    /**
     * @param  \App\Notifications\TaskCreated  $notification
     */
    public function __construct(private TaskCreated $notification)
    {
        //
    }

    /**
     * Creates a new task.
     *
     * @param  array  $input
     * @return Task
     * @throws ValidationException
     */
    public function execute(array $input): Task
    {
        // ... (validation)

        $task = Task::create($validator->validated());

        Notification::send($task->user, $this->notification);

        return $task;
    }
}

Error Handling:

You can use custom exceptions to handle specific errors within your actions. This gives you more granular control over your application’s flow.

Organizing into Subdirectories:

As your application grows, you can organize your actions into subdirectories within app/Actions. For example, you could have app/Actions/Tasks, app/Actions/Users, and so on.

Beyond the Basics: Design Patterns and the Action Pattern

The Action Pattern can be combined with other design patterns to create even more elegant solutions.

Chain of Responsibility:

If you have an action involving several steps, you can use the Chain of Responsibility pattern to chain multiple actions together. Each action in the chain is responsible for a specific step.

Command:

The Command Pattern is similar to the Action Pattern but focuses on representing an operation as an object. This allows you, for example, to queue actions for asynchronous execution.

Conclusion: Elevate Your Laravel Code with the Action Pattern

The Action Pattern is a powerful tool for writing clean, maintainable, and scalable Laravel code. By encapsulating your application logic into small, reusable units, you can simplify development and improve your code quality.

Don’t wait any longer! Start implementing the Action Pattern in your Laravel projects and experience the benefits for yourself. Remember that practice makes perfect, so don’t hesitate to experiment and explore different ways to use this pattern.

Herminio Heredia

¡Hola! Soy Herminio Heredia Santos, un apasionado del desarrollo web que ha encontrado en Laravel su herramienta predilecta para crear proyectos increíbles. Me encanta la elegancia y la potencia que...

Subscribe for Updates

Provide your email to get email notifications about new posts or updates.