How to write clean Controllers in Laravel
Beginner's Guide to clean code - Move your code where it belongs
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.
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 :
Smaller Methods with Single Responsibility
Validation with Form Request Class
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 :
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
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.
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 :
Code bloating and decreased readability
Code duplication across controller actions
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:
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:
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:
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:
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
php artisan make:resource Product
Let's move the logic to this class
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
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
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!