phpsymfonysymfony-messenger

Symfony 5.3: Async emails sent immediately


Edit: This question arose in the attempt to have both synchronous and synchronous emails in the same application. That was not made clear. As of this writing it is not possible, at least not as simply as tried here. See comments by @msg below.

An email service configured to send email asynchronously instead sends emails immediately. This happens with either doctrine or amqp selected as MESSENGER_TRANSPORT_DSN. doctrine transport successfully creates messenger_messages table, but with no contents. This tells me the MESSENGER_TRANSPORT_DSN is observed. Simple test of amqp using RabbitMQ 'Hello World' tutorial shows it is properly configured.

What have I missed in the code below?

Summary of sequence shown below: Opportunity added -> OppEmailService creates email contents -> gets TemplatedEmail() object from EmailerService (not shown) -> submits TemplatedEmail() object to LaterEmailService, which is configured to be async.

messenger.yaml:

framework:
    messenger:
        transports:
            async: '%env(MESSENGER_TRANSPORT_DSN)%'
            sync: 'sync://'

        routing:
            'App\Services\NowEmailService': sync
            'App\Services\LaterEmailService': async

OpportunityController:

class OpportunityController extends AbstractController
{

    private $newOpp;
    private $templateSvc;

    public function __construct(OppEmailService $newOpp, TemplateService $templateSvc)
    {
        $this->newOpp = $newOpp;
        $this->templateSvc = $templateSvc;
    }
...
    public function addOpp(Request $request): Response
    {
...
        if ($form->isSubmitted() && $form->isValid()) {
...
            $volunteers = $em->getRepository(Person::class)->opportunityEmails($opportunity);
            $this->newOpp->oppEmail($volunteers, $opportunity);
...
    }

OppEmailService:

class OppEmailService
{

    private $em;
    private $makeMail;
    private $laterMail;

    public function __construct(
            EmailerService $makeMail,
            EntityManagerInterface $em,
            LaterEmailService $laterMail
    )
    {
        $this->makeMail = $makeMail;
        $this->em = $em;
        $this->laterMail = $laterMail;
    }
...
    public function oppEmail($volunteers, $opp): array
    {
...
            $mailParams = [
                'template' => 'Email/volunteer_opportunities.html.twig',
                'context' => ['fname' => $person->getFname(), 'opportunity' => $opp,],
                'recipient' => $person->getEmail(),
                'subject' => 'New volunteer opportunity',
            ];
            $toBeSent = $this->makeMail->assembleEmail($mailParams);
            $this->laterMail->send($toBeSent);
...
    }

}

LaterEmailService:

namespace App\Services;

use Symfony\Component\Mailer\MailerInterface;

class LaterEmailService
{

    private $mailer;

    public function __construct(MailerInterface $mailer)
    {
        $this->mailer = $mailer;
    }

    public function send($email)
    {
        $this->mailer->send($email);
    }

}

Solution

  • I ended up creating console commands to be run as daily cron jobs. Each command calls a services that create and send emails. The use case is a low volume daily email to registered users informing them of actions that affect them. An example follows:

    Console command:

    class NewOppsEmailCommand extends Command
    {
    
        private $mailer;
        private $oppEmail;
        private $twig;
    
        public function __construct(OppEmailService $oppEmail, EmailerService $mailer, Environment $twig)
        {
            $this->mailer = $mailer;
            $this->oppEmail = $oppEmail;
            $this->twig = $twig;
    
            parent::__construct();
        }
    
        protected static $defaultName = 'app:send:newoppsemaiils';
    
        protected function configure()
        {
            $this->setDescription('Sends email re: new opps to registered');
        }
    
        protected function execute(InputInterface $input, OutputInterface $output)
        {
            $emails = $this->oppEmail->oppEmail();
    
            $output->writeln($emails . ' email(s) were sent');
    
            return COMMAND::SUCCESS;
        }
    
    }
    

    OppEmailService:

    class OppEmailService
    {
    
        private $em;
        private $mailer;
    
        public function __construct(EmailerService $mailer, EntityManagerInterface $em)
        {
            $this->mailer = $mailer;
            $this->em = $em;
        }
    
        /**
         * Send new opportunity email to registered volunteers
         */
        public function oppEmail()
        {
            $unsentEmail = $this->em->getRepository(OppEmail::class)->findAll(['sent' => false], ['volunteer' => 'ASC']);
            if (empty($unsentEmail)) {
                return 0;
            }
    
            $email = 0;
            foreach ($unsentEmail as $recipient) {
                $mailParams = [
                    'template' => 'Email/volunteer_opportunities.html.twig',
                    'context' => [
                        'fname' => $recipient->getVolunteer()->getFname(),
                        'opps' => $recipient->getOpportunities(),
                    ],
                    'recipient' => $recipient->getVolunteer()->getEmail(),
                    'subject' => 'New opportunities',
                ];
                $this->mailer->assembleEmail($mailParams);
                $recipient->setSent(true);
                $this->em->persist($recipient);
                $email++;
            }
            $this->em->flush();
    
            return $email;
        }
    
    }
    

    EmailerService:

    class EmailerService
    {
    
        private $em;
        private $mailer;
    
        public function __construct(EntityManagerInterface $em, MailerInterface $mailer)
        {
            $this->em = $em;
            $this->mailer = $mailer;
        }
    
        public function assembleEmail($mailParams)
        {
            $sender = $this->em->getRepository(Person::class)->findOneBy(['mailer' => true]);
            $email = (new TemplatedEmail())
                    ->to($mailParams['recipient'])
                    ->from($sender->getEmail())
                    ->subject($mailParams['subject'])
                    ->htmlTemplate($mailParams['template'])
                    ->context($mailParams['context'])
            ;
    
            $this->mailer->send($email);
    
            return $email;
        }
    
    }