DEV Community

Denis Augusto
Denis Augusto

Posted on

Validação condicional no Laravel sem virar espaguete de if

"Esse campo aqui só é obrigatório às vezes"

Toda regra de validação começa simples. required, email, min:8, beleza. Aí o PO chega e solta a frase mágica:

"Ah, mas o CNPJ só é obrigatório se a pessoa for pessoa jurídica."

E lá se foi a simplicidade. De repente sua validação depende de outro campo, e você faz o que todo mundo faz na primeira vez: joga um if no controller, valida um pedaço, depois valida outro pedaço, e o método store() vira um monstro de ramificação.

Já passou por isso? A boa notícia é que o Laravel tem ferramenta pra praticamente todo tipo de "depende". Você só precisa parar de resolver tudo na mão.

O problema: validação picada no controller

O jeito ingênuo é validar em pedaços, dependendo do que veio:

public function store(Request $request)
{
    $regras = [
        'tipo' => 'required|in:pf,pj',
        'nome' => 'required|string',
    ];

    // e aqui começa a bagunça
    if ($request->tipo === 'pj') {
        $regras['cnpj'] = 'required|string|size:14';
    } else {
        $regras['cpf'] = 'required|string|size:11';
    }

    if ($request->quer_newsletter) {
        $regras['email'] = 'required|email';
    }

    $request->validate($regras);

    // ...
}
Enter fullscreen mode Exit fullscreen mode

Funciona? Funciona. Mas repara no que aconteceu: sua regra de negócio agora tá espalhada em construção manual de array, o controller sabe demais sobre validação, e testar isso é um inferno. Cada if novo é mais uma ramificação pra dar errado.

A solução 1: required_if e amigos

Pra dependências simples entre campos, o Laravel já tem regras prontas. Você quase nunca precisa de if pra isso:

public function rules(): array
{
    return [
        'tipo' => 'required|in:pf,pj',
        'cnpj' => 'required_if:tipo,pj|nullable|size:14',
        'cpf'  => 'required_if:tipo,pf|nullable|size:11',
        'email' => 'required_if:quer_newsletter,true|email',
    ];
}
Enter fullscreen mode Exit fullscreen mode

Tem uma família inteira dessas: required_unless, required_with, required_without, required_with_all, exclude_if, prohibited_if... Antes de escrever um if, vale conferir se já não existe uma regra que faz exatamente o que você quer. Na maioria dos casos, existe.

A solução 2: Rule::when pra condição no meio da regra

Quando a condição não é "um campo depende de outro", mas sim algo do contexto (o usuário logado, um plano, uma feature flag), o Rule::when deixa a coisa declarativa dentro do próprio array:

use Illuminate\Validation\Rule;

public function rules(): array
{
    return [
        'titulo' => 'required|string',
        // só valida como obrigatório+único se for admin
        'slug' => Rule::when(
            $this->user()->isAdmin(),
            ['required', 'unique:posts,slug'],
            ['nullable'] // senão, opcional
        ),
    ];
}
Enter fullscreen mode Exit fullscreen mode

Leu de cima pra baixo e entendeu a regra inteira, sem sair caçando um if três métodos acima.

A solução 3: sometimes() pra lógica que não cabe no array

E quando a condição é complexa de verdade, tipo "valida o desconto só se ele veio E o pedido passa de 500 reais"? Aí entra o método sometimes() do Validator, ideal no withValidator() de um Form Request:

use Illuminate\Validation\Validator;

public function withValidator(Validator $validator): void
{
    $validator->sometimes('desconto', 'numeric|max:50', function ($input) {
        return $input->cupom && $input->total > 500;
    });
}
Enter fullscreen mode Exit fullscreen mode

A regra do desconto só é aplicada se aquele closure retornar true. Toda a lógica maluca fica isolada num lugar só, com nome, testável, e longe do caminho principal.

Como usar na prática

Validação de array/coleção — aquele formulário com N itens repetidos (um carrinho, uma lista de convidados). Usa o .*:

public function rules(): array
{
    return [
        'itens' => 'required|array|min:1',
        'itens.*.produto_id' => 'required|exists:produtos,id',
        'itens.*.quantidade' => 'required|integer|min:1',
    ];
}
Enter fullscreen mode Exit fullscreen mode

Uma regra só valida os N itens do array. Nada de foreach validando na mão.

Mensagem customizada por condição — combina com messages() no Form Request pra falar a língua do usuário: "Informe o CNPJ para pessoa jurídica" em vez do genérico.

A pegadinha: nullable vs a ausência da regra

Aqui cai muita gente. Quando você usa required_if, quase sempre precisa combinar com nullable no mesmo campo. Sem isso, se o campo vier vazio (mas presente no request), outras regras como size:14 ou email ainda vão tentar validar a string vazia e reclamar.

// ❌ se vier cnpj='' num PF, o size:14 reclama
'cnpj' => 'required_if:tipo,pj|size:14',

// ✅ vazio é permitido quando não obrigatório
'cnpj' => 'required_if:tipo,pj|nullable|size:14',
Enter fullscreen mode Exit fullscreen mode

nullable diz "pode ser nulo/vazio, e nesse caso ignora as outras regras". É o par natural do required_if. Guarda isso que economiza uma hora de debug.

Bônus: e agora?

Abre o Form Request (ou controller) mais cheio de if do seu projeto e faz o exercício: cada if que existe só pra montar array de regra provavelmente tem um required_if, Rule::when ou sometimes equivalente esperando pra substituir ele.

E se você ainda valida direto no controller com $request->validate(), esse é o empurrãozinho pra migrar tudo pra um Form Request. Todo esse arsenal fica muito mais organizado lá dentro.

Antes de você fechar a aba

A regra de ouro: se você tá construindo array de validação com if, o Laravel provavelmente já tem uma forma declarativa de fazer isso. Vale abrir a doc de validação e passar o olho na lista de regras — tem umas 20 que quase ninguém usa e resolvem casos que a gente insiste em fazer na mão.

Qual regra de validação condicional você mais usa? Ou tem algum caso cabeludo que não soube resolver? Joga nos comentários que a gente destrincha junto. 👇


Top comments (0)