DEV Community

Cover image for A comprehensive guide on how to design future-proof controllers: Part 2
Ibrahim Alausa
Ibrahim Alausa

Posted on

A comprehensive guide on how to design future-proof controllers: Part 2

Introduction

This article is the second part of a series of articles where we talk about how separating code based on functionality can improve the quality of your codebase. If you have not read the first part, check it out here.

This article will focus on separating the request validation concern from the rest of the code in the Controller. I will be using Laravel in this tutorial. Still, you can apply the ideas we will be discussing here to any programming language that supports any form of code separation into packages, modules, etc., in such a way that you can use code written in file A in file B.

Prerequisites

  • Reading the first part of this article here to understand some concepts and terminologies
  • Knowledge of Object-Oriented Programming (OOP) is required
  • Basic knowledge of Laravel
  • If you want to test the code on your system, then you **MUST **have PHP installed on your system

Let's dive in πŸš€

You can find all the code from this article here.

What we currently have

Let's look at a simple controller function that adds a new user to the database during a registration process. I have added comments to illustrate how we can identify the three concerns involved in the register function.

  /**
   * create a new account
   * @param \Illuminate\Http\Request $request
   * @return \Illuminate\Http\Response
   */
  public function register(Request $request)
  {
    /*----Concern (Request validation)----*/
    $validator = Validator::make($request->all(), [
      'email' => 'required|email|unique:users',
      'name' => 'required|string',
      'password' => 'required|min:6'
    ]);

    if ($validator->fails()) {
      /*----Concern (Sending response to the client)----*/
      return response()->json(
        [
          "status" => false,
          "message" => $validator->errors()
        ],
        422
      );
    }

    /*----Concern (Handling business logic (In this case, hashing the user's password and interaction with database)----*/
    $password = Hash::make($request->password);
    $user = User::create([
      'name' => $request->name,
      'password' => $password,
      'email' => $request->email
    ]);

    /*----Concern (Sending response to the client)----*/
    return response()->json(
      [
        "status" => true,
        "message" => 'User account created successfully',
        "data" => $user
      ],
      201
    );
  }
Enter fullscreen mode Exit fullscreen mode

Separating the Request Validation Concern

separation.gif

We will discuss two approaches to separate the Request validation concern from the Controller.

  • The first approach involves building a request validation class and writing all your validation logic in the validation class. The Class needs to contain some public methods to ensure that the Controller can execute the validation logic.
  • The second approach is called Form request validation. This approach is specific to Laravel, and the validation class has to follow a particular pattern. It's a powerful feature.

Approach 1: Building a Request Validation Class

building.gif

  • Create a folder called Validators inside the app/Http directory
  • Create a new file called AuthValidator.php in the newly created Validators folder
  • The code below should be the content of the AuthValidator.php file
<?php

namespace App\Http\Validators;

use Illuminate\Support\Facades\Validator;


class AuthValidator
{
  protected $errors = [];

  /**
   * validate user registration data
   *
   * @param  array $requestData
   * @return \App\Http\Validators\AuthValidator
   */
  public function validateRegistrationData(array $requestData)
  {
    $validator = Validator::make($requestData, [
      'email' => 'required|email|unique:users',
      'name' => 'required|string',
      'password' => 'required|min:6'
    ]);

    $this->errors = $validator->errors();

    return $this;
  }


  /**
   * get the errors that occurred during 
   * validation
   *
   * @return array
   */
  public function getErrors()
  {
    return $this->errors;
  }
}
Enter fullscreen mode Exit fullscreen mode

What exactly does the code do? πŸ˜•

  • First, we have a property in the Class called $errors and set the value as an empty array
  • Then, we create a method called validateRegisterationData(), which will be responsible for executing the actual validation logic
  • The result of the validation check ($validator->errors()) is stored in the $errors property of the current object. If all the data in the request are valid and no validation error(s) occurred, the validator returns an empty array.
  • Finally, we return the current object ($this)
  • The second method, getErrors() simply returns the $errors property for the current object. This property would contain the actual errors that occurred during validation or an empty array if no errors occurred

How do we use the AuthValidator in the Controller? πŸ€”

Create a constructor method (__construct()) in the AuthController class and pass the Authvalidator class as a parameter in the constructor. Then the value of the parameter is stored in a class property.

class AuthController extends Controller
{

  protected $authValidator;

  /**
  * __construct
  * 
  *  @param \App\Http\Validators\AuthValidator $authValidator
   * @return void
   */
  public function __construct(AuthValidator $authValidator)
  {
   $this->authValidator = $authValidator;

  }
Enter fullscreen mode Exit fullscreen mode

Now we can remove the existing validation logic in the register() method of the Controller and use the logic in the AuthValidator class we created ☺️.

