"De onde saiu esse e-mail?"
O cliente abre um chamado: está recebendo dois e-mails de confirmação toda vez que faz um pedido.
Você abre o PedidoController. Não tem Mail:: ali. Abre a Action. Nada. Segue o rastro e chega numa linha inocente:
PedidoConfirmado::dispatch($pedido);
E agora? Quem escuta isso? Você dá um Ctrl+Shift+F em PedidoConfirmado e aparecem seis arquivos. Dois deles mandam e-mail.
Meia hora de investigação pra uma linha de código que, na teoria, deixava tudo mais organizado.
Já passou por isso?
O que Event resolve de verdade
Antes de bater no coitado: Event é ótimo. O problema é o uso, não a ferramenta.
Olha esse controller, que é o cenário que motiva todo mundo a usar Event:
public function store(CriarPedidoRequest $request)
{
$pedido = $this->criarPedido->execute($request->validated());
Mail::to($pedido->cliente)->send(new PedidoConfirmado($pedido));
$this->estoque->baixar($pedido->itens);
$this->slack->avisarVendas($pedido);
$this->analytics->registrarConversao($pedido);
ActivityLog::registrar('pedido.criado', $pedido);
return redirect()->route('pedidos.show', $pedido);
}
Toda vez que o negócio quer mais uma coisinha quando um pedido é criado, você volta aqui e adiciona uma linha. Um ano depois o controller tem quatorze responsabilidades e cinco dependências que não têm nada a ver com "criar pedido".
Com Event, vira isso:
public function store(CriarPedidoRequest $request)
{
$pedido = $this->criarPedido->execute($request->validated());
PedidoConfirmado::dispatch($pedido);
return redirect()->route('pedidos.show', $pedido);
}
O código que cria pedido não precisa saber que existe Slack. Você adiciona um listener novo sem tocar em nada que já funciona. Isso é desacoplamento de verdade.
E o que ele cobra em troca
Só que tem uma conta chegando junto: você trocou legibilidade por flexibilidade.
Na versão de cima, você lê o controller e sabe exatamente tudo que acontece. Na versão de baixo, você lê o controller e sabe... que algo acontece. Em algum lugar. Talvez.
O fluxo virou invisível. E código invisível é código que dá medo de mexer.
Tem um segundo custo: erro em listener vira erro no request. Se o listener não estiver na fila, ele roda no mesmo processo, e uma exceção lá dentro estoura na cara do usuário — depois do pedido já ter sido criado. O desacoplamento é só de código, não de execução.
A regra que eu uso pra decidir
É uma pergunta só:
Isso é parte da operação, ou é uma consequência dela?
Se a operação está errada sem aquele passo, é parte dela. Chamada direta.
Se a operação está completa e correta mesmo que aquele passo falhe, é consequência. Event.
Aplicando no exemplo:
- Baixar estoque → parte da operação. Pedido com estoque não baixado é bug de negócio. Vai direto no código, dentro da mesma transaction.
- E-mail, Slack, analytics, log de atividade → consequência. O pedido continua válido se o Slack estiver fora. Event.
Repara que "baixar estoque" saiu do Event. Muita gente joga tudo pra lá porque parece mais limpo, e aí descobre em produção que um listener falhou silenciosamente e o estoque ficou errado por três dias.
Regra curta: Event é pra efeito colateral, não pra regra de negócio.
Fazendo direito
O listener fica em app/Listeners, e desde o Laravel 11 você nem precisa registrar nada — o framework descobre sozinho pelo type-hint do handle():
namespace App\Listeners;
use App\Events\PedidoConfirmado;
use Illuminate\Contracts\Queue\ShouldQueue;
class EnviarEmailDeConfirmacao implements ShouldQueue
{
public function handle(PedidoConfirmado $event): void
{
Mail::to($event->pedido->cliente)
->send(new ConfirmacaoDePedido($event->pedido));
}
}
Aquele implements ShouldQueue não é detalhe. Ele resolve o segundo custo que falei: o listener vai pra fila, o request devolve na hora, e se o e-mail falhar isso não derruba o pedido. Efeito colateral quase sempre quer ser um listener enfileirado.
E o nome do evento importa muito. Evento é fato consumado, no passado: PedidoConfirmado, UsuarioRegistrou, PagamentoRecusado. Se você escreveu EnviarEmailDePedido, isso não é um evento — é um comando com fantasia. Comando você chama direto.
A pegadinha do Ctrl+Shift+F
Lembra do problema lá do começo, de não saber quem escuta o quê? Tem comando pra isso:
php artisan event:list
Ele lista todos os eventos e seus listeners. Deixa esse comando no bolso — é a primeira coisa a rodar quando alguém pergunta "de onde saiu esse e-mail?".
Duas outras armadilhas que valem o aviso:
Dispatch dentro de transaction. Se o listener está na fila e você despacha o evento antes do commit, o worker pode buscar um registro que ainda não existe. Usa ShouldHandleEventsAfterCommit no listener e dorme tranquilo.
Model events como atalho. Aquele static::created() no boot() da model dispara em seed, em factory de teste, em import em massa. Você vai mandar e-mail de boas-vindas pros 5 mil usuários de um import. Já vi acontecer. 💀
Bônus: um Event, muitos ouvidos
O momento em que Event realmente brilha é quando o mesmo fato interessa a áreas diferentes do sistema que não deveriam se conhecer.
PagamentoAprovado pode liberar o acesso, disparar a nota fiscal, notificar o suporte e alimentar o dashboard — quatro listeners, quatro contextos, zero acoplamento entre eles. Adicionar o quinto não mexe em nenhum dos outros.
Se o seu evento tem um listener só e nunca vai ter outro, você provavelmente só escreveu uma chamada de função com três arquivos a mais.
Antes de você fechar a aba
Da próxima vez que for criar um Event, faz a pergunta: parte da operação ou consequência dela?
E se a resposta for "sei lá", começa com chamada direta. É bem mais fácil transformar código explícito em Event depois do que caçar um fluxo invisível às onze da noite.
Você é time "dispara Event pra tudo" ou time "só quando precisa mesmo"? Conta aí nos comentários — e se você já perdeu meia hora caçando quem mandou um e-mail, sabe exatamente do que eu tô falando.
Top comments (0)