Cómo crear y probar reglas de validación personalizadas en Laravel

Spread the love

Imagina que el requerimiento de un sistema es validar que el host de una cuenta de correo sea valido; es decir, que puedas de alguna forma determinar que ese host corresponde a un dominio registrado en un sistema de DNS.

Bien, esto es muy sencillo usando el paquete EmailValidator que usa Laravel.

Así que rápidamente creas una validación como la siguiente.

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
        Validator::extend('check_email_dns', function ($attribute, $value, $parameters, $validator) {
            return (new EmailValidator())->isValid($value, new DNSCheckValidation());
        });
    }

Y es aquí donde te pregunto…

¿Cómo compruebas que esta validación funciona?

sad
¿Cómo compruebas tus validaciones personalizadas?

Una opción es que agregues la validación a un Form Request o uses el método validate en el request. y luego pruebes todas las opciones que se te ocurran para comprobar que tu validación funciona.

El detalle es que esto es muy aburrido! y pierdes mucho tiempo!.

Pero existe una opción mas divertida, y es usar pruebas. así que vamos a explorar un poco esta perspectiva explicando cómo podemos crear pruebas personalizadas y al mismo tiempo probar que funcionan como esperamos.

Regla de validación personalizada mediante closure.

Esta validación es muy útil cuando queremos crear reglas de validación de un solo uso.

El closure recibe tres parámetros, el campo que se esta validando ($attribute) , el valor de ese campo ($value) y otro closure ($fail) que se encarga de manejar el mensaje que se regresa si la validación falla.

$validator = Validator::make($request->all(), [
    'title' => [
        'required',
        'max:255',
        function ($attribute, $value, $fail) {
            if ($value === 'foo') {
                $fail($attribute.' is invalid.');
            }.
        },
    ],
]);

Ya que sabemos como funciona este tipo de validación, veamos ahora como implementar su uso.

Cómo se crea

Como puedes observar crear la validación es realmente sencillo.

Route::post('check', function (Request $request) {

    $request->validate([
        'email' => function ($attribute, $value, $fail) {

        if ( !(new EmailValidator())->isValid($value, new DNSCheckValidation()) ) {
            $fail(trans('validation.check_email_dns',[$attribute]));
        }

    },]);

    return response()->json(['ok' => 'it´s ok']);
});

Es importante que notes como se puede usar el helper trans. De esta forma puedes obtener el mensaje de error correcto de acuerdo al idioma que tengas configurado en tu aplicación.

Cómo se comprueba

El mejor lugar para probar este tipo de validación es en la ruta o controller que la esta usando.

    /**
     * @test
     */
    public function closure_rule_with_invalid_host() :void
    {
        $response = $this->post('check', ['email' => 'someone@invalid.gmail.com']);
        
        $response->assertRedirect('/');
        $response->assertSessionHasErrors(['email' => 'The email has an invalid host.']);
    }

    /**
     * @test
     */
    public function closure_rule_with_valid_host() :void
    {
        $response = $this->post('check', ['email' => 'someone@gmail.com']);

        $response->assertOk();
        $response->assertSessionHasNoErrors();
    }

Regla de validación personalizada mediante extensión.

Estas validaciones requieren ser registradas en el AppServiceProvider y usando el método extend del facade Validator.

como se muestra en el siguiente ejemplo.

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Validator::extend('foo', function ($attribute, $value, $parameters, $validator) {
            return $value == 'foo';
        });
    }

Observa que el nombre de la validación se proporciona mediante el primer parámetro del método extend, el segundo parámetro es un Closure que recibe el campo que se va a validar($attribute), el valor que tiene ese campo ($value), si la validación requiere parámetros adicionales se pasan mediante $parameters y finalmente recibimos una instancia de Validator ($validator).

La validación mediante extend se utiliza como una validación regular de Laravel.

Route::post('check', function (Request $request) {

    $request->validate([
        'email' => 'foo',
    ]);

    return response()->json(['ok' => 'it´s ok']);
});

Cómo se crea

Muy bien ahora veamos como implementar este tipo de validación a nuestro ejemplo.

Recuerda que la validación se registra en el método boot del AppServiceProvider

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Validator::extend('check_email_dns', function ($attribute, $value, $parameters, $validator) {
            return (new EmailValidator())->isValid($value, new DNSCheckValidation());
        });
    }

Al igual que las validaciones que nos proporciona por defecto Laravel, la validación pasa con un valor de true.

