From 97eb49be33232745a793084dcef8f0ef4674e8e6 Mon Sep 17 00:00:00 2001 From: Tomas Date: Thu, 13 Mar 2025 10:19:53 +0200 Subject: [PATCH] [Mailer][TwigBridge] Add support for translatable subject --- src/Symfony/Bridge/Twig/CHANGELOG.md | 1 + .../Bridge/Twig/Mime/TemplatedEmail.php | 21 +++++++++++++++- .../Twig/Tests/Mime/TemplatedEmailTest.php | 17 +++++++++++++ .../TwigBundle/Resources/config/mailer.php | 6 ++++- src/Symfony/Component/Mailer/CHANGELOG.md | 1 + .../Mailer/EventListener/MessageListener.php | 16 ++++++++++++- .../EventListener/MessageListenerTest.php | 24 +++++++++++++++++++ src/Symfony/Component/Mailer/composer.json | 3 ++- src/Symfony/Component/Mime/Email.php | 4 ++-- 9 files changed, 87 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index 9695fe09f0549..c34616f3892bb 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Add `is_granted_for_user()` Twig function * Add `field_id()` Twig form helper function + * Add `TemplatedEmail::getTranslatableSubject()` method 7.2 --- diff --git a/src/Symfony/Bridge/Twig/Mime/TemplatedEmail.php b/src/Symfony/Bridge/Twig/Mime/TemplatedEmail.php index 2d308947f8498..b3b891e400bc0 100644 --- a/src/Symfony/Bridge/Twig/Mime/TemplatedEmail.php +++ b/src/Symfony/Bridge/Twig/Mime/TemplatedEmail.php @@ -18,11 +18,29 @@ */ class TemplatedEmail extends Email { + private string|\Stringable|null $subject = null; private ?string $htmlTemplate = null; private ?string $textTemplate = null; private ?string $locale = null; private array $context = []; + /** + * @return $this + */ + public function subject(string|\Stringable $subject): static + { + parent::subject($subject); + + $this->subject = $subject; + + return $this; + } + + public function getTranslatableSubject(): string|\Stringable|null + { + return $this->subject; + } + /** * @return $this */ @@ -100,7 +118,7 @@ public function markAsRendered(): void */ public function __serialize(): array { - return [$this->htmlTemplate, $this->textTemplate, $this->context, parent::__serialize(), $this->locale]; + return [$this->htmlTemplate, $this->textTemplate, $this->context, parent::__serialize(), $this->locale, $this->subject]; } /** @@ -110,6 +128,7 @@ public function __unserialize(array $data): void { [$this->htmlTemplate, $this->textTemplate, $this->context, $parentData] = $data; $this->locale = $data[4] ?? null; + $this->subject = $data[5] ?? null; parent::__unserialize($parentData); } diff --git a/src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php b/src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php index f77b3ad4b5337..f36fed899d16f 100644 --- a/src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php @@ -20,7 +20,10 @@ use Symfony\Component\Serializer\Normalizer\MimeMessageNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; +use Symfony\Component\Serializer\Normalizer\TranslatableNormalizer; use Symfony\Component\Serializer\Serializer; +use Symfony\Component\Translation\IdentityTranslator; +use Symfony\Component\Translation\TranslatableMessage; class TemplatedEmailTest extends TestCase { @@ -44,6 +47,7 @@ public function testSerialize() ->htmlTemplate('text.html.twig') ->context($context = ['a' => 'b']) ->locale($locale = 'fr_FR') + ->subject($subject = new TranslatableMessage('hello {{ name }}', ['name' => 'John'], 'greetings')) ; $email = unserialize(serialize($email)); @@ -51,12 +55,14 @@ public function testSerialize() $this->assertEquals('text.html.twig', $email->getHtmlTemplate()); $this->assertEquals($context, $email->getContext()); $this->assertEquals($locale, $email->getLocale()); + $this->assertEquals($subject, $email->getTranslatableSubject()); } public function testSymfonySerialize() { // we don't add from/sender to check that validation is not triggered to serialize an email $e = new TemplatedEmail(); + $e->subject(new TranslatableMessage('hello.world')); $e->to('you@example.com'); $e->textTemplate('email.txt.twig'); $e->htmlTemplate('email.html.twig'); @@ -67,6 +73,7 @@ public function testSymfonySerialize() $expectedJson = <<services() ->set('twig.mailer.message_listener', MessageListener::class) - ->args([null, service('twig.mime_body_renderer')]) + ->args([ + null, + service('twig.mime_body_renderer'), + '$translator' => service('translator')->ignoreOnInvalid(), + ]) ->tag('kernel.event_subscriber') ->set('twig.mime_body_renderer', BodyRenderer::class) diff --git a/src/Symfony/Component/Mailer/CHANGELOG.md b/src/Symfony/Component/Mailer/CHANGELOG.md index 3816cc474948b..ada28a2a8bc78 100644 --- a/src/Symfony/Component/Mailer/CHANGELOG.md +++ b/src/Symfony/Component/Mailer/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * Add DSN param `source_ip` to allow binding to a (specific) IPv4 or IPv6 address. * Add DSN param `require_tls` to enforce use of TLS/STARTTLS * Add `DkimSignedMessageListener`, `SmimeEncryptedMessageListener`, and `SmimeSignedMessageListener` + * Add support for translatable subject in `TemplatedEmail` 7.2 --- diff --git a/src/Symfony/Component/Mailer/EventListener/MessageListener.php b/src/Symfony/Component/Mailer/EventListener/MessageListener.php index 906e410faf83d..787b9376d4dae 100644 --- a/src/Symfony/Component/Mailer/EventListener/MessageListener.php +++ b/src/Symfony/Component/Mailer/EventListener/MessageListener.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Mailer\EventListener; +use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Mailer\Event\MessageEvent; use Symfony\Component\Mailer\Exception\InvalidArgumentException; @@ -19,9 +20,11 @@ use Symfony\Component\Mime\Header\Headers; use Symfony\Component\Mime\Header\MailboxListHeader; use Symfony\Component\Mime\Message; +use Symfony\Component\Translation\TranslatableMessage; +use Symfony\Contracts\Translation\TranslatorInterface; /** - * Manipulates the headers and the body of a Message. + * Manipulates the headers, subject, and the body of a Message. * * @author Fabien Potencier */ @@ -45,6 +48,7 @@ public function __construct( private ?Headers $headers = null, private ?BodyRendererInterface $renderer = null, array $headerRules = self::DEFAULT_RULES, + private ?TranslatorInterface $translator = null, ) { foreach ($headerRules as $headerName => $rule) { $this->addHeaderRule($headerName, $rule); @@ -68,6 +72,7 @@ public function onMessage(MessageEvent $event): void } $this->setHeaders($message); + $this->translateSubject($message); $this->renderMessage($message); } @@ -115,6 +120,15 @@ private function setHeaders(Message $message): void } } + private function translateSubject(Message $message): void + { + if (!$message instanceof TemplatedEmail || !$this->translator || !$message->getTranslatableSubject() instanceof TranslatableMessage) { + return; + } + + $message->subject($message->getTranslatableSubject()->trans($this->translator, $message->getLocale())); + } + private function renderMessage(Message $message): void { if (!$this->renderer) { diff --git a/src/Symfony/Component/Mailer/Tests/EventListener/MessageListenerTest.php b/src/Symfony/Component/Mailer/Tests/EventListener/MessageListenerTest.php index 5f5def704bf33..6ca41756c108a 100644 --- a/src/Symfony/Component/Mailer/Tests/EventListener/MessageListenerTest.php +++ b/src/Symfony/Component/Mailer/Tests/EventListener/MessageListenerTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Mailer\Tests\EventListener; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Event\MessageEvent; use Symfony\Component\Mailer\EventListener\MessageListener; @@ -20,6 +21,8 @@ use Symfony\Component\Mime\Header\MailboxListHeader; use Symfony\Component\Mime\Header\UnstructuredHeader; use Symfony\Component\Mime\Message; +use Symfony\Component\Translation\TranslatableMessage; +use Symfony\Contracts\Translation\TranslatorInterface; class MessageListenerTest extends TestCase { @@ -114,4 +117,25 @@ public static function provideHeaders(): iterable ]; yield 'Capitalized header rule (case-insensitive), replace if set' => [$initialHeaders, $defaultHeaders, $defaultHeaders, $rules]; } + + public function testTranslatableSubject() + { + $message = new TemplatedEmail(); + $message->subject(new TranslatableMessage('hello.world')); + $listener = new MessageListener(translator: new class implements TranslatorInterface { + public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string + { + return 'Hello World'; + } + + public function getLocale(): string + { + return 'en'; + } + }); + $event = new MessageEvent($message, new Envelope(new Address('sender@example.com'), [new Address('recipient@example.com')]), 'smtp'); + $listener->onMessage($event); + + $this->assertSame('Hello World', $message->getSubject()); + } } diff --git a/src/Symfony/Component/Mailer/composer.json b/src/Symfony/Component/Mailer/composer.json index 4336e725133fc..9b36cb678055e 100644 --- a/src/Symfony/Component/Mailer/composer.json +++ b/src/Symfony/Component/Mailer/composer.json @@ -21,13 +21,14 @@ "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/mime": "^7.2", + "symfony/mime": "^7.3", "symfony/service-contracts": "^2.5|^3" }, "require-dev": { "symfony/console": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0", "symfony/messenger": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", "symfony/twig-bridge": "^6.4|^7.0" }, "conflict": { diff --git a/src/Symfony/Component/Mime/Email.php b/src/Symfony/Component/Mime/Email.php index e238ce39d1313..4c988d1e009a9 100644 --- a/src/Symfony/Component/Mime/Email.php +++ b/src/Symfony/Component/Mime/Email.php @@ -58,9 +58,9 @@ class Email extends Message /** * @return $this */ - public function subject(string $subject): static + public function subject(string|\Stringable $subject): static { - return $this->setHeaderBody('Text', 'Subject', $subject); + return $this->setHeaderBody('Text', 'Subject', (string) $subject); } public function getSubject(): ?string