View code


Rebuilding Laravel’s Validation System: Part 2

In the previous part, we created the blueprint for the validation system we want to build. We created the Request class and the Rule interface. We also created some predefined rules like Required, Boolean, EmailAddress, and Numeric. In this part, we will see how to use these rules in the Request class to validate the data.

The Validator class

The Request class is responsible for connecting the data with the rules and performing the validation. To do this, we will create a Validator class that will receive the data, the rules, and the validation rules and perform the validation.

<?php
declare(strict_types=1);
namespace Devlob\Validation;
use Devlob\Validation\Rules\Boolean;
use Devlob\Validation\Rules\EmailAddress;
use Devlob\Validation\Rules\Numeric;
use Devlob\Validation\Rules\Required;
use Exception;
use FilesystemIterator;
class Validator
{
private array $data = [];
private array $rules = [];
private array $validation = [];
public function __construct()
{
$this->setupPredefinedRules();
}
private function setupPredefinedRules(): void
{
$predefinedRules = [Required::class, Numeric::class, EmailAddress::class, Boolean::class];
foreach ($predefinedRules as $rule) {
$class = new $rule();
$this->rules[$class->key()] = $rule;
}
}
public function make(array $data, array $validation): self
{
$this->data = $data;
$this->validation = $validation;
return $this;
}
public function validate(): array
{
$errors = [];
// Loop through the validation rules and validate the data
return $errors;
}
}

When we create a new instance of the Validator class, we call the setupPredefinedRules method to load the predefined rules we created in the previous part. The method simply creates an instance of each rule and stores it in the $rules array as a key-value pair where the key is the rule key and the value is the rule class.

If you var_dump the $rules array once the predefined rules are loaded, you will see something like this:

{"required":"Devlob\\Validation\\Rules\\Required","numeric":"Devlob\\Validation\\Rules\\Numeric","email_address":"Devlob\\Validation\\Rules\\EmailAddress","boolean":"Devlob\\Validation\\Rules\\Boolean"}

The make method is responsible for setting the data and the validation rules. The validate method is responsible for validating the data against the rules and returning an array of errors if any.

Validating the data

public function validate(): array
{
$errors = [];
foreach ($this->validation as $attribute => $validationRules) {
$validationRules = explode('|', $validationRules);
foreach ($validationRules as $validationRule) {
if (isset($this->rules[$validationRule])) {
$class = new $this->rules[$validationRule]();
$validate = $class->passes($attribute, $this->data[$attribute]);
if (!$validate) {
$errors[$attribute] = str_replace(':attribute', $attribute, $class->message());
}
} else {
throw new Exception("Rule '$validationRule' is not defined.");
}
}
}
return $errors;
}

We start by creating an empty array called $errors to store the errors. We loop through the validation rules and explode them by the pipe character |. We then loop through each rule and check if the rule is defined in the $rules array. If it is, we create an instance of the rule class and call the passes method to validate the data. If the data does not pass the validation, we store the error message in the $errors array using the attribute name as the key and replace the :attribute placeholder in the error message with the attribute name.

If the rule is not defined in the $rules array, we throw an exception, which makes sense because we should not have undefined rules in the validation rules.

Using the Validator class

Now that we have the Validator class, we can use it in the Request class to validate the data. Let's update the Request class to use the Validator class.

Update the validate method in the Request class:

public function validate(array $rules): string|bool
{
$errors = $this->validator
->make($this->data, $rules)
->validate();
if ($errors) {
return json_encode([
'errors' => $errors,
'message' => 'Validation failed.'
], 422);
}
return true;
}

Now, when we call the validate method on the Request class, it will use the Validator class to validate the data against the rules and return the errors if any.

Testing our validation system

Let's test our validation system by creating a new Request class and using it to validate some data.

<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Devlob\Request\Request;
$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',
]);

If you run the code above, you should see the following output:

{
"errors": {
"name": "The name field should not be empty.",
"age": "The age field must be numeric.",
"email": "The email field must be a valid email.",
"is_admin": "The is_admin field must be a boolean."
},
"message": "Validation failed."
}

The validation system is working as expected. We can now validate data using the predefined rules we created. Let's update the data and see if the validation passes.

<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Devlob\Request\Request;
$request = Request::create([
'name' => 'Renato Hysa',
'age' => '31',
'email' => 'renato@hysa.dev',
'is_admin' => true,
]);
echo $request->validate([
'name' => 'not_empty',
'age' => 'numeric',
'email' => 'required|email_address',
'is_admin' => 'boolean',
]);

If you run the code above, you should see 1 as the output, which means the validation passed.

Coming up next

In the next part, we will see how to create custom rules and how to use them in the validation system.