From b8bbceda222a90db1d23f7bd7c877f812ad097a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Poirier=20Th=C3=A9or=C3=AAt?= Date: Mon, 18 Dec 2023 11:09:17 -0500 Subject: [PATCH 1/5] [TesterBundle] add EventListenerTestTrait to check if event listener are properly registered --- .../EventListenerTestTrait.php | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 packages/tester-bundle/EventDispatcher/EventListenerTestTrait.php diff --git a/packages/tester-bundle/EventDispatcher/EventListenerTestTrait.php b/packages/tester-bundle/EventDispatcher/EventListenerTestTrait.php new file mode 100644 index 00000000..1bd38102 --- /dev/null +++ b/packages/tester-bundle/EventDispatcher/EventListenerTestTrait.php @@ -0,0 +1,27 @@ +get('event_dispatcher')->getListeners(); + + $events = []; + foreach ($listeners as $eventName => $eventListeners) { + foreach ($eventListeners as $eventListener) { + if ($eventListener[0] instanceof $className) { + $events[$eventName][] = $eventListener[1]; + } + } + } + + ksort($events); + ksort($expectedEvents); + + TestCase::assertSame($expectedEvents, $events); + } +} From 28e833ae1b2bfc232f554d324f8cd5e6faacde4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Poirier=20Th=C3=A9or=C3=AAt?= Date: Mon, 18 Dec 2023 11:50:45 -0500 Subject: [PATCH 2/5] [Mailer] Separation of concern between listener and mail composor --- .../Compiler/EmailWriterCompilerPass.php | 4 +- .../Integration/MailerIntegration.php | 4 +- .../Integration/MailerIntegrationTest.php | 13 ++- ...ilWriterListener.php => EmailComposer.php} | 72 ++++-------- .../EventListener/EmailComposerListener.php | 38 +++++++ ...ListenerTest.php => EmailComposerTest.php} | 104 ++---------------- .../EmailComposerListenerTest.php | 104 ++++++++++++++++++ .../EmailComposerListenerTest.php | 30 +++++ 8 files changed, 221 insertions(+), 148 deletions(-) rename packages/mailer/{EventListener/EmailWriterListener.php => EmailComposer.php} (77%) create mode 100644 packages/mailer/EventListener/EmailComposerListener.php rename packages/mailer/Tests/{EventListener/EmailWriterListenerTest.php => EmailComposerTest.php} (63%) create mode 100644 packages/mailer/Tests/EventListener/EmailComposerListenerTest.php create mode 100644 tests/Mailer/EventListener/EmailComposerListenerTest.php diff --git a/packages/framework-extra-bundle/DependencyInjection/Compiler/EmailWriterCompilerPass.php b/packages/framework-extra-bundle/DependencyInjection/Compiler/EmailWriterCompilerPass.php index 8018e6b5..816d296a 100644 --- a/packages/framework-extra-bundle/DependencyInjection/Compiler/EmailWriterCompilerPass.php +++ b/packages/framework-extra-bundle/DependencyInjection/Compiler/EmailWriterCompilerPass.php @@ -3,8 +3,8 @@ namespace Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Compiler; use Draw\Component\Core\Reflection\ReflectionAccessor; +use Draw\Component\Mailer\EmailComposer; use Draw\Component\Mailer\EmailWriter\EmailWriterInterface; -use Draw\Component\Mailer\EventListener\EmailWriterListener; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -16,7 +16,7 @@ class EmailWriterCompilerPass implements CompilerPassInterface public function process(ContainerBuilder $container): void { try { - $emailWriterListenerDefinition = $container->findDefinition(EmailWriterListener::class); + $emailWriterListenerDefinition = $container->findDefinition(EmailComposer::class); } catch (ServiceNotFoundException) { return; } diff --git a/packages/framework-extra-bundle/DependencyInjection/Integration/MailerIntegration.php b/packages/framework-extra-bundle/DependencyInjection/Integration/MailerIntegration.php index 3aafed7e..711e6de9 100644 --- a/packages/framework-extra-bundle/DependencyInjection/Integration/MailerIntegration.php +++ b/packages/framework-extra-bundle/DependencyInjection/Integration/MailerIntegration.php @@ -2,6 +2,7 @@ namespace Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Integration; +use Draw\Component\Mailer\EmailComposer; use Draw\Component\Mailer\EmailWriter\DefaultFromEmailWriter; use Draw\Component\Mailer\EmailWriter\EmailWriterInterface; use Draw\Component\Mailer\EventListener\EmailCssInlinerListener; @@ -28,8 +29,7 @@ public function load(array $config, PhpFileLoader $loader, ContainerBuilder $con $loader, $namespace = 'Draw\\Component\\Mailer\\', \dirname( - (new \ReflectionClass(EmailWriterInterface::class))->getFileName(), - 2 + (new \ReflectionClass(EmailComposer::class))->getFileName(), ), ); diff --git a/packages/framework-extra-bundle/Tests/DependencyInjection/Integration/MailerIntegrationTest.php b/packages/framework-extra-bundle/Tests/DependencyInjection/Integration/MailerIntegrationTest.php index 1ec1cce0..37f75d8b 100644 --- a/packages/framework-extra-bundle/Tests/DependencyInjection/Integration/MailerIntegrationTest.php +++ b/packages/framework-extra-bundle/Tests/DependencyInjection/Integration/MailerIntegrationTest.php @@ -5,11 +5,12 @@ use Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Integration\IntegrationInterface; use Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Integration\MailerIntegration; use Draw\Component\Mailer\Command\SendTestEmailCommand; +use Draw\Component\Mailer\EmailComposer; use Draw\Component\Mailer\EmailWriter\DefaultFromEmailWriter; use Draw\Component\Mailer\EmailWriter\EmailWriterInterface; +use Draw\Component\Mailer\EventListener\EmailComposerListener; use Draw\Component\Mailer\EventListener\EmailCssInlinerListener; use Draw\Component\Mailer\EventListener\EmailSubjectFromHtmlTitleListener; -use Draw\Component\Mailer\EventListener\EmailWriterListener; use Draw\Component\Mailer\Twig\TranslationExtension; use PHPUnit\Framework\Attributes\CoversClass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; @@ -109,9 +110,15 @@ public static function provideTestLoad(): iterable ] ), new ServiceConfiguration( - 'draw.mailer.event_listener.email_writer_listener', + 'draw.mailer.email_composer', [ - EmailWriterListener::class, + EmailComposer::class, + ] + ), + new ServiceConfiguration( + 'draw.mailer.event_listener.email_composer_listener', + [ + EmailComposerListener::class, ] ), new ServiceConfiguration( diff --git a/packages/mailer/EventListener/EmailWriterListener.php b/packages/mailer/EmailComposer.php similarity index 77% rename from packages/mailer/EventListener/EmailWriterListener.php rename to packages/mailer/EmailComposer.php index e44647f7..50c48bc3 100644 --- a/packages/mailer/EventListener/EmailWriterListener.php +++ b/packages/mailer/EmailComposer.php @@ -1,32 +1,46 @@ ['composeMessage', 200], - ]; } - public function __construct(private ContainerInterface $serviceLocator) + public function compose(Message $message, Envelope $envelope): void { + $headers = $message->getHeaders(); + + foreach ($this->getTypes($message) as $type) { + foreach ($this->getWriters($type) as $writerConfiguration) { + [$writer, $writerMethod] = $writerConfiguration; + $writer = $writer instanceof EmailWriterInterface ? $writer : $this->serviceLocator->get($writer); + \call_user_func([$writer, $writerMethod], $message, $envelope); + } + } + + if ($message instanceof TemplatedEmail) { + if ($template = $message->getHtmlTemplate()) { + $headers->add(new UnstructuredHeader('X-DrawEmail-HtmlTemplate', $template)); + } + if ($template = $message->getTextTemplate()) { + $headers->add(new UnstructuredHeader('X-DrawEmail-TextTemplate', $template)); + } + } } public function registerEmailWriter(EmailWriterInterface $emailWriter): void @@ -78,46 +92,6 @@ private function sortWriters(string $email): void $this->sortedWriters[$email] = array_merge(...$this->writers[$email]); } - public function composeMessage(MessageEvent $event): void - { - if ($event->isQueued()) { - return; - } - - $message = $event->getMessage(); - if (!$message instanceof Message) { - return; - } - - $headers = $message->getHeaders(); - if ($headers->has('X-DrawEmail')) { - return; - } - - $headers->add(new UnstructuredHeader('X-DrawEmail', '1')); - - $envelope = $event->getEnvelope(); - - $types = $this->getTypes($message); - - foreach ($types as $type) { - foreach ($this->getWriters($type) as $writerConfiguration) { - [$writer, $writerMethod] = $writerConfiguration; - $writer = $writer instanceof EmailWriterInterface ? $writer : $this->serviceLocator->get($writer); - \call_user_func([$writer, $writerMethod], $message, $envelope); - } - } - - if ($message instanceof TemplatedEmail) { - if ($template = $message->getHtmlTemplate()) { - $headers->add(new UnstructuredHeader('X-DrawEmail-HtmlTemplate', $template)); - } - if ($template = $message->getTextTemplate()) { - $headers->add(new UnstructuredHeader('X-DrawEmail-TextTemplate', $template)); - } - } - } - private function getTypes(RawMessage $message): array { return [$message::class] diff --git a/packages/mailer/EventListener/EmailComposerListener.php b/packages/mailer/EventListener/EmailComposerListener.php new file mode 100644 index 00000000..6605006d --- /dev/null +++ b/packages/mailer/EventListener/EmailComposerListener.php @@ -0,0 +1,38 @@ +isQueued()) { + return; + } + + $message = $event->getMessage(); + if (!$message instanceof Message) { + return; + } + + $headers = $message->getHeaders(); + if ($headers->has('X-DrawEmail')) { + return; + } + + $headers->add(new UnstructuredHeader('X-DrawEmail', '1')); + + $this->emailComposer->compose($message, $event->getEnvelope()); + } +} diff --git a/packages/mailer/Tests/EventListener/EmailWriterListenerTest.php b/packages/mailer/Tests/EmailComposerTest.php similarity index 63% rename from packages/mailer/Tests/EventListener/EmailWriterListenerTest.php rename to packages/mailer/Tests/EmailComposerTest.php index c8986641..bc1f261e 100644 --- a/packages/mailer/Tests/EventListener/EmailWriterListenerTest.php +++ b/packages/mailer/Tests/EmailComposerTest.php @@ -1,59 +1,36 @@ object = new EmailWriterListener( + $this->object = new EmailComposer( $this->serviceLocator = $this->createMock(ContainerInterface::class) ); } - public function testConstruct(): void - { - static::assertInstanceOf( - EventSubscriberInterface::class, - $this->object - ); - } - - public function testGetSubscribedEvents(): void - { - static::assertSame( - [ - MessageEvent::class => ['composeMessage', 200], - ], - $this->object::getSubscribedEvents() - ); - } - public function testWriterMutator(): void { static::assertSame([], $this->object->getWriters(\stdClass::class)); @@ -83,42 +60,11 @@ public function testWriterMutator(): void ); } - public function testComposeMessageNotMessage(): void - { - $this->serviceLocator - ->expects(static::never()) - ->method('get'); - - $this->object->composeMessage( - $this->createMessageEvent( - $this->createMock(RawMessage::class) - ) - ); - } - - public function testComposeMessageComposed(): void - { - $this->serviceLocator - ->expects(static::never()) - ->method('get'); - - $message = $this->createMock(Message::class); - - $message - ->expects(static::once()) - ->method('getHeaders') - ->willReturn($headers = new Headers()); - - $headers->add(new UnstructuredHeader('X-DrawEmail', '1')); - - $this->object->composeMessage($this->createMessageEvent($message)); - } - public function testComposeMessage(): void { $message = new TemplatedEmail(); - $event = $this->createMessageEvent($message); + $envelope = new Envelope(new Address('test@example.com'), [new Address('test@example.com')]); $this->object->addWriter(Message::class, $writer1 = uniqid('writer-1-'), 'method1'); $this->object->addWriter(Email::class, $writer2 = uniqid('writer-2-'), 'method2'); @@ -142,7 +88,7 @@ public function testComposeMessage(): void ->method('method1') ->with( $message, - $event->getEnvelope() + $envelope ); $emailWriter @@ -159,14 +105,13 @@ function (TemplatedEmail $templatedEmail) use ($message): bool { return true; }, ), - $event->getEnvelope() + $envelope ); - $this->object->composeMessage($event); + $this->object->compose($message, $envelope); $headers = $message->getHeaders(); - static::assertTrue($headers->has('X-DrawEmail')); static::assertSame( 'html-template', $headers->get('X-DrawEmail-HtmlTemplate')->getBodyAsString() @@ -177,21 +122,6 @@ function (TemplatedEmail $templatedEmail) use ($message): bool { ); } - public function testComposeMessageQueued(): void - { - $this->serviceLocator - ->expects(static::never()) - ->method('get'); - - $message = $this->createMock(Message::class); - - $message - ->expects(static::never()) - ->method('getHeaders'); - - $this->object->composeMessage($this->createMessageEvent($message, true)); - } - public function testRegisterEmailWriter(): void { $message = $this->createMock(Email::class); @@ -201,7 +131,7 @@ public function testRegisterEmailWriter(): void ->method('getHeaders') ->willReturn(new Headers()); - $event = $this->createMessageEvent($message); + $envelope = new Envelope(new Address('test@example.com'), [new Address('test@example.com')]); $emailWriter = new class() implements EmailWriterInterface { public int $compose1CallCounter = 0; @@ -247,19 +177,9 @@ public function compose2(Message $email): void ->expects(static::never()) ->method('get'); - $this->object->composeMessage($event); + $this->object->compose($message, $envelope); static::assertSame(1, $emailWriter->compose1CallCounter); static::assertSame(1, $emailWriter->compose2CallCounter); } - - private function createMessageEvent(RawMessage $message, bool $queue = false): MessageEvent - { - return new MessageEvent( - $message, - new Envelope(new Address('test@example.com'), [new Address('test@example.com')]), - uniqid('transport-'), - $queue - ); - } } diff --git a/packages/mailer/Tests/EventListener/EmailComposerListenerTest.php b/packages/mailer/Tests/EventListener/EmailComposerListenerTest.php new file mode 100644 index 00000000..a482132f --- /dev/null +++ b/packages/mailer/Tests/EventListener/EmailComposerListenerTest.php @@ -0,0 +1,104 @@ +object = new EmailComposerListener( + $this->emailComposer = $this->createMock(EmailComposer::class) + ); + } + + public function testComposeMessageNotMessage(): void + { + $this->emailComposer + ->expects(static::never()) + ->method('compose'); + + $this->object->composeMessage( + $this->createMessageEvent( + $this->createMock(RawMessage::class) + ) + ); + } + + public function testComposeMessageComposed(): void + { + $this->emailComposer + ->expects(static::never()) + ->method('compose'); + + $message = $this->createMock(Message::class); + + $message + ->expects(static::once()) + ->method('getHeaders') + ->willReturn($headers = new Headers()); + + $headers->add(new UnstructuredHeader('X-DrawEmail', '1')); + + $this->object->composeMessage($this->createMessageEvent($message)); + } + + public function testComposeMessage(): void + { + $message = new TemplatedEmail(); + + $event = $this->createMessageEvent($message); + + $this->emailComposer + ->expects(static::once()) + ->method('compose'); + + $this->object->composeMessage($event); + + $headers = $message->getHeaders(); + + static::assertTrue($headers->has('X-DrawEmail')); + } + + public function testComposeMessageQueued(): void + { + $this->emailComposer + ->expects(static::never()) + ->method('compose'); + + $message = $this->createMock(Message::class); + + $this->object->composeMessage($this->createMessageEvent($message, true)); + } + + private function createMessageEvent(RawMessage $message, bool $queue = false): MessageEvent + { + return new MessageEvent( + $message, + new Envelope(new Address('test@example.com'), [new Address('test@example.com')]), + uniqid('transport-'), + $queue + ); + } +} diff --git a/tests/Mailer/EventListener/EmailComposerListenerTest.php b/tests/Mailer/EventListener/EmailComposerListenerTest.php new file mode 100644 index 00000000..eb71c715 --- /dev/null +++ b/tests/Mailer/EventListener/EmailComposerListenerTest.php @@ -0,0 +1,30 @@ +object = static::getContainer()->get(EmailComposerListener::class); + } + + public function testEventListenersRegistered(): void + { + static::assertEventListenersRegistered( + $this->object::class, + [ + MessageEvent::class => ['composeMessage'], + ] + ); + } +} From 0c0ac10abffae7139815d4cb56c63cb0c7c3852e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Poirier=20Th=C3=A9or=C3=AAt?= Date: Mon, 18 Dec 2023 12:01:18 -0500 Subject: [PATCH 3/5] [Mailer] Split responsiblity of email template header in writer --- .../Integration/MailerIntegrationTest.php | 7 +++ packages/mailer/EmailComposer.php | 13 ----- .../AddTemplateHeaderEmailWriter.php | 27 +++++++++ packages/mailer/Tests/EmailComposerTest.php | 28 +--------- .../AddTemplateHeaderEmailWriterTest.php | 56 +++++++++++++++++++ 5 files changed, 91 insertions(+), 40 deletions(-) create mode 100644 packages/mailer/EmailWriter/AddTemplateHeaderEmailWriter.php create mode 100644 packages/mailer/Tests/EmailWriter/AddTemplateHeaderEmailWriterTest.php diff --git a/packages/framework-extra-bundle/Tests/DependencyInjection/Integration/MailerIntegrationTest.php b/packages/framework-extra-bundle/Tests/DependencyInjection/Integration/MailerIntegrationTest.php index 37f75d8b..bdcb6e73 100644 --- a/packages/framework-extra-bundle/Tests/DependencyInjection/Integration/MailerIntegrationTest.php +++ b/packages/framework-extra-bundle/Tests/DependencyInjection/Integration/MailerIntegrationTest.php @@ -6,6 +6,7 @@ use Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Integration\MailerIntegration; use Draw\Component\Mailer\Command\SendTestEmailCommand; use Draw\Component\Mailer\EmailComposer; +use Draw\Component\Mailer\EmailWriter\AddTemplateHeaderEmailWriter; use Draw\Component\Mailer\EmailWriter\DefaultFromEmailWriter; use Draw\Component\Mailer\EmailWriter\EmailWriterInterface; use Draw\Component\Mailer\EventListener\EmailComposerListener; @@ -115,6 +116,12 @@ public static function provideTestLoad(): iterable EmailComposer::class, ] ), + new ServiceConfiguration( + 'draw.mailer.email_writer.add_template_header_email_writer', + [ + AddTemplateHeaderEmailWriter::class, + ] + ), new ServiceConfiguration( 'draw.mailer.event_listener.email_composer_listener', [ diff --git a/packages/mailer/EmailComposer.php b/packages/mailer/EmailComposer.php index 50c48bc3..618c5557 100644 --- a/packages/mailer/EmailComposer.php +++ b/packages/mailer/EmailComposer.php @@ -5,9 +5,7 @@ use Draw\Component\Core\Reflection\ReflectionAccessor; use Draw\Component\Mailer\EmailWriter\EmailWriterInterface; use Psr\Container\ContainerInterface; -use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Component\Mailer\Envelope; -use Symfony\Component\Mime\Header\UnstructuredHeader; use Symfony\Component\Mime\Message; use Symfony\Component\Mime\RawMessage; @@ -23,8 +21,6 @@ public function __construct(private ContainerInterface $serviceLocator) public function compose(Message $message, Envelope $envelope): void { - $headers = $message->getHeaders(); - foreach ($this->getTypes($message) as $type) { foreach ($this->getWriters($type) as $writerConfiguration) { [$writer, $writerMethod] = $writerConfiguration; @@ -32,15 +28,6 @@ public function compose(Message $message, Envelope $envelope): void \call_user_func([$writer, $writerMethod], $message, $envelope); } } - - if ($message instanceof TemplatedEmail) { - if ($template = $message->getHtmlTemplate()) { - $headers->add(new UnstructuredHeader('X-DrawEmail-HtmlTemplate', $template)); - } - if ($template = $message->getTextTemplate()) { - $headers->add(new UnstructuredHeader('X-DrawEmail-TextTemplate', $template)); - } - } } public function registerEmailWriter(EmailWriterInterface $emailWriter): void diff --git a/packages/mailer/EmailWriter/AddTemplateHeaderEmailWriter.php b/packages/mailer/EmailWriter/AddTemplateHeaderEmailWriter.php new file mode 100644 index 00000000..5b1cf524 --- /dev/null +++ b/packages/mailer/EmailWriter/AddTemplateHeaderEmailWriter.php @@ -0,0 +1,27 @@ + -255, + ]; + } + + public function addHeader(TemplatedEmail $message): void + { + $headers = $message->getHeaders(); + if ($template = $message->getHtmlTemplate()) { + $headers->add(new UnstructuredHeader('X-DrawEmail-HtmlTemplate', $template)); + } + if ($template = $message->getTextTemplate()) { + $headers->add(new UnstructuredHeader('X-DrawEmail-TextTemplate', $template)); + } + } +} diff --git a/packages/mailer/Tests/EmailComposerTest.php b/packages/mailer/Tests/EmailComposerTest.php index bc1f261e..d5ddca03 100644 --- a/packages/mailer/Tests/EmailComposerTest.php +++ b/packages/mailer/Tests/EmailComposerTest.php @@ -13,7 +13,6 @@ use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; -use Symfony\Component\Mime\Header\Headers; use Symfony\Component\Mime\Message; #[CoversClass(EmailComposer::class)] @@ -95,42 +94,17 @@ public function testComposeMessage(): void ->expects(static::once()) ->method('method2') ->with( - static::callback( - function (TemplatedEmail $templatedEmail) use ($message): bool { - static::assertSame($message, $templatedEmail); - - $templatedEmail->htmlTemplate('html-template'); - $templatedEmail->textTemplate('text-template'); - - return true; - }, - ), + $message, $envelope ); $this->object->compose($message, $envelope); - - $headers = $message->getHeaders(); - - static::assertSame( - 'html-template', - $headers->get('X-DrawEmail-HtmlTemplate')->getBodyAsString() - ); - static::assertSame( - 'text-template', - $headers->get('X-DrawEmail-TextTemplate')->getBodyAsString() - ); } public function testRegisterEmailWriter(): void { $message = $this->createMock(Email::class); - $message - ->expects(static::once()) - ->method('getHeaders') - ->willReturn(new Headers()); - $envelope = new Envelope(new Address('test@example.com'), [new Address('test@example.com')]); $emailWriter = new class() implements EmailWriterInterface { diff --git a/packages/mailer/Tests/EmailWriter/AddTemplateHeaderEmailWriterTest.php b/packages/mailer/Tests/EmailWriter/AddTemplateHeaderEmailWriterTest.php new file mode 100644 index 00000000..bff7df32 --- /dev/null +++ b/packages/mailer/Tests/EmailWriter/AddTemplateHeaderEmailWriterTest.php @@ -0,0 +1,56 @@ +object = new AddTemplateHeaderEmailWriter(); + } + + public function testConstruct(): void + { + static::assertInstanceOf( + EmailWriterInterface::class, + $this->object + ); + } + + public function testGetForEmails(): void + { + static::assertSame( + ['addHeader' => -255], + $this->object::getForEmails() + ); + } + + public function testAddHeader(): void + { + $message = new TemplatedEmail(); + + $message->htmlTemplate('html-template'); + $message->textTemplate('text-template'); + + $this->object->addHeader($message); + + static::assertSame( + 'html-template', + $message->getHeaders()->get('X-DrawEmail-HtmlTemplate')->getBodyAsString() + ); + + static::assertSame( + 'text-template', + $message->getHeaders()->get('X-DrawEmail-TextTemplate')->getBodyAsString() + ); + } +} From 89199ad3ff8441a103bac97d4a9941a4f37c62f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Poirier=20Th=C3=A9or=C3=AAt?= Date: Mon, 18 Dec 2023 12:21:24 -0500 Subject: [PATCH 4/5] [Mailer] Support for localise email --- .../Integration/MailerIntegration.php | 10 +++++ .../Integration/MailerIntegrationTest.php | 7 ++++ .../BodyRenderer/LocalizeBodyRenderer.php | 40 +++++++++++++++++++ .../mailer/Email/LocalizeEmailInterface.php | 8 ++++ packages/mailer/EmailComposer.php | 37 +++++++++++++---- packages/mailer/Tests/EmailComposerTest.php | 38 +++++++++++++++++- 6 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 packages/mailer/BodyRenderer/LocalizeBodyRenderer.php create mode 100644 packages/mailer/Email/LocalizeEmailInterface.php diff --git a/packages/framework-extra-bundle/DependencyInjection/Integration/MailerIntegration.php b/packages/framework-extra-bundle/DependencyInjection/Integration/MailerIntegration.php index 711e6de9..5ef3d9a9 100644 --- a/packages/framework-extra-bundle/DependencyInjection/Integration/MailerIntegration.php +++ b/packages/framework-extra-bundle/DependencyInjection/Integration/MailerIntegration.php @@ -2,6 +2,7 @@ namespace Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Integration; +use Draw\Component\Mailer\BodyRenderer\LocalizeBodyRenderer; use Draw\Component\Mailer\EmailComposer; use Draw\Component\Mailer\EmailWriter\DefaultFromEmailWriter; use Draw\Component\Mailer\EmailWriter\EmailWriterInterface; @@ -12,6 +13,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; +use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Mime\Address; class MailerIntegration implements IntegrationInterface, PrependIntegrationInterface @@ -33,6 +35,14 @@ public function load(array $config, PhpFileLoader $loader, ContainerBuilder $con ), ); + $container + ->getDefinition(LocalizeBodyRenderer::class) + ->setDecoratedService( + 'twig.mime_body_renderer', + 'draw.mailer.body_renderer.localize_body_renderer.inner' + ) + ->setArgument('$bodyRenderer', new Reference('draw.mailer.body_renderer.localize_body_renderer.inner')); + $container ->registerForAutoconfiguration(EmailWriterInterface::class) ->addTag(EmailWriterInterface::class); diff --git a/packages/framework-extra-bundle/Tests/DependencyInjection/Integration/MailerIntegrationTest.php b/packages/framework-extra-bundle/Tests/DependencyInjection/Integration/MailerIntegrationTest.php index bdcb6e73..5c590d07 100644 --- a/packages/framework-extra-bundle/Tests/DependencyInjection/Integration/MailerIntegrationTest.php +++ b/packages/framework-extra-bundle/Tests/DependencyInjection/Integration/MailerIntegrationTest.php @@ -4,6 +4,7 @@ use Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Integration\IntegrationInterface; use Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Integration\MailerIntegration; +use Draw\Component\Mailer\BodyRenderer\LocalizeBodyRenderer; use Draw\Component\Mailer\Command\SendTestEmailCommand; use Draw\Component\Mailer\EmailComposer; use Draw\Component\Mailer\EmailWriter\AddTemplateHeaderEmailWriter; @@ -116,6 +117,12 @@ public static function provideTestLoad(): iterable EmailComposer::class, ] ), + new ServiceConfiguration( + 'draw.mailer.body_renderer.localize_body_renderer', + [ + LocalizeBodyRenderer::class, + ] + ), new ServiceConfiguration( 'draw.mailer.email_writer.add_template_header_email_writer', [ diff --git a/packages/mailer/BodyRenderer/LocalizeBodyRenderer.php b/packages/mailer/BodyRenderer/LocalizeBodyRenderer.php new file mode 100644 index 00000000..78f6d025 --- /dev/null +++ b/packages/mailer/BodyRenderer/LocalizeBodyRenderer.php @@ -0,0 +1,40 @@ +translator = $translator; + } + } + + public function render(Message $message): void + { + $currentLocale = null; + if ($this->translator && $message instanceof LocalizeEmailInterface && $message->getLocale()) { + $currentLocale = $this->translator->getLocale(); + $this->translator->setLocale($message->getLocale()); + } + + try { + $this->bodyRenderer->render($message); + } finally { + if ($currentLocale) { + $this->translator?->setLocale($currentLocale); + } + } + } +} diff --git a/packages/mailer/Email/LocalizeEmailInterface.php b/packages/mailer/Email/LocalizeEmailInterface.php new file mode 100644 index 00000000..031acdff --- /dev/null +++ b/packages/mailer/Email/LocalizeEmailInterface.php @@ -0,0 +1,8 @@ +translator = $translator; + } } public function compose(Message $message, Envelope $envelope): void { - foreach ($this->getTypes($message) as $type) { - foreach ($this->getWriters($type) as $writerConfiguration) { - [$writer, $writerMethod] = $writerConfiguration; - $writer = $writer instanceof EmailWriterInterface ? $writer : $this->serviceLocator->get($writer); - \call_user_func([$writer, $writerMethod], $message, $envelope); + $currentLocale = null; + + if ($this->translator && $message instanceof LocalizeEmailInterface && $message->getLocale()) { + $currentLocale = $this->translator->getLocale(); + $this->translator->setLocale($message->getLocale()); + } + + try { + foreach ($this->getTypes($message) as $type) { + foreach ($this->getWriters($type) as $writerConfiguration) { + [$writer, $writerMethod] = $writerConfiguration; + $writer = $writer instanceof EmailWriterInterface ? $writer : $this->serviceLocator->get($writer); + \call_user_func([$writer, $writerMethod], $message, $envelope); + } + } + } finally { + if ($currentLocale) { + $this->translator?->setLocale($currentLocale); } } } diff --git a/packages/mailer/Tests/EmailComposerTest.php b/packages/mailer/Tests/EmailComposerTest.php index d5ddca03..93af4bd5 100644 --- a/packages/mailer/Tests/EmailComposerTest.php +++ b/packages/mailer/Tests/EmailComposerTest.php @@ -2,6 +2,7 @@ namespace Draw\Component\Mailer\Tests; +use Draw\Component\Mailer\Email\LocalizeEmailInterface; use Draw\Component\Mailer\EmailComposer; use Draw\Component\Mailer\EmailWriter\EmailWriterInterface; use Draw\Component\Tester\MockTrait; @@ -14,19 +15,24 @@ use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; use Symfony\Component\Mime\Message; +use Symfony\Component\Translation\Translator; #[CoversClass(EmailComposer::class)] class EmailComposerTest extends TestCase { use MockTrait; + private EmailComposer $object; private ContainerInterface&MockObject $serviceLocator; + private Translator&MockObject $translator; + protected function setUp(): void { $this->object = new EmailComposer( - $this->serviceLocator = $this->createMock(ContainerInterface::class) + $this->serviceLocator = $this->createMock(ContainerInterface::class), + $this->translator = $this->createMock(Translator::class) ); } @@ -156,4 +162,34 @@ public function compose2(Message $email): void static::assertSame(1, $emailWriter->compose1CallCounter); static::assertSame(1, $emailWriter->compose2CallCounter); } + + public function testComposeLocalizeEmail(): void + { + $message = new class() extends Email implements LocalizeEmailInterface { + public function getLocale(): ?string + { + return 'fr'; + } + }; + + $this->translator + ->expects(static::once()) + ->method('getLocale') + ->willReturn('en'); + + $this->translator + ->expects(static::exactly(2)) + ->method('setLocale') + ->with( + ...static::withConsecutive( + ['fr'], + ['en'] + ) + ); + + $this->object->compose( + $message, + new Envelope(new Address('test@example.com'), [new Address('test@example.com')]) + ); + } } From 207dcce03b39b9a7a33ba52dec6083a1fcb69a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Poirier=20Th=C3=A9or=C3=AAt?= Date: Mon, 18 Dec 2023 13:37:42 -0500 Subject: [PATCH 5/5] [UserBundle] email sent in proper locale --- app/migrations/Version20231218175905.php | 31 +++++++++++++++++++ app/src/DataFixtures/AppFixtures.php | 1 + app/src/Entity/User.php | 19 ++++++++++-- config/serializer/Entity/User.yaml | 2 ++ packages/mailer/Email/CallToActionEmail.php | 4 ++- packages/mailer/Email/LocalizeEmailTrait.php | 20 ++++++++++++ .../Recipient/LocalizationAwareInterface.php | 8 +++++ .../User/Controller/LoginController.php | 5 ++- .../user-bundle/Email/ToUserEmailTrait.php | 17 ++++++++++ .../NewUserSendEmailMessageHandler.php | 15 ++++++--- ...ChangeRequestedSendEmailMessageHandler.php | 11 ++++++- .../Resources/translations/DrawEmail.en.yaml | 2 +- .../Resources/translations/DrawEmail.fr.yaml | 9 ++++++ 13 files changed, 133 insertions(+), 11 deletions(-) create mode 100644 app/migrations/Version20231218175905.php create mode 100644 packages/mailer/Email/LocalizeEmailTrait.php create mode 100644 packages/mailer/Recipient/LocalizationAwareInterface.php diff --git a/app/migrations/Version20231218175905.php b/app/migrations/Version20231218175905.php new file mode 100644 index 00000000..f05dcb8c --- /dev/null +++ b/app/migrations/Version20231218175905.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE draw_acme__user ADD preferred_locale VARCHAR(255) DEFAULT \'en\' NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE draw_acme__user DROP preferred_locale'); + } +} diff --git a/app/src/DataFixtures/AppFixtures.php b/app/src/DataFixtures/AppFixtures.php index 65994ffd..cffaaeed 100644 --- a/app/src/DataFixtures/AppFixtures.php +++ b/app/src/DataFixtures/AppFixtures.php @@ -78,6 +78,7 @@ public function load(ObjectManager $manager): void $user->setPlainPassword('password'); if (1 === $number) { $user->setTags([$inactiveTag]); + $user->setPreferredLocale('fr'); } $manager->persist($user); } diff --git a/app/src/Entity/User.php b/app/src/Entity/User.php index e5eb9d38..62a8f140 100644 --- a/app/src/Entity/User.php +++ b/app/src/Entity/User.php @@ -21,6 +21,7 @@ use Draw\Bundle\UserBundle\Security\TwoFactorAuthentication\Entity\ConfigurationTrait; use Draw\Bundle\UserBundle\Security\TwoFactorAuthentication\Entity\TwoFactorAuthenticationUserInterface; use Draw\Component\EntityMigrator\MigrationTargetEntityInterface; +use Draw\Component\Mailer\Recipient\LocalizationAwareInterface; use Draw\Component\Messenger\DoctrineMessageBusHook\Entity\MessageHolderInterface; use Draw\Component\Messenger\DoctrineMessageBusHook\Entity\MessageHolderTrait; use Draw\DoctrineExtra\Common\Collections\CollectionUtil; @@ -34,7 +35,7 @@ #[ORM\Table(name: 'draw_acme__user')] #[ORM\HasLifecycleCallbacks] #[UniqueEntity(fields: ['email'])] -class User implements MessageHolderInterface, SecurityUserInterface, TwoFactorAuthenticationUserInterface, PasswordChangeUserInterface, LockableUserInterface, TwoFactorInterface, ByEmailInterface, ByTimeBaseOneTimePasswordInterface, MigrationTargetEntityInterface +class User implements MessageHolderInterface, SecurityUserInterface, TwoFactorAuthenticationUserInterface, PasswordChangeUserInterface, LockableUserInterface, TwoFactorInterface, ByEmailInterface, ByTimeBaseOneTimePasswordInterface, MigrationTargetEntityInterface, LocalizationAwareInterface { use ByEmailTrait; use ByTimeBaseOneTimePasswordTrait; @@ -148,9 +149,11 @@ class User implements MessageHolderInterface, SecurityUserInterface, TwoFactorAu private Collection $userTags; #[Assert\NotNull] - #[Serializer\ReadOnlyProperty] private string $requiredReadOnly = 'value'; + #[ORM\Column(type: 'string', nullable: false, options: ['default' => 'en'])] + private string $preferredLocale = 'en'; + public function __construct() { $this->address = new Address(); @@ -176,6 +179,18 @@ public function setId(string $id): void $this->id = $id; } + public function getPreferredLocale(): string + { + return $this->preferredLocale; + } + + public function setPreferredLocale(string $preferredLocale): static + { + $this->preferredLocale = $preferredLocale; + + return $this; + } + /** * @see UserInterface */ diff --git a/config/serializer/Entity/User.yaml b/config/serializer/Entity/User.yaml index 5d6edee3..627b10b2 100644 --- a/config/serializer/Entity/User.yaml +++ b/config/serializer/Entity/User.yaml @@ -49,5 +49,7 @@ App\Entity\User: exclude: true emailAuthCodeGeneratedAt: exclude: true + preferredLocale: + exclude: true twoFactorAuthenticationEnabledProviders: exclude: true diff --git a/packages/mailer/Email/CallToActionEmail.php b/packages/mailer/Email/CallToActionEmail.php index 16ef3a9a..f4406945 100644 --- a/packages/mailer/Email/CallToActionEmail.php +++ b/packages/mailer/Email/CallToActionEmail.php @@ -4,8 +4,10 @@ use Symfony\Bridge\Twig\Mime\TemplatedEmail; -class CallToActionEmail extends TemplatedEmail +class CallToActionEmail extends TemplatedEmail implements LocalizeEmailInterface { + use LocalizeEmailTrait; + private ?string $callToActionLink = null; public array $translationTokens = []; diff --git a/packages/mailer/Email/LocalizeEmailTrait.php b/packages/mailer/Email/LocalizeEmailTrait.php new file mode 100644 index 00000000..5331da80 --- /dev/null +++ b/packages/mailer/Email/LocalizeEmailTrait.php @@ -0,0 +1,20 @@ +locale = $locale; + + return $this; + } + + public function getLocale(): ?string + { + return $this->locale; + } +} diff --git a/packages/mailer/Recipient/LocalizationAwareInterface.php b/packages/mailer/Recipient/LocalizationAwareInterface.php new file mode 100644 index 00000000..a2b69964 --- /dev/null +++ b/packages/mailer/Recipient/LocalizationAwareInterface.php @@ -0,0 +1,8 @@ +handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $mailer->send(new ForgotPasswordEmail($form->get('email')->getData())); + $mailer->send( + (new ForgotPasswordEmail($form->get('email')->getData())) + ->setLocale($request->getLocale()) + ); return new RedirectResponse($this->generateUrl('admin_check_email')); } diff --git a/packages/user-bundle/Email/ToUserEmailTrait.php b/packages/user-bundle/Email/ToUserEmailTrait.php index aab30815..a499d724 100644 --- a/packages/user-bundle/Email/ToUserEmailTrait.php +++ b/packages/user-bundle/Email/ToUserEmailTrait.php @@ -2,10 +2,27 @@ namespace Draw\Bundle\UserBundle\Email; +use Draw\Bundle\UserBundle\Entity\SecurityUserInterface; +use Draw\Component\Mailer\Email\LocalizeEmailTrait; +use Draw\Component\Mailer\Recipient\LocalizationAwareInterface; + trait ToUserEmailTrait { + use LocalizeEmailTrait; + private string|int|null $userId = null; + public function toUser(SecurityUserInterface $user): self + { + $this->userId = $user->getId(); + + if ($user instanceof LocalizationAwareInterface) { + $this->setLocale($user->getPreferredLocale()); + } + + return $this; + } + public function setUserId(string|int $userId): self { $this->userId = $userId; diff --git a/packages/user-bundle/MessageHandler/NewUserSendEmailMessageHandler.php b/packages/user-bundle/MessageHandler/NewUserSendEmailMessageHandler.php index 2ed125b8..6a00d1d9 100644 --- a/packages/user-bundle/MessageHandler/NewUserSendEmailMessageHandler.php +++ b/packages/user-bundle/MessageHandler/NewUserSendEmailMessageHandler.php @@ -5,6 +5,7 @@ use Doctrine\ORM\EntityRepository; use Draw\Bundle\UserBundle\Email\UserOnboardingEmail; use Draw\Bundle\UserBundle\Message\NewUserMessage; +use Draw\Component\Mailer\Recipient\LocalizationAwareInterface; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -29,14 +30,18 @@ public function __invoke(NewUserMessage $message): void { $user = $this->drawUserEntityRepository->find($message->getUserId()); - if (null === $user) { - return; - } - if (!method_exists($user, 'getEmail') || empty($user->getEmail())) { return; } - $this->mailer->send((new UserOnboardingEmail())->setUserId($message->getUserId())); + $this->mailer->send( + (new UserOnboardingEmail()) + ->setUserId($message->getUserId()) + ->setLocale( + $user instanceof LocalizationAwareInterface ? + $user->getPreferredLocale() : + null + ) + ); } } diff --git a/packages/user-bundle/MessageHandler/PasswordChangeRequestedSendEmailMessageHandler.php b/packages/user-bundle/MessageHandler/PasswordChangeRequestedSendEmailMessageHandler.php index 80844a21..a75a5d5f 100644 --- a/packages/user-bundle/MessageHandler/PasswordChangeRequestedSendEmailMessageHandler.php +++ b/packages/user-bundle/MessageHandler/PasswordChangeRequestedSendEmailMessageHandler.php @@ -6,6 +6,7 @@ use Draw\Bundle\UserBundle\Email\PasswordChangeRequestedEmail; use Draw\Bundle\UserBundle\Entity\PasswordChangeUserInterface; use Draw\Bundle\UserBundle\Message\PasswordChangeRequestedMessage; +use Draw\Component\Mailer\Recipient\LocalizationAwareInterface; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -38,6 +39,14 @@ public function __invoke(PasswordChangeRequestedMessage $message): void return; } - $this->mailer->send((new PasswordChangeRequestedEmail())->setUserId($user->getId())); + $this->mailer->send( + (new PasswordChangeRequestedEmail()) + ->setUserId($user->getId()) + ->setLocale( + $user instanceof LocalizationAwareInterface + ? $user->getPreferredLocale() + : null + ) + ); } } diff --git a/packages/user-bundle/Resources/translations/DrawEmail.en.yaml b/packages/user-bundle/Resources/translations/DrawEmail.en.yaml index 1bd013b5..e1d656c3 100644 --- a/packages/user-bundle/Resources/translations/DrawEmail.en.yaml +++ b/packages/user-bundle/Resources/translations/DrawEmail.en.yaml @@ -32,7 +32,7 @@ email: subject: 'Welcome' top_section: |

Your account have been created.

-

Please click on the button below to finally your account creation.

+

Please click on the button below to finalise your account creation.

call_to_action_text: 'Finalise my account creation' bottom_section: |

Follow the instruction on the site to finalise your account creation.

diff --git a/packages/user-bundle/Resources/translations/DrawEmail.fr.yaml b/packages/user-bundle/Resources/translations/DrawEmail.fr.yaml index 9a9dfe88..84c366be 100644 --- a/packages/user-bundle/Resources/translations/DrawEmail.fr.yaml +++ b/packages/user-bundle/Resources/translations/DrawEmail.fr.yaml @@ -20,3 +20,12 @@ email: bottom_section: |

Si vous n'êtes pas l'auteur de cette demande, peut-être que quelqu'un à fait une erreur.

Peut-être seriez vous intéressé par notre site web ? Cliquer sur le lien plus haut pour en savoir plus.

+ + user_onboarding: + subject: 'Bienvenue' + top_section: | +

Votre compte a été créé.

+

Veuillez cliquer sur le bouton ci-dessous pour finaliser la création de votre compte.

+ call_to_action_text: 'Finaliser la création de mon compte' + bottom_section: | +

Suivez les instructions sur le site pour finaliser la création de votre compte.