Validação Customizada no Laravel

O Laravel é um framework excepcional que oferece diversas facilidades, como um amplo conjunto de regras de validação para serem aplicadas em nossos projetos. No entanto, em determinados casos, podemos precisar de uma regra de validação própria, baseada na nossa regra de negócio. Nessas situações, o Laravel conta com um mecanismo extremamente simples para a criação e utilização da nossa própria regra de validação em conjunto com as regras já disponíveis, tornando todo o processo muito mais fácil. Tudo o que precisamos fazer é criar um objeto de regra para o Laravel.

Para exemplificar, iremos criar uma regra de validação para um código próprio que contém três partes distintas: um número de 4 dígitos que representa uma entidade – por exemplo, um produto – seguido por um traço, um caractere maiúsculo e um número, outro traço e, por fim, três letras que representam um país associado (que está especificado em um Enum). Um exemplo de código válido dentro dessa regra seria 0541-A3-BRA.

Agora, iremos criar uma regra personalizada no Laravel para validar o nosso código utilizando o comando “artisan“.

php artisan make:rule CodigoProprio

O comando irá gerar um arquivo na pasta app/Rules com o seguinte conteúdo:

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class CodigoProprio implements ValidationRule
{
    /**
     * Run the validation rule.
     *
     * @param  \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString  $fail
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        //
    }
}

A classe criada contém um método chamado “validate“. Esse método recebe três parâmetros: uma string chamada “$attribute“, a qual é o nome do campo que está sendo validado; um”$value“, o qual é o valor a ser testado para a validação; e uma Closure chamada “$fail“, que será executada caso a validação falhe.

Vamos então criar o código que valida nosso código próprio.

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $produtoId = intval(substr($value, 0, 4));
        $produto = Produto::find($produtoId);
        $paises = join('|', array_map( fn($pais) => $pais->value, PaisEnum::cases()) );
        
        $pattern = "/^[0-9]{4}-[A-Z][0-9]-($paises)$/";

        if(!preg_match($pattern, $value) || $produto === null){
            $fail("O código proprio '{$value}' é inválido");
        }

    }

Para realizar a validação, na linha 3 do método “validate“, utilizamos a função “substr” para obter os primeiros 4 caracteres do valor informado na variável “$value” e, em seguida, convertemos esse valor para inteiro. Na linha seguinte, utilizamos esse valor para buscar uma entidade Produto pelo código correspondente (lembrando que, caso a entidade não exista, será retornado null).

Na linha 5, utilizamos a função “array_map” para mapear todos os valores do PaisEnum em um array de string. Em seguida, concatenamos esses valores utilizando a função “join“, separando-os com o o caracter “ | ” (ou na regex). Essa string será utilizada posteriormente na expressão regular que iremos utilizar.

Na linha 7, definimos o padrão da expressão regular que será utilizada para validar o código. Essa expressão regular é composta por 4 dígitos ([0-9]{4}), um traço (-), uma letra maiúscula seguida de um dígito ([A-Z][0-9]), outro traço (-) e um dos valores do enum de países que já havíamos formatado na linha 5.

Em seguida, na linha 9, utilizamos um bloco “if” para verificar se o valor informado na variável “$value” não corresponde ao regex que definimos ou se o id do Produto não existe. Caso essa condição seja verdadeira, chamamos a Closure$fail“, que será responsável por rejeitar o valor informado com a mensagem de erro correspondente.

Com a classe devidamente implementada, podemos utilizá-la da mesma forma que uma regra de validação pronta. A seguir, apresentamos um exemplo de como utilizar a classe customizada por meio da facade Validator:

$validator = Validator::make(
    data: 
    [
        'codigo-proprio' => $codigoProprio
    ], 
    rules: 
    [
        'codigo-proprio' => new CodigoProprio(),
    ]
);

Dessa forma, é possível construir testes facilmente para as nossas regras. A seguir, apresentamos alguns testes utilizando o PestPHP:

uses(TestCase::class);

beforeEach(function(){
    Artisan::call('migrate');
    
    Produto::factory()->create(['id' => 1001]);
    Produto::factory()->create(['id' => 958]);
    Produto::factory()->create(['id' => 25]);
    Produto::factory()->create(['id' => 376]);
    Produto::factory()->create(['id' => 946]);
    Produto::factory()->create(['id' => 46]);
});

test('deve validar os código próprios corretos', function($codigoProprio){   
    $validator = Validator::make(
    data: 
        [
            'codigo-proprio' => $codigoProprio
        ], 
    rules: 
        [
            'codigo-proprio' => new CodigoProprio(),
]
    );

    expect($validator->passes())->toBeTrue();

})->with([
    '1001-A1-BRA',
    '0958-B2-URU',
    '0025-A3-CHI',
    '0376-C2-BRA',
    '0946-A2-ARG',
    '0046-C1-BRA',
]);

test('deve rejeitar os código próprios errados', function($codigoProprio){
   
    $validator = Validator::make(
        data: 
        [
            'codigo-proprio' => $codigoProprio
        ], 
        rules: 
        [
            'codigo-proprio' => new CodigoProprio(),
        ]
    );
    expect($validator->passes())->toBeFalse();

    expect($validator->errors()->first())->toBe("O código proprio '{$codigoProprio}' é inválido");

})->with([
    '1001-A1-EUA', // pais não existe
    '0958-bc-URU', // codigo do meio inválido
    '0025-aa-CHI', // codigo do meio inválido
    '0001-C2-BRA', // id do prodito não exist
    '06-A2-ARG', // id não possui 1 digitos
    '0046-22-BRA', // codigo do meio invalido
]);

Além disso, podemos utilizar a nossa regra de validação customizada em um FormRequest sem qualquer problema:

class MyRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
     */
    public function rules(): array
    {
        return [
            'codigo_proprio' => new CodigoProprio()
        ];
    }
}

Usando Validação com Closure

Também é possível realizar uma validação customizada utilizando uma Closure em vez de uma classe. Essa Closure deve possuir a mesma assinatura do método “validate” da classe de regra.

Validator::make(
    data: $request->all(), 
    rules: 
        [
            'codigo_proprio' => function (string $attribute, mixed $value, Closure $fail){
                $produtoId = intval(substr($value, 0, 4));
                $produto = Produto::find($produtoId);
                $paises = join('|', array_map( fn($pais) => $pais->value, PaisEnum::cases()) );                        
                $pattern = "/^[0-9]{4}-[A-Z][0-9]-($paises)$/";
                if(!preg_match($pattern, $value) || $produto === null){
                    $fail("O código proprio '{$value}' é inválido");
                }
            },
    ]
)->validate();

Embora seja possível, não é muito aconselhável colocar uma regra de validação complexa em uma Closure. No exemplo acima, a Closure já ficou um pouco difícil de ler e entender.

Bom, era isso. Espero ter ajudado! Se você deseja saber mais sobre as validações do Laravel, não deixe de conferir diretamente na documentação oficial.

T++