Laravel 6 min de leitura

Como Validar Regras de Negócio Complexas no Laravel com o Método after()

Como Validar Regras de Negócio Complexas no Laravel com o Método after()

Recentemente, enquanto desenvolvia o Preve em live no YouTube, me deparei com um problema interessante: como validar que o tipo de uma transação corresponde ao tipo da sua categoria?

Normalmente, quando trabalhamos com validação no Laravel, as regras simples do método rules() resolvem tranquilo. Mas nesse caso, eu precisava comparar dois campos e consultar o banco de dados para buscar o tipo da categoria. Foi aí que descobri o método after() dos FormRequests - e é sobre ele que vou falar neste artigo.

O Problema

Imagine que você tem uma aplicação financeira onde usuários registram transações. Cada transação tem:

  • Um campo tipo (income/expense)
  • Uma categoria associada

A regra de negócio é: uma transação só pode ser do tipo income ou expense se a categoria também for do mesmo tipo.

Com as regras básicas de validação do nosso FormRequest, conseguimos validar se os campos existem e se estão no formato correto:

public function rules(): array
{
    return [
        'category_id' => ['required', 'integer', 'exists:categories,id'],
        'type'        => ['required', 'in:income,expense'],
        // ...
    ];
}

Mas como validar se o tipo da transação corresponde ao tipo da categoria? É aqui que entramos em um território de validação mais complexa.

A Solução: O Método after()

O Laravel oferece o método after() nos FormRequests para executar validações adicionais após as regras básicas passarem. Isso é perfeito para regras de negócio que precisam consultar o banco de dados ou comparar múltiplos campos.

Como Funciona

O método after() retorna um array de callables que serão executados após a validação inicial. Cada callable recebe uma instância do Validator:

use Illuminate\Validation\Validator;

public function after(): array
{
    return [
        function (Validator $validator) {
            // Sua lógica de validação personalizada aqui
					  // E pode ter mais de uma 😅
        }
    ];
}

Exemplo Prático

Vamos ver como resolver nosso problema de validação de transações:

<?php

namespace App\Http\Requests;

use App\Models\Category;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;

final class TransactionRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'category_id'      => ['required', 'integer', 'exists:categories,id'],
            'tag_id'           => ['nullable', 'integer', 'exists:tags,id'],
            'amount'           => ['required', 'numeric'],
            'type'             => ['required', 'in:income,expense'],
            'description'      => ['required', 'string', 'min:3'],
            'notes'            => ['nullable', 'string'],
            'transaction_date' => ['required', 'date'],
        ];
    }

    public function after(): array
    {
        return [
            function (Validator $validator) {
                // 1. Se já houver erros, não executar validação adicional
                if ($validator->errors()->isNotEmpty()) {
                    return;
                }

                // 2. Buscar a categoria do banco de dados
                $category = Category::query()->find($this->category_id);

                // 3. Verificar se o tipo da categoria corresponde ao tipo da transação
                if ($category && $category->type->value !== $this->type) {
                    $validator->errors()->add(
                        'category_id',
                        "The selected category must be of type {$this->type}."
                    );
                }
            },
        ];
    }
}

Entendendo o Código

1. Verificação de erros existentes:

if ($validator->errors()->isNotEmpty()) {
    return;
}

Se as regras básicas já falharam, não faz sentido executar validações adicionais. Isso evita consultas desnecessárias ao banco de dados.

2. Consulta ao banco de dados:

$category = Category::query()->find($this->category_id);

Aqui buscamos a categoria para verificar seu tipo. Como o category_id já passou pela validação exists, sabemos que ele existe.

3. Validação da regra de negócio:

if ($category && $category->type->value !== $this->type) {
    $validator->errors()->add(
        'category_id',
        "The selected category must be of type {$this->type}."
    );
}

Comparamos o tipo da categoria com o tipo da transação. Se não coincidirem, adicionamos um erro personalizado.

Quando Usar o Método after()

Use o método after() quando você precisar:

  1. Validar relações entre múltiplos campos

    • Como no nosso exemplo: tipo da transação vs. tipo da categoria
  2. Executar consultas ao banco de dados para validação

    • Quando as regras exists ou unique não são suficientes
  3. Validar regras de negócio complexas

    • Cálculos, comparações de datas, verificações de permissões, etc.
  4. Evitar queries desnecessárias

    • Só executar consultas pesadas se a validação básica passar

Exemplo Adicional: Validação com Classes Invocáveis

Para validações mais complexas ou reutilizáveis, você pode criar classes invocáveis:

namespace App\Validation;

use Illuminate\Validation\Validator;

class ValidateCategoryType
{
    public function __invoke(Validator $validator): void
    {
        if ($validator->errors()->isNotEmpty()) {
            return;
        }

        $category = Category::find($validator->getData()['category_id']);

        if ($category && $category->type->value !== $validator->getData()['type']) {
            $validator->errors()->add(
                'category_id',
                'The selected category must be of type ' . $validator->getData()['type'] . '.'
            );
        }
    }
}

E usar no FormRequest:

use App\Validation\ValidateCategoryType;

public function after(): array
{
    return [
        new ValidateCategoryType,
        function (Validator $validator) {
            // Outras validações...
        }
    ];
}

Por que não validar no Controller?

Uma alternativa seria validar essa regra de negócio diretamente no controller após receber os dados do FormRequest. Porém, isso violaria o princípio de responsabilidade única - o FormRequest existe justamente para centralizar toda a lógica de validação. Ao usar o método after(), mantemos a separação de responsabilidades e deixamos o controller focado apenas em orquestrar a aplicação.

Conclusão

Quando me deparei com esse problema durante as lives do Preve, minha primeira reação foi pensar: "vou validar isso no controller mesmo". Mas aí parei pra pensar - isso ia bagunçar toda a organização do código e misturar validação com lógica de negócio.

Foi conversando com a Claude Code (kkkkk mas depois fui ler lá na Documentação e tá tudo muito bem esclarecido) que descobri o método after(), e cara, mudou completamente minha forma de lidar com validações complexas. Não é só sobre deixar o código mais limpo (que também é importante), mas sobre manter as responsabilidades bem definidas: FormRequest cuida de validação, Controller orquestra tudo.

Se você está enfrentando uma situação onde as regras básicas do Laravel não dão conta - seja porque precisa consultar o banco, comparar múltiplos campos, ou implementar alguma regra de negócio mais elaborada - dá uma chance pro método after(). Ele tá ali justamente pra isso.

E se você se interessou pelo Preve e quiser acompanhar o desenvolvimento, tem todas as lives gravadas no YouTube onde eu vou construindo o projeto do zero, enfrentando problemas reais como esse e mostrando as soluções que vou encontrando pelo caminho. 🚀


Referências: