Write Your Laravel Validation Logic Like a Senior Dev, Part 1
Separating validation from your controllers with Form Requests
Laravel senior devs are constantly looking for ways to ensure their functions, especially their controller methods, are DRY and only do one thing. Laravel provides many easy and intuitive ways to refactor your controllers to make them highly readable and maintainable. Laravel has one nifty feature called Form Requests, which are custom request classes that contain validation and authorization logic. This is the first of a three- (or more) part series to show how we can take advantage of Laravel features to make Validation more powerful.
Preliminaries:
- Set up your API and make a controller or two for registration and logging in. Because the actual controller methods won't be triggered, you won't need to install Sanctum or worry too much about the code behind authentication.
- If you're using this tutorial for your project, then carry along with installing what you need.
- Also, if you're using Blade or Jetstream, you may want to skip any API-specific instructions ahead.
- If you haven't already, download and install Postman.
- Don't forget to run your server!
php artisan serve
First Steps: Because we're working with an API, we'll need to create custom validation Exceptions. As it stands now, Laravel will return HTML every time we get a validation error.
- Run
php artisan make:request ApiFormRequest
. This will create a file that can be found at app/Http/Requests/ApiFormRequest.php. - In this file, we'll be overriding the
failedValidation()
method. However, because we want to reuse it for our other Form Requests, we'll makeApiFormRequest
an abstract class. We'll remove the bodies of ourauthorize()
andrules()
methods and make them abstract so that we don't end up clashing or overriding them.
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
abstract class ApiFormRequest extends FormRequest
{
abstract public function authorize();
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(response()->json([
'errors' => $validator->errors()
], 422));
}
abstract public function rules();
}
The Work Begins:
- Let's start with validation for our Registration controller/method. We'll create a Form Request for it by running
php artisan make:request RegisterRequest
. - And if you know your PHP OOP principles and Inheritance, you probably predicted that we'll be extending
ApiFormRequest
instead ofFormRequest
to use its methods.
<?php
namespace App\Http\Requests;
use App\Http\Requests\ApiFormRequest;
class RegisterRequest extends ApiFormRequest
{
//
}
- Within each Form Request, Laravel automatically sets
authorize()
to return false. Do not forget to set this to true, or you will get "Unauthorized" errors that may trip you up. NB: You can also define custom logic for yourauthorize()
method but that's for another day.
The Work Continues:
- Now we need to set the rules. In the array, we'll place the name of each field to be validated as the key, and for the value, we'll use a variety of rules in a string. The three fields we'll be validating are
name
,email
, andpassword
. NB: Each value can be an array instead of a string. This comes in handy when we're making custom rules.
<?php
namespace App\Http\Requests;
use App\Http\Requests\ApiFormRequest;
use App\Rules\StrongPassword;
class RegisterRequest extends ApiFormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8',
];
}
- In your controller, import RegisterRequest and use it to type-hint the
$request
parameter passed into your controller's method.
<?php
namespace App\Http\Controllers;
use App\Http\Requests\RegisterRequest;
use Illuminate\Support\Facades\Hash;
use App\Models\User;
class RegisterController extends Controller
{
public function __invoke(RegisterRequest $request){
$validated = $request->validated();
...
}
- In the code above, we retrieved the validated data and passed it to the
$validated
variable. Our validated data is in an array, not an object, so to access the validated email data, for instance, we usevalidated['email']
. NB: Laravel recommends doing it this way but you'll still be able to use the$request
object if your validations pass. - Now follow the previous steps to make another Form Request for your Login controller/method.
The Work Ends:
- Open up Postman and play around in the body of your request. You can omit values or make the
password
too short. Whatever you do, try to get a negative response from the API. Here's what I did and the response I got:
- You might be asking why your error messages may look different than mine. That's because I made custom error messages for my rules! You can do this too in your Form Request (only if you want to because you really don't).
- In RegisterRequest, under your
rules()
method, add another method calledmessages()
. - The format follows the
rules()
method where you'll return an associative array. The difference is that the keys are named a bit differently, and your values will be the messages for each rule.
- In RegisterRequest, under your
public function messages(){
return [
'name.max:255' => 'Your name is too long :/',
'email.email' => 'Please ensure that your email address is in the correct format',
'email.max:255' => 'Your email address is too long :/',
'email.unique:users' => 'This user already exists',
'password.required' => 'Please enter a valid password'
];
}
Conclusion:
- Laravel suggests that separating your validation logic should be done when it is complex. I believe that for clean and maintainable code, it's best to do it as often as possible.
- There's so much more power we can unlock with Laravel Validation and Form Requests. In the next article, we'll make a custom Rule object to help us with modern password validation.