Make your Laravel controller THINNER using FormRequest

Refactoring is always a tough job to do. It gets even worst if they are not in the proper format and things are not in the right place. When working with my teams I often found codes that are messy and not properly organized. The main reason behind it not to think before you write them up.

When building software most of us take the help of a framework. Nothing wrong with that. A framework helps us to build faster and guide us to put the code where it belongs. But again, it will not restrict you from writing bad code. I am personally a big fan of Laravel. It is more structured and great for faster developments. Today we will focus on organizing a Laravel controller or writing a thinner controller as the title say.

What is a Controller?

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\User;

class UserController extends Controller
{
/**
* Show the profile for the given user.
*
* @param int $id
* @return \Illuminate\View\View
*/
public function show($id)
{
return view('user.profile', [
'user' => User::findOrFail($id)
]);
}
}

This is how a typical controller looks like in Laravel. You can generate a fresh new controller by hitting the command below:

php artisan make:controller MyAwesomeController

It’s a good practice to keep the Controller word at the end of your controller name. But it is optional. You can name it as you like. So as you can see, the controller holds a method named show() which returning a view with the profile for the id passed in the parameter. Pretty simple right? But in our development, the taste is not always as sweet as you expected. You have to deal with a lot of logic and corner cases that the controller gets too messy. And whenever a client comes up with a change you keep adding logics and checks to the controller. End of the day your controller gets big and hard to refactor. After a few months when you come back to look at your own code you end up saying something like “Really? I wrote this code!!”.

Don’t worry, you are not alone. But you can be different. Before we dive deep let’s see an example of a messy controller with having only two(so that we can discuss much keeping the area small) methods on it.

<?php

namespace App\Http\Controllers;

use App\Coupon;
use App\Mail\OrderPlaced;
use App\Order;
use App\Product;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Session;

class CheckoutControllerController extends Controller
{
public function showCheckoutForm()
{
// getting already added to cart products
$data['selectedProducts'] = Session::get('productCart');

// returning view
return view('form.checkout', $data);
}

public function handleCheckOut(Request $request)
{
// setting validation rules for user's input
$formData = $request->validate([
'name' => 'required|max:100',
'email' => 'required|email|unique:users,email',
'password' => 'required|confirmed',
'contact_number' => 'required',
'country' => 'required',
'shipping_address' => 'required',
'billing_address' => 'required',
'payment_method' => 'required|in:credit,cod',
'coupon' => 'nullable|exists:coupons,code',
'products.*.id' => 'required|integer|exists:products,id',
'products.*.price' => 'required|numeric',
'products.*.quantity' => 'required|numeric',
], [
'products.*.id.required' => 'The product field is required',
'products.*.id.integer' => 'The product field must be integer',
'products.*.id.exists' => 'The product do not exist',
]);

// registering user
$user = User::create([
'name' => $formData['name'],
'email' => $formData['email'],
'password' => bcrypt($formData['password']),
'country' => $formData['country'],
'contact_number' => $formData['contact_number'],
]);

// calculating total amount
$total = 0;
foreach ($request->get('products') as $product) {
$total += Product::find($product)->price;
}

// checking if coupon exists
if ($request->has('coupon')) {
$coupon = Coupon::find($request->get('coupon'));

$total = $total - (($coupon * $total) / 100);
}

// placing order
$order = Order::create([
'date' => now()->toDateString(),
'order_no' => time(),
'user_id' => $user->id,
'amount' => $total,
'shipping_address' => $request->get('shipping_address'),
'billing_address' => $request->get('billing_address'),
]);

$order->products()->attach($request->get('products'));

// sending mail to user
Mail::to($user->email)->send(new OrderPlaced($order));

// generating flash message
session()->flash('success', 'Order Placed Successfully');

// redirecting to home page
return redirect()->route('home');
}
}

As you can see the controller has only two methods. The first one(showCheckoutForm()) is responsible for showing products that are added to the cart by a user and then simply return the view with the data. No problem with that. It’s a perfect case to write in that way. But look at the second one (handCheckOut()), do all the code belongs to that method?

