Write Clean and Maintainable Code with the Action Pattern
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.