  /**
   * create a new account
   * @param \Illuminate\Http\Request $request
   * @return \Illuminate\Http\Response
   */
  public function register(Request $request)
  {
    $errors = $this->authValidator
      ->validateRegistrationData($request->all())
      ->getErrors();

    if (count($errors)) {
      return response()->json([ "status" => false,"message" => $errors],
        422
      );
    }

Enter fullscreen mode Exit fullscreen mode

Since the validateRegistrationData() method returns an object, we can immediately execute the getErrors() method on the returned object without assigning it to a variable first. This pattern is called Fluent interface. count($errors) checks if there is at least one element in the errors array returned from the validator and returns a response to the client if any error exists.

With the code designed this way, the Controller does not care how the validation logic works. The Controller knows that the validator will do its job and return a response after completing the validation process. Then the final response is sent from the Controller. So we can add, remove or modify the validation logic, and the Controller still works perfectly without any issues. Sweet right. If you think this is great, let's look at the second method, which is using Laravel Form Requests.

Approach 2: Using Laravel Form Requests

laravelvalidation.gif

Form requests are custom request classes containing their validation logic, just like we did in the first approach. Laravel Form requests are also awesome because they contain authorization logic by default. To create a form request class, you may use the make:request artisan command provided by Laravel.

php artisan make:request CreateNewUserRequest
Enter fullscreen mode Exit fullscreen mode

Laravel will generate a form request class in the app/Http/Requests directory with the name passed as the file name. In our case, a new file, CreateNewUserRequest.php will be created.

By default, Form request classes generated by Laravel come with two methods, authorize() and rules(). The authorize() method contains the logic that determines if the user making that request has the permission to do so. Of course, you can modify the logic to fit your specific use case at any time. In our case, we do not require any special permission or authorization for the CreateNewUserRequest Class, so we return true.


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

Enter fullscreen mode Exit fullscreen mode

On the other hand, the rules() method returns the validation rules that should apply to the request's data.


/**
   * Get the validation rules that apply to the request.
   *
   * @return array
   */
  public function rules()
  {
    return [
      'email' => 'required|email|unique:users',
      'name' => 'required|string',
      'password' => 'required|min:6'
    ];
  }

Enter fullscreen mode Exit fullscreen mode

How do we use the rules in the CreateNewUserRequest class in the Controller? πŸ€”

  • Change the type of the parameter in the register function from \Illuminate\Http\Request to \App\Http\Requests\CreateNewUserRequestwith(this is the path to the Laravel form request class we just built)
  • Remove the validation logic in the register() function
  • Since Laravel internally checks for errors and returns an appropriate response when using Form requests, we do not need an if statement to check if the validation failed
/**
   * create a new account
   * @param App\Http\Requests\CreateNewUserRequest $request
   * @return \Illuminate\Http\Response
   */
  public function register(CreateNewUserRequest $request)
  {
    /*--------Concern 2 (Business logic execution)------------------*/
    $password = Hash::make($request->password);
    $user = User::create([
      'name' => $request->name,
      'password' => $password,
      'email' => $request->email
    ]);

    /*--------Concern 3(Response formatting and return)------------------*/
    return response()->json(
      [
        "status" => true,
        "message" => 'User account created',
        "data" => $user
      ],
      201
    );
  }
Enter fullscreen mode Exit fullscreen mode

And voila! Our Controller has absolutely no idea about the validation logic, and changes to the validations rules do not affect the Controller. So once again, we can change the validation logic however we want, and the Controller will remain the same and work perfectly.

Let's explore Form Requests a little further πŸ’ͺ🏿

Here is a sample error response from the CreateNewUserRequest class after sending a request with an empty value as the name of the new user

{
    "message": "The given data was invalid.",
    "errors": {
        "name": [
            "The name field is required."
        ]
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, let's assume that due to our team's convention, we need to change the structure of the error response sent back to the client after a failed validation. We can achieve this by overriding the default failedValidation() method in all Laravel form request classes that extend FormRequest, just like our CreateNewUserRequest Class.

Now we have an extra method, failedValidation(), that looks like this.

/**
   * Handle a failed validation attempt.
   *
   * @param  \Illuminate\Contracts\Validation\Validator  $validator
   * @return void
   *
   * @throws \Illuminate\Validation\ValidationException
   */
  protected function failedValidation(Validator $validator)
  {
    throw new HttpResponseException(
      response()->json(
        [
          'status' => false,
          'message' => 'Validation errors occurred',
          'errors' => $validator->errors(),
        ],
        422
      )
    );
  }
Enter fullscreen mode Exit fullscreen mode

After making our changes on how we want the error message to be structured, our response looks like this.

{
    "status": false,
    "message": "Validation errors occurred",
   "errors": {
        "name": [
            "The name field is required."
        ]
    }
}
Enter fullscreen mode Exit fullscreen mode

What's next? πŸ’­

In the next part of this series, we will be looking at how to use Actions to separate business logic away from the Controller. Believe me, that would be awesome, and you don't want to miss it.

Quick recap πŸ”„

  • The controllers in your codebase should know what validation class to call to a particular request but have zero knowledge about the validation rules that apply to the request's data. That is the responsibility of Validators
  • You can build your validator class or use Laravel form requests. I don't think any approach is better than the other. It all depends on whichever approach you prefer
  • The Laravel form request class can also handle user authorization even before checking if the data from the client passes all the validation rules, and this is done via the authorize() method
  • The advantage of building your validators is that your methods can follow any naming convention you want. However, when using Laravel Form requests, the method handling the data validation must be named rules while the method that handles authorization must be named authorize.

It's a wrap πŸŽ‰

You can check out all the code in this article on Github. After reading this, I sincerely hope that you have learned something, no matter how small. If you did, kindly drop a thumbs up.
Thanks for sticking with me till the end. If you have any suggestions or feedback, kindly drop them in the comment section. Enjoy the rest of your day. Bye 😊.

Top comments (0)