View code
Rebuilding Laravel’s Validation System: Part 1
A lot of frameworks have a validation system, and Laravel is no exception. Laravel's validation system is very powerful and flexible, but how does it work? In this series, we will rebuild Laravel’s validation system from scratch. We will start by creating a simple validation system, and then we will allow it to grow and become more complex by adding our own validation rules, custom messages, and more.
The blueprint
I like creating a blueprint for the code I want to have. This helps me to understand what I want to achieve and how I want to achieve it. Here is the code I want to have:
$request = Request::create(['name' => '','age' => 'a','email' => 'renato@','is_admin' => 'not_a_boolean',]);echo $request->validate(['name' => 'not_empty','age' => 'numeric','email' => 'required|email_address','is_admin' => 'boolean',]);
Working on the Request class
Based on the blueprint, we need to create a Request class that will have a static create method. This method will receive an array of data and return a new instance of the Request class. The Request class will also have a validate method that will receive an array of rules and return a string with the validation errors.
<?phpdeclare(strict_types=1);namespace Devlob\Request;class Request{ protected array $data; private function __construct(array $data = []) { $this->data = $data; } public static function create(array $data = []): Request { return new static($data); } public function validate(array $rules): string|bool { }}
As of now, the Request class is very simple. It has a data property that will store the data passed to the create method. Think of it as the data that comes from an HTTP request, but in this case, we are simplifying it by passing an array directly.
Creating the validation rules
A rule is a class that will validate a specific field. For example, the NotEmpty rule will check if the field is not empty. The Numeric rule will check if the field is numeric. The EmailAddress rule will check if the field is a valid email address. The Boolean rule will check if the field is a boolean and so on. We will create a Rule interface that all rules must implement.
Here is the code for the Rule interface:
<?phpdeclare(strict_types=1);namespace Devlob\Validation\Rules;interface Rule{ public function key(): string; public function passes(string $attribute, mixed $value): bool; public function message(): string;}
The Rule interface has three methods: key, passes, and message. The key method will return the key of the rule, that must be unique. The passes method will receive the attribute name and the value to be validated and return a boolean indicating if the validation passed or not. The message method will return the error message if the validation fails.
Regarding the key method, we can easily avoid it by using the class name as the key. However, it would be nice to have the flexibility to change the key if needed. The keys will be used in the validate method of the Request class, so allowing the developer to change the key can be useful in some cases.
Now that we have the Rule interface, we can create our rules. Let's start with the Required rule.
The Required rule
<?phpdeclare(strict_types=1);namespace Devlob\Validation\Rules;class Required implements Rule{ public function key(): string { return 'required'; } public function passes(string $attribute, $value): bool { return isset($value); } public function message(): string { return 'The :attribute field is required.'; }}
The Required rule is very simple. It checks if the value is set. If it is not set, it returns false, indicating that the validation failed. The error message is also very simple. It says that the field is required. Make sure to include the :attribute placeholder in the error message. This placeholder will be replaced by the attribute name when the error message is generated.
The Boolean rule
<?phpdeclare(strict_types=1);namespace Devlob\Validation\Rules;class Boolean implements Rule{ public function key(): string { return 'boolean'; } public function passes(string $attribute, $value): bool { if (isset($value) && ($value == 'true' || $value == 'false' || $value == '1' || $value == '0')) { $isBoolean = true; } else { $isBoolean = false; } return $isBoolean; } public function message(): string { return 'The :attribute field must be a boolean.'; }}
The EmailAddress rule
<?phpdeclare(strict_types=1);namespace Devlob\Validation\Rules;class EmailAddress implements Rule{ public function key(): string { return 'email_address'; } public function passes(string $attribute, $value): bool { return isset($value) && filter_var($value, FILTER_VALIDATE_EMAIL); } public function message(): string { return 'The :attribute field must be a valid email.'; }}
The Numeric rule
<?phpdeclare(strict_types=1);namespace Devlob\Validation\Rules;class Numeric implements Rule{ public function key(): string { return 'numeric'; } public function passes(string $attribute, $value): bool { return isset($value) && is_numeric($value); } public function message(): string { return 'The :attribute field must be numeric.'; }}
Wrapping up
The rules that we just created (Required, Boolean, EmailAddress, and Numeric) will be our predefined rules. Laravel already comes with a lot of predefined rules, thus in this part we wanted to replicate the same behavior. In the next part, we will see how to use these rules in the Request class to validate the data.