Dev Talks

Web Development Tutorials

Laravel REST API Part 7: Refactor to Actions
Laravel

Laravel REST API Part 7: Refactor to Actions

Published on Dec 11, 2024

When working on Laravel projects, organizing your code is essential for readability, maintainability, and scalability. An effective way to achieve this is by refactoring controller logic into action classes. In this guide, we'll walk through the process of refactoring a Laravel controller to use action classes, step by step.

Why Use Action Classes?

We can refactor controller logic in many ways, some use Repositories, some other people prefer to use services. 
From my humble opinion. I may use services for third party integrations. And for other logic I prefer to use actions because it offer several benefits:

  • Organized Code: Each action is placed in its own class, making the codebase more structured.
  • Single Responsibility: Every action handles only one task, adhering to the Single Responsibility Principle.
  • Reusability: Action classes can be reused across different parts of your application. 


Setting Up the Action Class

In this example, we’ll refactor a controller method for listing products into an action class. Here’s how you can do it:

Create the Action Class

First, create a new class named ListProductAction in the appropriate namespace. For better organization, place it under App\Http\Actions\Product. And move the logic in controller to it


<?php
namespace App\Http\Actions\Product;

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

class ListProductsAction
{
    public function execute(Request $request)
    {
        $query = Product::query();

        if ($request->has("product_name")) {
            $query = $query->where("name", "like", $request->product_name . "%");
        }

        if ($request->has("category_id")) {
            $query = $query->where("category_id", $request->category_id);
        }

        if ($request->has("price_from")) {
            $query = $query->where("price", '>=', $request->price_from);
        }

        if ($request->has("price_to")) {
            $query = $query->where("price", '<=', $request->price_to);
        }

        if ($request->has("stock_from")) {
            $query = $query->where("stock", '>=', $request->stock_from);
        }

        if ($request->has("stock_to")) {
            $query = $query->where("stock", '<=', $request->stock_to);
        }

        $query->with('category')
            ->orderBy('name');

        return $query->paginate();
    }
}


The important thing here we should have one public method that is doing one thing. We might use __invoke magic method, but I like to keep it in it's simple form. 

Now, if you want to add more actions I think you know what you should do, right?

Yes, exactly, we might have CreateProductAction, UpdateProductAction and so on. The cool thing here also is that we can call actions inside each others and we can call it from anywhere else.


Use the Action Class in the Controller

Inject the ListProductAction class into your controller and call its execute method.

<?php

namespace App\Http\Controllers;

use App\Http\Actions\Product\ListProductsAction;
use App\Http\Resources\ProductResource;
use Illuminate\Http\Request;

class ProductController extends Controller
{
    public function index(Request $request, ListProductsAction $listProductsAction)
    {
        $products = $listProductsAction->execute($request);

        return ProductResource::collection($products);
    }
}


Testing the Refactored Code

Thanks to unit tests we have write in the previous article we feel comfortable to refactor. And after the refactor we need to make sure that is everything still working as expected, so we need to run test cases


php artisan test


If your test cases pass without errors, the refactor is successful.