Matoušův zápisník o všem možném

Asynchroni maily v nette

Na projektu tescorecepty.cz neustále řeším všelijakou mailovou komunikaci. Ať to jsou potvrzení registrace, newslettery, zapomenutá hesla nebo notikace administrátorům.

Původně se maily posílaly okamžitě v modelu nebo presenteru. Což sebou samozřejmě neslo řadu problému. Namátkou zdržování requestu v případě kdy se posílalo více mailů najednou. Selhání celé stránky při neošetřené chybě v mailu a podobně.

Proto jsem začal hledat jiné řešení.

#Fronta nás zachrání

Hlavní část řešení přinesl článek Filipa Procházky o RabbitMQ a jeho rozšíření do Nette Kdyby/RabbitMq. Dobře mám nástroj který zajistí posílání mailů na pozadí. Jak ale elegantně dostat do RabbitMQ consumeru samotný mail.

Napsal jsem několik variant, několikrát se spálil a nakonec jsem skončil u velmi jednoduchého řešení.

Jeden producer a consumer pro všechny maily. Do produceru předávám serializovaný objekt s jednoduchým interfacem.

interface IMail {
  function sendMail(IMailer $mailer);
}

Consumer je pak opět velmi jednoduchý.

class EmailConsumer extends Object
{
    /** @var IMailer */
    protected $mailer;

    /** @var Logger */
    protected $logger;

    function __construct(IMailer $mailer, Logger $logger)
    {
        $this->mailer = $mailer;
        $this->logger = $logger;
    }

    public function send(AMQPMessage $message)
    {
        $task = unserialize($message->body);

        if ($task instanceof IMail) {
            $task->sendMail($this->mailer);
        } else {
            $this->logger
            ->addError('Invalid email task ' . Debugger::dump($task, true));
        }
    }
}

Pro jednotlivé maily pak vytvořím třídy které implementují IMail. V jejich implementaci se pak nemusím moc omezovat.

V budoucnosti možná budu potřebovat předávat závislosti do deserializovaných objektů. Druhým parametrem Imail::sendMail pak může být Nette container. Třeba mě napadne lepší řešení. Zatím jsem spokojený s tímto.