DEV Community

Denis Augusto
Denis Augusto

Posted on

Middleware é porteiro, não gerente

Ele começou com um if. Hoje tem 80 linhas.

Sabe como é: precisava barrar quem não tem assinatura ativa. Um middleware, três linhas, resolvido.

Depois entrou o período de teste. Depois o plano legado que tem regra diferente. Depois "aproveita que já buscou a assinatura e desconta um crédito". Depois o e-mail de aviso quando faltam 3 dias pro vencimento.

Hoje esse arquivo tem 80 linhas, faz quatro queries, altera dado no banco e dispara e-mail. Ele não é mais um middleware. É um Service que mora na pasta errada e roda em todo request.

E o pior: essa regra não existe pro resto do seu sistema.

O middleware que virou gerente

class VerificarAssinatura
{
    public function handle(Request $request, Closure $next): Response
    {
        $assinatura = $request->user()->assinatura;

        if (! $assinatura || $assinatura->venceu()) {
            return redirect()->route('planos');
        }

        // "aproveita que já tá aqui" 🙃
        if ($assinatura->creditos < 1) {
            return redirect()->route('planos')->withErrors('Sem créditos');
        }

        $assinatura->decrement('creditos');
        $assinatura->update(['ultimo_acesso' => now()]);

        if ($assinatura->vence_em->diffInDays(now()) <= 3) {
            Mail::to($request->user())->send(new AssinaturaVencendo($assinatura));
        }

        return $next($request);
    }
}
Enter fullscreen mode Exit fullscreen mode

Funciona. Passa nos testes de feature. E tem quatro problemas escondidos que só aparecem meses depois.

Problema 1: middleware só existe no HTTP

Esse é o grande. Middleware é uma camada de request HTTP. Ela não roda em outro lugar nenhum.

Então:

  • O comando php artisan relatorio:gerar não desconta crédito.
  • O job na fila não desconta crédito.
  • Sua rota de API que você esqueceu de agrupar não desconta crédito.
  • O tinker passa por cima de tudo.

Você não criou uma regra de negócio. Criou uma regra da porta da frente. Qualquer outra entrada no sistema ignora ela.

E, sério, isso não é hipótese: um dia alguém vai criar um endpoint novo, esquecer o middleware, e a assinatura vira um detalhe decorativo.

Problema 2: cobrou antes de saber se deu certo

Repare na ordem: o decrement('creditos') acontece antes do $next($request).

O controller ainda vai rodar. Pode estourar uma exceção, pode devolver erro de validação, o usuário pode ter mandado um PDF corrompido. Não importa — o crédito já foi.

Cobrança é resultado da operação ter dado certo. Não da requisição ter chegado.

Problema 3: ele decide como responder

redirect()->route('planos') é ótimo pra quem está no navegador. E é péssimo pra todo mundo mais.

Seu app mobile chama a API e recebe... um redirect com HTML de página de planos. O front tenta dar response.json() e explode com um erro que não tem nada a ver com o problema real.

Problema 4: e-mail dentro do middleware

Isso significa que todo request carrega a possibilidade de esperar um SMTP responder. E se o servidor de e-mail estiver lento, seu app inteiro fica lento — inclusive pra quem não tem nada a ver com assinatura vencendo.

O que middleware faz bem

Pensa nele como o porteiro do prédio. Ele tem um trabalho só, e faz muito bem: decide quem passa e quem não passa — e ajusta detalhes na entrada e na saída.

Cabe no middleware:

  • Autenticação e autorização de acesso (auth, signed, verified)
  • Rate limit (throttle)
  • Definir idioma, timezone, tenant do request
  • Mexer em request/response: limpar strings, adicionar header, forçar HTTPS
  • Correlação e observabilidade: trace_id no contexto do log

Não cabe: alterar dado de negócio, cobrar, enviar e-mail, disparar evento de domínio, decidir preço.

O porteiro confere se você mora no prédio. Ele não reajusta seu condomínio.

O mesmo código, cada coisa no lugar

O middleware volta a ser o que era: um porteiro.

class VerificarAssinatura
{
    public function handle(Request $request, Closure $next): Response
    {
        abort_unless($request->user()->temAssinaturaAtiva(), 403, 'Assinatura inativa');

        return $next($request);
    }
}
Enter fullscreen mode Exit fullscreen mode

Uma linha, uma decisão. E abort_unless deixa o Laravel resolver o formato da resposta: HTML pra navegador, JSON pra API. Você não escolhe por ele.

O crédito vira responsabilidade de quem executa a ação:

class GerarRelatorio
{
    public function execute(User $user, array $filtros): Relatorio
    {
        $assinatura = $user->assinatura;

        // a regra mora aqui: vale pro controller, pro job e pro comando
        throw_unless($assinatura->temCredito(), SemCreditosException::class);

        return DB::transaction(function () use ($assinatura, $filtros) {
            $relatorio = Relatorio::gerar($filtros);

            $assinatura->consumirCredito(); // cobra depois de dar certo

            return $relatorio;
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora a regra é do negócio, não do HTTP. Rodou pelo painel, pela API, pelo comando agendado ou pela fila? Mesma regra, mesma cobrança, mesma transação.

E o e-mail de "vence em 3 dias" nem devia estar no caminho de request nenhum: é um comando no scheduler rodando uma vez por dia.

A pegadinha: $request->user() vindo null

Enquanto estamos aqui, a confusão mais comum de middleware: ordem.

Middleware global roda antes do auth. Se você registrar algo global que usa $request->user(), vai receber null e um "call to a member function on null" que não faz sentido nenhum.

Se seu middleware depende de usuário logado, ele é de rota (ou de grupo), depois do auth. Nunca global.

Bônus: e se for coisa pra depois da resposta?

Se a ideia era "registrar o acesso sem atrasar o usuário", existe o lugar certo: middleware terminable, com um método terminate() que roda depois da resposta ir pro navegador.

Mas se o dado importa de verdade, prefira um evento e um listener na fila. Aí funciona igual em qualquer entrada do sistema — e é a mesma lógica de sempre: regra de negócio não deveria depender de como o request chegou.

Antes de você fechar a aba

Teste rápido pro seu middleware: se essa mesma ação rodasse por um comando Artisan, a regra ainda precisaria valer?

Se sim, ela não é do middleware. Ela é do domínio, e o middleware está só escondendo isso de você.

Porteiro barra ou libera. Gerente decide o negócio. Quando o porteiro começa a decidir o negócio, alguém entra pela garagem e ninguém percebe.

Me conta: qual o middleware mais gordo que você já viu? Eu já achei um com 200 linhas que fazia login, criava tenant, populava dado inicial e mandava e-mail de boas-vindas — tudo em handle(). 😬

Top comments (0)