DEV Community

Denis Augusto
Denis Augusto

Posted on

Seu código dispara um Event e você não faz ideia do que acontece depois

"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);
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)