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);
}
}
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:gerarnã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
tinkerpassa 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_idno 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);
}
}
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;
});
}
}
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)