How to write clean Controllers in Laravel
Tags: laravel, php, beginners, cleancode
Published: 2023-05-25 08:55:10

As a beginner Laravel developer - writing clean controllers is crucial. In my experience, I have come across a lot of laravel projects where the controllers are 1000s of lines long. It should not have to be this way.

![Bad Contollers written by junior devs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gz7m41he38fjvw3h85w1.jpeg)

Writing maintainable and efficient controllers isn't hard. You just have to know how to organize your codebase. In this article, we will explore essential tips to help you write cleaner controllers.

## 3 tips for cleaner laravel controllers :

1. Smaller Methods with Single Responsibility
    
2. Validation with Form Request Class
    
3. Eloquent API Resources
    

We'll use a simple example of an e-commerce product management system to explain each point. So let's dive in!


## 1 - Smaller Methods with Single Responsibility

A controller method should have a single responsibility. Handling a specific HTTP request and returning the appropriate response.

You should avoid adding complex logic to your controller methods. Decouple the request-handling part of our application from the complex business logic. Your controller should only be responsible for "controlling" the flow of execution.

Let's understand this with an example :

```php
class ProductController extends Controller
{
    // ...

    public function update(UpdateProductRequest $request, $id)
    {
        $product = Product::findOrFail($id);

        $data = $request->validated();

        $product = Product::findOrFail($id);

        if ($request->hasFile('image')) {
            $image = $request->file('image');
            $imagePath = 'uploads/products/' . $image->hashName();
            $image->storeAs('public', $imagePath);
            $product->image = $imagePath;
        }

        $product->name = $request->input('name');
        $product->price = $request->input('price');
        $product->category_id = $request->input('category_id');
        $product->description = $request->input('description');
        $product->save();

        return redirect()->route('products.index')->with('success', 'Product updated successfully.');
    }
        
    // ...
}
```

Here `update` method here is responsible for multiple things. It also handles file uploads and storing images which is not its primary concern. It should only focus on 'updating' product data.

Let's see how we can clean up our `update` method

```php
class ProductController extends Controller
{
    private $productService;

    public function __construct(ProductService $productService)
    {
        $this->productService = $productService;
    }

    // ...

    public function update(UpdateProductRequest $request, $id): RedirectResponse
    {
        $product = Product::findOrFail($id);

        $data = $request->validated();

        if ($request->hasFile('image')) {
            $image = $request->file('image');
            $data['image'] = $this->productService->storeProductImage($image);
        }

        $this->productService->updateProduct($product, $data);

        return redirect()->route('products.index')->with('success', 'Product updated successfully.');
    }
}
```

Note that we still have to update the `Product` model and upload the image. But we do that in a separate class `ProductService` that handles these specific operations.

> Create Service Classes or Actions to handle complex business logic

This is a very simple example but you can imagine a more complex feature that involves probably 5 different operations based on the request and model. It can get very complicated very quickly.

**Key Takeaways :**

* Stick to a single responsibility for each controller action
    
* Keep your methods focused and concise, performing a specific task
    
* Break down complex operations into smaller, reusable methods
    


## 2 - Validation with Form Request Class

Validating incoming request data is essential for ensuring data integrity and security. But if you are not careful - the validation logic can get complex making code unreadable.

You can create custom form request classes to keep your controllers clean. This separates validation logic from the controller itself. Laravel provides a great way to overcome this - Request classes.

Let's explore this. First, consider `store` method of the `ProductController`. This is your typical validation logic implemented within the controller action.

```php
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Product;

class ProductController extends Controller
{
    public function store(Request $request)
    {
        // Validate the request data
        $validatedData = $request->validate([
            'name' => 'required|string|max:255',
            'description' => 'required|string',
            'price' => 'required|numeric|min:0',
            'quantity' => 'required|integer|min:0',
            'category_id' => 'required|exists:categories,id',
            'brand_id' => 'required|exists:brands,id',
            'color' => 'required|string|max:50',
            'size' => 'required|string|max:20',
            'weight' => 'required|numeric|min:0',
            'images' => 'required|array',
            'images.*' => 'required|image|max:2048',
        ]);

        // Create a new product ...        
    }
}
```

There are a few disadvantages of doing validation this way :

1. Code bloating and decreased readability
    
2. Code duplication across controller actions
    
3. Testing challenges and reduced testability
    

So let's see how you can rewrite this using a `Request` class and make it cleaner, reusable, and more testable

Creating a request class using the Artisan command:

```bash
php artisan make:request CreateProductRequest
```

This will generate a new `CreateProductRequest` class in the `app/Http/Requests` directory. Open the generated file and update its `rules` method with the validation rules:

