0

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);
    }

}
geoB
  • 4,578
  • 5
  • 37
  • 70
  • 1
    In messenger.yaml under "routing" you should map message classes to transports, not services to transports as you did. – xtx Dec 20 '21 at 02:31
  • My understanding of the Message class is that it expects `$context` to be a string. [See this](https://symfony.com/doc/current/messenger.html#creating-a-message-handler). In [symfonycasts](https://symfonycasts.com/screencast/mailer/async-emails) they only show all emails as async – geoB Dec 20 '21 at 04:12
  • Message class is kind of a Dto, that holds data needed for a message handler to act upon. It can contain any data, not necesseraly strings. When pushed to a message bus, the message gets serialized anyway. The whole process should look like this: you create a message, push it to a message bus (MessageBusInterface). When the message is received, a handler processes it. – xtx Dec 20 '21 at 04:31
  • I don't see the part in your code where you push a message into MessageBusInterface::dispatch. In OpportunityController you call OppEmailService, which in its turn calls LaterEmailService, which sends the email. No Symfony Messager is involved here. – xtx Dec 20 '21 at 04:31
  • As mentioned earlier, your `framework.messenger.routing` configuration is wrong. You should be mapping message classes, not services. – yivi Dec 20 '21 at 07:06
  • With ` $bus->dispatch(new AsyncEmail($toBeSent));` in controller, handler simply does `__invoke(AsyncEmail $toBeSent)`, routing is `'App\Controller\OpportunityController': async`, email is still sent immediately. Regardless of transport. – geoB Dec 20 '21 at 14:15
  • Don't map controller to async transport, makes no sense :) Try mapping AsyncEmail instead – xtx Dec 20 '21 at 16:08
  • Ok. But event `'App\Message\AsyncEmail': async` sends immediately. But oddly, `php bin/console messenger:consume async -vv` returns `INFO [messenger] Received message App\Message\AsyncEmail...` well after email has been sent. `doctrine` transport will show its table contents as empty set before running `consume`. Boy am I confused! – geoB Dec 20 '21 at 16:50
  • what is your MESSENGER_TRANSPORT_DSN? – xtx Dec 20 '21 at 18:09
  • I've experimented with both `MESSENGER_TRANSPORT_DSN=doctrine://default` and `MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages`. With `amqp` I can see activity using the RabbitMQ Management plugin. – geoB Dec 20 '21 at 19:06
  • You [cannot have Mailer route to multiple transports](https://github.com/symfony/symfony/issues/36348), not out of the box. You can dispatch your own messages with AMQP as I explained in [this answer](https://stackoverflow.com/questions/64356463/#64373705). – msg Dec 20 '21 at 19:39
  • Only one has been configured at a time. I wanted to see if the issue was related to the transport mechanism. The transport mechanism is not the reason email is sent immediately. – geoB Dec 20 '21 at 19:45
  • @geoB No, is not, it is because there is no transport configured for `SendEmailMessage` so it defaults to `sync`. That's the message dispatched internally by [the Mailer code](https://github.com/symfony/mailer/blob/309ba427654351dcad9691bef817b96920ebd2cf/Mailer.php#L55). In the issue linked in the previous comment there is an alternative solution to mine, implement a custom `MailerInterface` and dispatch to the desired transport. – msg Dec 20 '21 at 19:50
  • I'll try working with the answer you mention. I was led astray, in part, by this statement: "Great. As soon as you install Messenger, when Mailer sends an email, internally, it will automatically start doing that by dispatching a message through Messenger. Hit Shift + Shift to open a class called SendEmailMessage" at [symfonycasts](https://symfonycasts.com/screencast/mailer/async-emails) – geoB Dec 20 '21 at 19:57
  • And that's what happens, Mailer internally dispatchs a Messenger message (the `SendEmailMessage`), but if you don't configure a transport for it it defaults to `sync` so Messenger still handles it, but synchronously. As I say in the answer, my approach has some limitations, so you might be better off copying the Mailer code to dispatch your specific messages. – msg Dec 20 '21 at 20:10

1 Answers1

0

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;
    }

}
geoB
  • 4,578
  • 5
  • 37
  • 70