De esta forma es muy sencillo crear la prueba para nuestro ejemplo.

Cómo se comprueba.

La prueba para estos casos se realiza creando un validator mediante el Validator::Make. y comprobar que la validación falla o pasa según sea el caso.

Veamos esto con un ejemplo.

    /**
     * @test
     */
    public function check_extends_version_with_valid_host(): void
    {
        $validator = Validator::make(['email' => 'somebody@gmail.com'], [
            'email' => 'check_email_dns',
        ]);

        $this->assertTrue($validator->passes());
    }

    /**
     * @test
     */
    public function check_extends_version_with_invalid_host(): void
    {
        $validator = Validator::make(['email' => 'somebody@invalid.gmail.com'], [
            'email' => 'check_email_dns',
        ]);

        $this->assertTrue($validator->fails());
        $this->assertEquals(
            'The email has an invalid host.',
            $validator->errors()->first()
        );
    }

Toma nota que también compruebas el mensaje de error en la segunda prueba.

Regla de validación personalizada mediante Rule Objects.

Esta es la ultima opción que veremos y son las que mas uso por que son mas orientadas a objetos. Para crea un Rule object solo tienes que usar el comando rule:make

$ php artisan make:rule Uppercase

El comando anterior crea una clase con solo dos métodos, el método pass debe de regresar true o false según pase o no la validación. Este método también recibe dos parámetros el primero es el campo que se va a validar($attribute) y el segundo el valor que tienes ese campo ($value).

El segundo método se encarga de proporcionar el mensaje de error cuando la validación no pasa.

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class Uppercase implements Rule
{
    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        return strtoupper($value) === $value;
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'The :attribute must be uppercase.';
    }
}

Es importante destacar que el método message puede usar el helper trans.

muy bien veamos como implementar esto a nuestro ejemplo.

Cómo se crea

Usa el comando make:rule para crear la validación CheckEmalDns

php artisan make:rule CheckEmailDns

Edita el archivo app/Rules/CheckEmailDns.php y agrega la lógica que hemos estado usando para validar.

use Egulias\EmailValidator\EmailValidator;
use Egulias\EmailValidator\Validation\DNSCheckValidation;
use Illuminate\Contracts\Validation\Rule;

class CheckEmailDns implements Rule
{
    /**
     * Create a new rule instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        return (new EmailValidator())->isValid($value, new DNSCheckValidation());
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return trans('validation.check_email_dns');
    }
}

Observa como agregue el helper trans para regresa mensajes de error acordes al idioma que este manejando la aplicación.

Cómo se comprueba

La comprobación es realmente sencilla y muy similar a lo que hicimos usando extend

    /**
     * @test
     */
    public function rule_object_version_with_valid_hosts() : void
    {
        $validator = Validator::make(['email' => 'somebody@gmail.com'], [
            'email' => new CheckEmailDns(),
        ]);

        $this->assertTrue($validator->passes());
    }

    /**
     * @test
     */
    public function rule_object_version_with_invalid_hosts() : void
    {

        $validator = Validator::make(['email' => 'somebody@invalid.gmail.com'], [
            'email' => new CheckEmailDns(),
        ]);

        $this->assertTrue($validator->fails());
        $this->assertEquals(
            'The email has an invalid host.',
            $validator->errors()->first()
        );
    }

¿Cómo compruebo mensaje en otro idioma?

Si estas manejando otros idiomas y quieres comprobar los mensaje de error estas de suerte porque esto es muy sencillo. Solo tienes que cambiar el idioma usando setLocale.

    /**
     * @test
     */
    public function check_on_validator_with_invalid_hosts() : void
    {
        $this->app->setLocale('es');

        $validator = Validator::make(['email' => 'somebody@invalid.gmail.com'], [
            'email' => new CheckEmailDns(),
        ]);

        $this->assertTrue($validator->fails());
        $this->assertEquals(
            'El campo correo electrónico no tiene un host válido.',
            $validator->errors()->first()
        );
    }

Recuerda agregar el mensaje en el archivo resource/lang/es/validation.php

'check_email_dns'      => 'El campo :attribute no tiene un host válido.',

Si quieres evitar el tener que escribir una prueba para cada lenguaje puedes usar Data Providers.

Si tienes otra forma de comprobar tus validaciones o tienes alguna duda deja tu comentario me gustaría saber tu opinión.