Laravel provides a very powerful and convenient way to validate incoming requests. All you need to do is define a bunch of rules for each field that you are expecting on the end point. For example,
[
'title' => 'required|unique:posts|max:255',
'body' => 'required',
]
We can easily understand all the rules and even many non-programmer's can understand what is going on here.
What's the problem here?
Most of the time, defining rules like this works like a charm. But the issue starts when you have to define too many rules. It actually becomes a mess. Consider this example:
'password' => [
'required',
'confirmed',
'min:8',
'max:20',
'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/',
function ($attribute, $value, $fail) {
// Custom validation rule - additional logic can be added here
if (strpos($value, 'password') !== false) {
$fail('The password cannot contain the word "password".');
}
},
]
Yes, it becomes a mess when there are too many rules and too many fields to work with. Also, what if we need the same field for some other end points. For example, we need the password field for both login and register endpoints.
In bigger projects, this can become too repetitive and eventually it will be very hard to maintain this code. Luckily, Laravel provides us with custom rule objects. Here is an example from the official Laravel 10.x docs:
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class Uppercase implements ValidationRule
{
/**
* Run the validation rule.
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (strtoupper($value) !== $value) {
$fail('The :attribute must be uppercase.');
}
}
}
We can validate the attribute anyway we like, and then simply use it back like this:
'name' => ['required', 'string', new Uppercase],
This looks simple enough but there is still a problem. If we use a custom rule object like above for our field, we have to define logic ourselves as we just saw in the case of Uppercase rule.
Let's take a look at another example, we may need to validate username field on multiple endpoints. In order to avoid code duplication and maintaining a single source of truth, here is a custom rule class that checks for username:
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class Username implements ValidationRule
{
/**
* Run the validation rule.
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! is_string($value))
{
$fail("Username should be a string");
}
if (count($value) > 32)
{
$fail("Username shouldn\'t be greater than 32 characters.");
} else if (count($value) < 1) {
$fail("Username shouldn\'t be less than 1 character.");
}
}
}
In the example above, we had to manually define all the validation logic for rules. But hold on! when there is built-in system of rules and validation in Laravel already, why do we have to reinvent the wheel. We literally have all the tools in our hand.
We can simply use the Laravel's built-in validator for this purpose.
namespace App\Rules;
use Closure;
use Illuminate\Support\Facades\Validator;
use Illuminate\Contracts\Validation\ValidationRule;
class Username implements ValidationRule
{
/**
* Run the validation rule.
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$validator = Validator::make(
[$attribute => $value],
[$attribute => 'string|min:1|max:32'],
[
'string' => "Username should be a string",
'min' => "Username shouldn\'t be greater than 32 characters.",
'max' => "Username shouldn\'t be less than 1 character."
]
);
if($validator->fails()){
$fail($validator->error()->first());
}
}
}
This is much convenient that defining the logic yourself. So, the Username will now act as our own rule group.
One last problem!
But still one last issue, we will have to make validator every time for every rule group class like this. The solution lies in separating what is being repeated. So, let's do it:
First make a new BaseRule abstract class in the App\Rule directory using command:
php artisan make:rule BaseRule
In the BaseRule.php, write an abstract class that implements Illuminate\Contracts\Validation\ValidationRule
like normal rule classes.
For now out BaseRule.php should look like this:
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\ValidationRule;
abstract class BaseRule implements ValidationRule
{
}
Now its time to write our magical function that will make our life easier:
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Validator;
abstract class BaseRule implements ValidationRule
{
/**
* Allows validating the attribute's value agianst a group of rules
*
* @param string $attribute the name of attribute
* @param string $value the value to validate
*
* @return RuleGroupValidation an object with a method named `against` to perform the actual validation
*/
protected function check(string $attribute, string $value): RuleGroupValidation
{
return new class($attribute, $value) implements RuleGroupValidation
{
public function __construct(public string $attribute, public string $value)
{
}
public function against(array|string $rules = [], array $messages = []): string | null
{
$validator = Validator::make(
[$this->attribute => $this->value],
[
$this->attribute => $rules
],
$messages
);
if ($validator->fails()) {
return $validator->errors()->first();
} else {
return null;
}
}
};
}
}
interface RuleGroupValidation
{
/**
* Performs the actual validation on the given attribute's value against given rules and returns first error message that it encounters
*
* @return string|null the first error message or null in case there was no error
*/
public function against(): string | null;
};
Let's break down this function:
First we are using an anonymous class so we can make our code clean and more readable by separating the rules/messages from the input attribute.
We store the attribute and value in the constructor because we are gonna need it in the
against
method.While the method
against
is simply the generalized version of our attempt to useIlluminate\Support\Facades\Validator
.Finally, there is
RuleGroupValidation
interface whose sole purpose is to provide auto-completion. Otherwise the IDE doesn't know what this anonymous class can do.
Now lets utilize this class in our Username Rule.
namespace App\Rules;
use Closure;
use Illuminate\Support\Facades\Validator;
use Illuminate\Contracts\Validation\ValidationRule;
class Username extends BaseRule
{
/**
* Run the validation rule.
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if($error = $this->check($attribute, $value)
->against(
'string|min:1|max:32',
[
'string' => "Username should be a string",
'min' => "Username shouldn\'t be greater than 32 characters.",
'max' => "Username shouldn\'t be less than 1 character."
]
)
)
{
$fail($error);
}
}
}
Now, its looks much better and is readable also. We can now make as many rule groups as we like and we can also manage them easily. For example, if this Username class is being used in many places and we need to change max length, we can simply change it in one place. Isn't it amazing?
One thing to note here is that we replaced
class Username implements ValidationRule
with
class Username extends BaseRule
Which allows us to use the check
method in our rules.
This is actually how the laravel's built in rules like Password, Enum work under the hood but in a more explicit environment.
I know many people will disagree with me, many people can improve this further. I am open to everyone. Any feedback is appreciated, Thank you!