```php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class CreateProductRequest extends FormRequest
{
    public function rules()
    {
        return [
            'name' => 'required|string|max:255',
            'description' => 'required|string',
            'price' => 'required|numeric|min:0',
            'quantity' => 'required|integer|min:0',
            'category_id' => 'required|exists:categories,id',
            'brand_id' => 'required|exists:brands,id',
            'color' => 'required|string|max:50',
            'size' => 'required|string|max:20',
            'weight' => 'required|numeric|min:0',
            'images' => 'required|array',
            'images.*' => 'required|image|max:2048',
        ];
    }
}
```

Now, in your `ProductController` you can use the new request class like so:

```php
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Http\Requests\CreateProductRequest;
use App\Models\Product;

class ProductController extends Controller
{
    public function store(CreateProductRequest $request)
    {
        // Retrieve the validated data
        $validatedData = $request->validated();

        // Create a new product ...
    }
}
```

It's a good idea to reuse the request class for different controller actions if possible. But if you want you can also create a different request class for `update` action. You can create as many custom request classes as needed.

**Key Takeaways:**

* Separate validation logic from your controller using custom form requests.
    
* Reuse the request class in other controllers/methods
    


## 3 - Eloquent API Resources

When you are writing controllers for your API it is very common to format the `JSON` response.

If the response format that you need is different than your schema, the formatting logic can also make your controllers bloated and hard to read. And sometimes you may want to reuse the same response format in multiple API endpoints.

This is where you can leverage Eloquent API Resources to keep your responses consistent, make your formats reusable and your controllers clean

Let's look at an example of an API response that is formatted within the controller method:

```php
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class ProductController extends Controller
{
    public function index(Request $request)
    {
        $products = Product::with('brand', 'categories', 'features')->get();

        // Transform the product list with required attributes
        $transformedProducts = $products->map(function ($product) {
            return [
                'id' => $this->id,
                'name' => $this->name,
                'price' => $this->price,
                'description' => $this->description,
                'brand_name' => $product->brand->name,
                'brand_image' => $product->brand->image,
                'categories' => $product->categories->map(function ($category) {
                    return [
                        'name' => $category->name,
                        'alias' => $category->alias,
                        'image' => $category->image,
                    ];
                }),
                'features' => $product->features->map(function ($feature) {
                    return [
                        'title' => $feature->title,
                        'description' => $feature->description
                    ];
                })
            ];
        });

        return response()->json(['products' => $transformedProducts], Response::HTTP_OK);
    }
}
```

Now let's look at how can we extract this logic and put it in a `Resource` class

First, create `ProductResource` class

```bash
php artisan make:resource Product
```

Let's move the logic to this class

```php
use Illuminate\Http\Resources\Json\JsonResource;

class ProductResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'price' => $this->price,
            'description' => $this->description,
            'brand_name' => $this->brand->name,
            'brand_image' => $this->brand->image,
            'categories' => $this->categories->map(function ($category) {
                return [
                    'name' => $category->name,
                    'alias' => $category->alias,
                    'image' => $category->image,
                ];
            }),
            'features' => $this->features->map(function ($feature) {
                return [
                    'title' => $feature->title,
                    'description' => $feature->description
                ];
            })
        ];
    }
}
```

Now we can use this in our controller

```php
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Http\Resources\ProductResource;

class ProductController extends Controller
{
    public function index(Request $request)
    {
        $products = Product::with('brand', 'categories', 'features')->get();

        $productResource = ProductResource::collection($products);

        return response()->json(['products' => $productResource], Response::HTTP_OK);
    }
}
```

As you can see with this approach our controller is much cleaner compared to the original `index` method. It is also reusable. Let's say you have a customer orders endpoint where want to provide the products along with an order object (a very common API requirement) you can reuse the same resource/collection

**Key Takeaways:**

* Use the Eloquent API Resources to keep controllers more readable
    
* Improve the reusability and maintainability of your code by separating data transformation logic from your controllers
    

---

Writing clean controllers in Laravel is essential for building maintainable and efficient applications. Get into the habit of writing cleaner code and your life as a developer will be much easier. As with all good habits, it will take deliberate thinking and practice.

By practicing these principles and incorporating them into your development workflow, you'll be well on your way to writing cleaner and more organized controllers in Laravel.

I have also published a blog on [getting started with background jobs in laravel](https://anantparmar.com/laravel-background-jobs-for-beginners)

I hope you find this valuable, if you did - awesome **👌** share it with your folks if it's relevant to them. If you have any suggestions/comments please feel free.

Happy coding!