First of all, the second method is breaking the first rule of the SOLID principle. Single Responsibility. Meaning that a class should be responsible for only one task. If we quote like Uncle Bob the originator of the term “A class should have only one reason to change”. But look at our code. It has a lot of things to do in it. What are they?

  1. Apply/Check validation rules for the inputs
  2. Customize the error messages
  3. Register/Save the user
  4. Calculate the total amount
  5. Check if coupon applied and apply on the total amount
  6. Place the order
  7. Send an email to the user with the invoice
  8. Generate flash message and redirect the user to the home page

Oooo!! Lots of things to do. But we can’t compromise them right? As they are important and need to store for further process. Yes, you’re absolutely right. We don’t need to pick some from them and trough them out thinking it is making the code longer. But what we can do, we can extract some part of it and put it somewhere else. But where to put?

FormRequest is your friend

php artisan make:request CheckoutRequest

With the help of the above command, you will see a class that has been created under your App\Http\Requests namespace. Go to the app directory, followed by Http folder. There you will see a new folder has been created named Requests. Under that folder, your FormRequest classes will be generated. So you will find the CheckoutRequest class there. It inherits the FormRequest class so that you can use all the awesome methods of it. By default, the following code is generated for a newly created FormRequest class.

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class CheckoutRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return false;
}

/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
//
];
}
}

Here, the first method checks whether you have access to this class or not. If not, the rest of the code will be not executed and you will be shown an unauthorized error. This is a good place for checking your user’s accessing criteria and applying rules to check if he is eligible or not. This method returns a boolean value. If you don’t want to check anything you may simply return true. Or you can add your custom logic there(like checking if the user is logged in or not). But don’t return false as it will always trough an unauthorized error.

/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return false; // trough unauthorized error
return true; // no check, allowing to go further return auth()->check(); // returning true if user is logged in /* your custom logic to check user's role. If user has
the 'customer' role it will return true or false. */
return auth()->user()->hasRole('customer');
}

Another default method that is generated after firing the command is the rules() method. This method returns an array. An array that holds the validation rules for your inputs. Here you can apply rules for your form data. So what can we do now, we can move all our validation rules out from the controller class and put it here in the rules method array.

/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required|max:100',
'email' => 'required|email|unique:users,email',
'password' => 'required|confirmed',
'contact_number' => 'required',
'country' => 'required',
'shipping_address' => 'required',
'billing_address' => 'required',
'payment_method' => 'required|in:credit,cod',
'coupon' => 'nullable|exists:coupons,code',
'products.*.id' => 'required|integer|exists:products,id',
'products.*.price' => 'required|numeric',
'products.*.quantity' => 'required|numeric'
];
}

By putting it there in the rules method, your controller is a bit thinner than the previous. But wait? Where are the awesome methods I mentioned earlier? The class has only two methods. Don’t worry we will get there soon. Your generated CheckoutRequest class extends the core FormRequest class. The FormRequest class holds the methods. So whenever, you need any method from that just override it in your CheckoutRequest class. Let’s start by overriding the messages() method of the base class.

/**
* @return array|string[]
*/
public function messages()
{
return [];
}

Like rules() method messages() method also returns an array. But this array acts differently. What it does, it allows you to override the default error messages thrown during validation. If you want to write your own custom error message just write the attribute name you used in the form and followed by your custom message. Note that you don’t need to write all error messages, only them whom you want to write in your way. The rest of the error messages will be generated by default. So let’s move our second array from the $request->validate() method and put it in our CheckoutFormRequest class by overriding the messages() of the FormRequest class.

/**
* @return array|string[]
*/
public function messages()
{
return [
'products.*.id.required' => 'The product field is required',
'products.*.id.integer' => 'The product field must be integer',
'products.*.id.exists' => 'The product do not exist',

];
}

Ok, done with moving. But wait a minute. How can our controller know how/when to apply these rules and messages? We didn’t tell it to do so. Very good point. Moving the code has nothing to do until or unless you call them up. So we need to call/inject the CheckoutRequest class in our controller. Simply replace the default Illuminate\Http\Request by App\Http\Requests\CheckoutRequest class. And Laravel will automatically map the class and get the job done for you. Let’s look at our previously written code with the new changes applied.

