Refactor Laravel Controllers with Action-Based Approach

Published on | Reading time: 6 min | Author: Andrés Reyes Galgani

Refactor Laravel Controllers with Action-Based Approach
Photo courtesy of Joshua Hoehne

Table of Contents


Introduction

Picture this: You’re working late on a Laravel application, and the clock is ticking toward a looming deadline. You muster the strength to refactor your code, eager to optimize that sprawling controller that has multiple methods for handling similar tasks. Just as you prepare to battle the tangled logic, a whisper of despair emerges — what if there’s a way to simplify it all? 🤔

You’re not alone in this predicament. Many developers stumble upon this situation where their controllers become unwieldy and harder to maintain. It’s a classic case of how excessive code can reduce efficiency, making even trivial tasks feel monumental. Fortunately, in the Laravel ecosystem, there's a lesser-known feature just waiting to be embraced: Action-based Controllers. This technique not only promotes cleaner code but also enhances readability — making it easier for future developers (including yourself) to understand and manipulate.

In the following sections, I’ll guide you through what Action-based Controllers are, how they work, and how you can implement them in your existing projects. With this knowledge, you’ll be armed to tackle complexity and enhance the scalability of your applications. 💪


Problem Explanation

Before diving into how Action-based Controllers can save your day, let’s explore the problem a bit further. Many developers use traditional controllers that handle multiple actions together, leading to bloated methods with excessive logic.

For instance, a standard UserController might look something like this:

class UserController extends Controller
{
    public function index()
    {
        // Retrieves all users
        $users = User::all();
        return view('users.index', compact('users'));
    }

    public function create()
    {
        // Shows a form to create a user
        return view('users.create');
    }

    public function store(Request $request)
    {
        // Saves the new user 
        $user = User::create($request->all());
        return redirect()->route('users.index');
    }

    public function edit($id)
    {
        // Shows a form to edit a user
        $user = User::find($id);
        return view('users.edit', compact('user'));
    }

    public function update(Request $request, $id)
    {
        // Updates the user
        $user = User::find($id);
        $user->update($request->all());
        return redirect()->route('users.index');
    }

    public function destroy($id)
    {
        // Deletes a user
        User::destroy($id);
        return redirect()->route('users.index');
    }
}

This UserController has multiple responsibilities: it retrieves users, handles user creation, modification, and deletion — a classic manifestation of the Single Responsibility Principle violation. Not only does it make the code harder to read, but it also creates logistical problems when changes are required.


Solution with Code Snippet

Enter Action-based Controllers! By leveraging this method, we can refactor our bloated controllers into focused classes, organized by actions. Here’s how you can do it:

  1. Create Action Classes:

    You would start by creating specific action classes for each feature. Here’s how you might define the CreateUserAction:

    namespace App\Actions\User;
    
    use App\Models\User;
    use Illuminate\Http\Request;
    
    class CreateUserAction
    {
        public function execute(Request $request)
        {
            return User::create($request->all());
        }
    }
    

    You can create similar classes for all other actions like UpdateUserAction, DeleteUserAction, etc.

  2. Using the Action Classes in the Controller:

    Next, you’ll modify the UserController to use these action classes:

    class UserController extends Controller
    {
        public function store(Request $request, CreateUserAction $action)
        {
            $user = $action->execute($request);
            return redirect()->route('users.index');
        }
    
        public function update(Request $request, UpdateUserAction $action, $id)
        {
            $action->execute($request, $id);
            return redirect()->route('users.index');
        }
    
        public function destroy(DeleteUserAction $action, $id)
        {
            $action->execute($id);
            return redirect()->route('users.index');
        }
    }
    

In this example, the store, update, and destroy methods make clear calls to respective action classes. Now each action is encapsulated within its class with a single responsibility.

Benefits of This Approach:

  • Enhanced Readability: Each action is responsible for a specific task, making the codebase easier to follow.
  • Testability: Actions can be tested independently without the overhead of dealing with multiple responsibilities.
  • Reusability: If another controller needs to recreate a user, it can just call the CreateUserAction, promoting DRY principles.
  • Maintenance: Changes can be made in a single action class without the risk of affecting unrelated logic.

Practical Application

Consider a scenario where you have multiple resources like Product, Order, or Post. Each of these resources can deploy its action classes following the same principles laid out for User. This modular structure greatly enhances maintainability as your application scales.

For example, if you want to add validation or logging functionalities, you can do so without altering the core logic of the controller or compromising other actions. Simply modify the respective action class!

Example of Integration:

Let’s say you’re using Action-based Controllers in an Order module:

namespace App\Actions\Order;

use App\Models\Order;
use Illuminate\Http\Request;

class AddOrderAction
{
    public function execute(Request $request)
    {
        // Optional: You can add validation here
        return Order::create($request->all());
    }
}

You can then directly inject AddOrderAction into the OrderController, keeping your implementation clean and organized.


Potential Drawbacks and Considerations

While the benefits are noteworthy, it is essential to consider potential drawbacks.

  1. Increased Number of Files: Creating an action for every functionality can lead to a proliferation of classes, which might feel overwhelming.
  2. Complexity for Simple Applications: If your application has relatively few routes or actions, using action classes may introduce unnecessary complexity.

To mitigate these, evaluate your project structure and adopt action classes where they add considerable value. Start small – perhaps just with your most complex controllers – and expand as needed.


Conclusion

To sum it up, embracing Action-based Controllers can transform how you manage complexity in your application. By focusing on single-responsibility classes for each action, you enhance readability, ease maintenance, and improve testability. It's like having a well-organized toolbox where finding the right tool for the job becomes a breeze! 🛠️

As you integrate this approach, you will witness a shift in your perspective on managing code. Your controllers can breathe easy, your fellow developers will thank you, and you might even find a bit of joy in refactoring.


Final Thoughts

Give Action-based Controllers a shot in your next Laravel project! Experiment with separating your logic into focused action classes and see how it affects your code quality. Feel free to share your experiences, insights, or even alternative solutions in the comments below!

And remember, we’re in this together. If you’d like to continue receiving tips and tricks like this, subscribe for more expert insights that elevate your development game! 🚀


Further Reading