Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mail localisation #219

Merged
merged 5 commits into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions app/migrations/Version20231218175905.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20231218175905 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->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');
}
}
1 change: 1 addition & 0 deletions app/src/DataFixtures/AppFixtures.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
19 changes: 17 additions & 2 deletions app/src/Entity/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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
*/
Expand Down
2 changes: 2 additions & 0 deletions config/serializer/Entity/User.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,7 @@ App\Entity\User:
exclude: true
emailAuthCodeGeneratedAt:
exclude: true
preferredLocale:
exclude: true
twoFactorAuthenticationEnabledProviders:
exclude: true
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

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;
use Draw\Component\Mailer\EventListener\EmailCssInlinerListener;
Expand All @@ -11,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
Expand All @@ -28,11 +31,18 @@ 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(),
),
);

$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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@

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;
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;
Expand Down Expand Up @@ -109,9 +112,27 @@ 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.body_renderer.localize_body_renderer',
[
LocalizeBodyRenderer::class,
]
),
new ServiceConfiguration(
'draw.mailer.email_writer.add_template_header_email_writer',
[
AddTemplateHeaderEmailWriter::class,
]
),
new ServiceConfiguration(
'draw.mailer.event_listener.email_composer_listener',
[
EmailComposerListener::class,
]
),
new ServiceConfiguration(
Expand Down
40 changes: 40 additions & 0 deletions packages/mailer/BodyRenderer/LocalizeBodyRenderer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Draw\Component\Mailer\BodyRenderer;

use Draw\Component\Mailer\Email\LocalizeEmailInterface;
use Symfony\Component\Mime\BodyRendererInterface;
use Symfony\Component\Mime\Message;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

class LocalizeBodyRenderer implements BodyRendererInterface
{
private ?LocaleAwareInterface $translator = null;

public function __construct(
private BodyRendererInterface $bodyRenderer,
TranslatorInterface $translator
) {
if ($translator instanceof LocaleAwareInterface) {
$this->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);
}
}
}
}
4 changes: 3 additions & 1 deletion packages/mailer/Email/CallToActionEmail.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down
8 changes: 8 additions & 0 deletions packages/mailer/Email/LocalizeEmailInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Draw\Component\Mailer\Email;

interface LocalizeEmailInterface
{
public function getLocale(): ?string;
}
20 changes: 20 additions & 0 deletions packages/mailer/Email/LocalizeEmailTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Draw\Component\Mailer\Email;

trait LocalizeEmailTrait
{
private ?string $locale = null;

public function setLocale(?string $locale): self
{
$this->locale = $locale;

return $this;
}

public function getLocale(): ?string
{
return $this->locale;
}
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,56 @@
<?php

namespace Draw\Component\Mailer\EventListener;
namespace Draw\Component\Mailer;

use Draw\Component\Core\Reflection\ReflectionAccessor;
use Draw\Component\Mailer\Email\LocalizeEmailInterface;
use Draw\Component\Mailer\EmailWriter\EmailWriterInterface;
use Psr\Container\ContainerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\Event\MessageEvent;
use Symfony\Component\Mime\Header\UnstructuredHeader;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\RawMessage;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

class EmailWriterListener implements EventSubscriberInterface
class EmailComposer
{
private array $writers = [];

private array $sortedWriters = [];

public static function getSubscribedEvents(): array
{
return [
MessageEvent::class => ['composeMessage', 200],
];
private ?LocaleAwareInterface $translator = null;

public function __construct(
private ContainerInterface $serviceLocator,
TranslatorInterface $translator
) {
if ($translator instanceof LocaleAwareInterface) {
$this->translator = $translator;
}
}

public function __construct(private ContainerInterface $serviceLocator)
public function compose(Message $message, Envelope $envelope): void
{
$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);
}
}
}

public function registerEmailWriter(EmailWriterInterface $emailWriter): void
Expand Down Expand Up @@ -78,46 +102,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]
Expand Down
Loading
Loading