/* The defualt Illuminate\Http\Request is replaced by the App\Http\Requests\CheckoutRequest class */public function handleCheckOut(CheckoutRequest $request)
{

// all the validation and custom error messages are applied
$formData = $request->all();

// registering user
$user = User::create([
'name' => $formData['name'],
'email' => $formData['email'],
'password' => bcrypt($formData['password']),
'country' => $formData['country'],
'contact_number' => $formData['contact_number'],
]);

// calculating total amount
$total = 0;
foreach ($request->get('products') as $product) {
$total += Product::find($product)->price;
}

// checking if coupon exists
if ($request->has('coupon')) {
$coupon = Coupon::find($request->get('coupon'));

$total = $total - (($coupon * $total) / 100);
}

// placing order
$order = Order::create([
'date' => now()->toDateString(),
'order_no' => time(),
'user_id' => $user->id,
'amount' => $total,
'shipping_address' => $request->get('shipping_address'),
'billing_address' => $request->get('billing_address'),
]);

$order->products()->attach($request->get('products'));

// sending mail to user
Mail::to($user->email)->send(new OrderPlaced($order));

// generating flash message
session()->flash('success', 'Order Placed Successfully');

// redirecting to home page
return redirect()->route('home');
}

As you can see, our method is much cleaner and our controller is thinner than before now. By reaching this line below, your validation rules are applied and custom error messages are thrown with default errors if any validation fails.

$formData = $request->all();

So we are done with the first two tasks that we did in our controller. They still exist but in a different place.

  1. A̶p̶p̶l̶y̶/̶C̶h̶e̶c̶k̶ ̶v̶a̶l̶i̶d̶a̶t̶i̶o̶n̶ ̶r̶u̶l̶e̶s̶ ̶f̶o̶r̶ ̶t̶h̶e̶ ̶i̶n̶p̶u̶t̶s̶
  2. C̶u̶s̶t̶o̶m̶i̶z̶e̶ ̶t̶h̶e̶ ̶e̶r̶r̶o̶r̶ ̶m̶e̶s̶s̶a̶g̶e̶s̶
  3. Register/Save the user
  4. Calculate the total amount
  5. Check if coupon applied and apply on the total amount
  6. Place the order
  7. Send an email to the user with the invoice
  8. Generate flash message and redirect the user to the home page

Let’s go for the third task, registering the user. But is the CheckoutController responsible for registering users? Of course not, CheckoutController is responsible for handling checkout kinds of stuff. So we need to move that part of code somewhere else. But where? You can come out with a lot of solutions at this point. But wait, we have recently created a FormRequest class. Can we use that? Of course, you can and this is not a bad idea at all.

Let’s create a method to register/save the user in the CheckoutRequest class. You can use any of the access modifiers(public/protected/private) for the method but I would use protected or private as I don’t want to call this method outside the class or only can be accessed by classes that inherit it. But let’s use the public for now. And previously we accessed the methods of the Illuminate\Http\Request class by an object of it. As we are now in the dedicated FormRequest class we can use them directly by using $this keyword.

// In the controller using Illuminate\Http\Request $request object
$request->get('name')
// in the CheckoutRequest class
$this->get('name'); // no need to create object.

Ok, let’s create the registerUser() method.

public function registerUser()
{
return User::create([
'name' => $this->get('name'),
'email' => $this->get('email'),
'password' => bcrypt($this->get('password')),
'country' => $this->get('country'),
'contact_number' => $this->get('contact_number')
]);
}

Ok, but will this part of code will be called automatically like the previous? A big NO my friend. Validation is handled by default and the methods you override from FormRequest class to CheckoutRequest class will be called internally by Laravel, but your custom methods will not be called. You need to call it yourself. So what can we do, we can call this registerUser() method from our controller.

// registering user before using CheckoutRequest class$user = User::create([
'name' => $this->get('name'),
'email' => $this->get('email'),
'password' => bcrypt($this->get('password')),
'country' => $this->get('country'),
'contact_number' => $this->get('contact_number')
]);
// registering user after using CheckoutRequest class$user = $request->registerUser();

By extracting that code, our controller is again a bit thinner and we are done with the third task(3.R̶e̶g̶i̶s̶t̶e̶r̶/̶S̶a̶v̶e̶ ̶t̶h̶e̶ ̶u̶s̶e̶r̶) of our list. Let’s see how our controller method looks like now.

public function handleCheckOut(CheckoutRequest $request)
{
// registering user
$user = $request->registerUser();

// calculating total amount
$total = 0;
foreach ($request->get('products') as $product) {
$total += Product::find($product)->price;
}

// checking if coupon exists
if ($request->has('coupon')) {
$coupon = Coupon::find($request->get('coupon'));

$total = $total - (($coupon * $total) / 100);
}

// placing order
$order = Order::create([
'date' => now()->toDateString(),
'order_no' => time(),
'user_id' => $user->id,
'amount' => $total,
'shipping_address' => $request->get('shipping_address'),
'billing_address' => $request->get('billing_address'),
]);

$order->products()->attach($request->get('products'));

// sending mail to user
Mail::to($user->email)->send(new OrderPlaced($order));

// generating flash message
session()->flash('success', 'Order Placed Successfully');

// redirecting to home page
return redirect()->route('home');
}

Now, focus on the next three tasks we have to work with.

4. Calculate the total amount

5. Check if coupon applied and apply on the total amount

6. Place the order

Like before we did for registering users, we can do this kind of stuff in our FormRequest class. You can move the code somewhere else, but again this is not a bad choice at all to put is here in the CheckoutRequest class as it is a part of the checkout process. So make methods to handle them in the CheckoutRequest class.

private function getActualTotalAmount()
{
$total = 0;

foreach ($this->get('products') as $product) {
$total += Product::find($product)->price;
}

return $total;
}

private function getTotalAfterCouponDiscount()
{
$total = $this->getActualTotalAmount();

if ($this->has('coupon')) {
$coupon = Coupon::find($this->get('coupon'));

$total = $total - (($coupon * $total) / 100);
}

return $total;
}

public function placeOrder()
{
$order = Order::create([
'date' => now()->toDateString(),
'order_no' => time(),
'user_id' => $this->registerUser()->id,
'amount' => $this->getTotalAfterCouponDiscount(),
'shipping_address' => $this->get('shipping_address'),
'billing_address' => $this->get('billing_address'),
]);

$order->products()->attach($this->get('products'));

return $order;
}

Again, these all are custom methods. You need to call them manually in your controller. But as you called the registerUser(), getTotalAfterCouponDiscount(), getActualTotalAmount() internally in the placeOrder() method they are already called. No need to call them again. Just call the placeOrder() method in the controller and you can now remove the call of registerUser() method in your controller as you called it internally.

public function handleCheckOut(CheckoutRequest $request)
{
// placing order
$order = $request->placeOrder();

// sending mail to user
Mail::to($request->get('email'))->send(new OrderPlaced($order));

// generating flash message
session()->flash('success', 'Order Placed Successfully');

// redirecting to home page
return redirect()->route('home');
}

As you can see we have reduced 50(+-) line from our controller method and the controller is now more readable, cleaner, and of course more THINNER. And here how your controller looks now.

<?php

namespace App\Http\Controllers;

use App\Http\Requests\CheckoutRequest;
use App\Mail\OrderPlaced;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Session;

class CheckoutControllerController extends Controller
{
public function showCheckoutForm()
{
// getting already added to cart products
$data['selectedProducts'] = Session::get('cart');

// returning view
return view('form.checkout', $data);
}

public function handleCheckOut(CheckoutRequest $request)
{
// placing order
$order = $request->placeOrder();

// sending mail to user
Mail::to($request->get('email'))->send(new OrderPlaced($order));

// generating flash message
session()->flash('success', 'Order Placed Successfully');

// redirecting to home page
return redirect()->route('home');
}
}

And let’ check our task list again.

  1. A̶p̶p̶l̶y̶/̶C̶h̶e̶c̶k̶ ̶v̶a̶l̶i̶d̶a̶t̶i̶o̶n̶ ̶r̶u̶l̶e̶s̶ ̶f̶o̶r̶ ̶t̶h̶e̶ ̶i̶n̶p̶u̶t̶s̶
  2. C̶u̶s̶t̶o̶m̶i̶z̶e̶ ̶t̶h̶e̶ ̶e̶r̶r̶o̶r̶ ̶m̶e̶s̶s̶a̶g̶e̶s̶
  3. R̶e̶g̶i̶s̶t̶e̶r̶/̶S̶a̶v̶e̶ ̶t̶h̶e̶ ̶u̶s̶e̶r̶
  4. C̶a̶l̶c̶u̶l̶a̶t̶e̶ ̶t̶h̶e̶ ̶t̶o̶t̶a̶l̶ ̶a̶m̶o̶u̶n̶t̶
  5. C̶h̶e̶c̶k̶ ̶i̶f̶ ̶c̶o̶u̶p̶o̶n̶ ̶a̶p̶p̶l̶i̶e̶d̶ ̶a̶n̶d̶ ̶a̶p̶p̶l̶y̶ ̶o̶n̶ ̶t̶h̶e̶ ̶t̶o̶t̶a̶l̶ ̶a̶m̶o̶u̶n̶t̶
  6. P̶l̶a̶c̶e̶ ̶t̶h̶e̶ ̶o̶r̶d̶e̶r̶
  7. Send an email to the user with the invoice
  8. Generate flash message and redirect the user to the home page

As you can see out of 8, six tasks are done. Now with the last two, I think there is no harm to keep them where they are and they have some valid point to stay there. Though if you want you can generate the flash message during order placement in the placeOrder() method in the CheckoutRequest class. But I am good with that, no big deal. So now, what your controller’s handleCheckOut() method saying is, place the order, send an invoice to the user and redirect back to the home page with a success message. Pretty straight forward. And all the other parts are moved away from the controller and have given a dedicated class to manage stuff there. So here what your CheckoutRequest class looks like.

<?php

namespace App\Http\Requests;

use App\Coupon;
use App\Order;
use App\Product;
use App\User;
use Illuminate\Foundation\Http\FormRequest;

class CheckoutRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}

/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required|max:100',
'email' => 'required|email|unique:users,email',
'password' => 'required|confirmed',
'contact_number' => 'required',
'country' => 'required',
'shipping_address' => 'required',
'billing_address' => 'required',
'payment_method' => 'required|in:credit,cod',
'coupon' => 'nullable|exists:coupons,code',
'products.*.id' => 'required|integer|exists:products,id',
'products.*.price' => 'required|numeric',
'products.*.quantity' => 'required|numeric'
];
}

/**
* @return array|string[]
*/
public function messages()
{
return [
'products.*.id.required' => 'The product field is required',
'products.*.id.integer' => 'The product field must be integer',
'products.*.id.exists' => 'The product do not exist',
];
}

public function registerUser()
{
return User::create([
'name' => $this->get('name'),
'email' => $this->get('email'),
'password' => bcrypt($this->get('password')),
'country' => $this->get('country'),
'contact_number' => $this->get('contact_number')
]);
}

private function getActualTotalAmount()
{
$total = 0;

foreach ($this->get('products') as $product) {
$total += Product::find($product)->price;
}

return $total;
}

private function getTotalAfterCouponDiscount()
{
$total = $this->getActualTotalAmount();

if ($this->has('coupon')) {
$coupon = Coupon::find($this->get('coupon'));

$total = $total - (($coupon * $total) / 100);
}

return $total;
}

public function placeOrder()
{
$order = Order::create([
'date' => now()->toDateString(),
'order_no' => time(),
'user_id' => $this->registerUser()->id,
'amount' => $this->getTotalAfterCouponDiscount(),
'shipping_address' => $this->get('shipping_address'),
'billing_address' => $this->get('billing_address'),
]);

$order->products()->attach($this->get('products'));

return $order;
}
}

So now you learned to make your controller THINNER with the help of FormRequest class. Keep going and hit claps for me if you like it.

A software engineer from Bangladesh.