From a2d50a100a6dec2a02f99a9ac72d16a7bf05d419 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Tue, 30 Dec 2025 12:13:09 +0400 Subject: [PATCH 1/2] Style: Update constructor --- .../AsyncEmailMessageHandler.php | 5 +-- .../CampaignProcessorMessageHandler.php | 6 +-- .../PasswordResetMessageHandler.php | 14 +++---- .../SubscriberConfirmationMessageHandler.php | 14 +++---- ...SubscriptionConfirmationMessageHandler.php | 31 +++------------- .../Repository/SubscriberListRepository.php | 16 ++++++++ .../Service/SubscriberCsvImporter.php | 37 +++++-------------- 7 files changed, 45 insertions(+), 78 deletions(-) diff --git a/src/Domain/Messaging/MessageHandler/AsyncEmailMessageHandler.php b/src/Domain/Messaging/MessageHandler/AsyncEmailMessageHandler.php index 42d91417..bfda027a 100644 --- a/src/Domain/Messaging/MessageHandler/AsyncEmailMessageHandler.php +++ b/src/Domain/Messaging/MessageHandler/AsyncEmailMessageHandler.php @@ -14,11 +14,8 @@ #[AsMessageHandler] class AsyncEmailMessageHandler { - private EmailService $emailService; - - public function __construct(EmailService $emailService) + public function __construct(private readonly EmailService $emailService) { - $this->emailService = $emailService; } /** diff --git a/src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php b/src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php index 35b4c841..2230ab67 100644 --- a/src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php +++ b/src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php @@ -195,9 +195,9 @@ private function handleEmailSending( MessagePrecacheDto $precachedContent, ): void { $processed = $this->messagePreparator->processMessageLinks( - $campaign->getId(), - $precachedContent, - $subscriber + campaignId: $campaign->getId(), + cachedMessageDto: $precachedContent, + subscriber: $subscriber ); $processed->textContent = $this->userPersonalizer->personalize( $processed->textContent, diff --git a/src/Domain/Messaging/MessageHandler/PasswordResetMessageHandler.php b/src/Domain/Messaging/MessageHandler/PasswordResetMessageHandler.php index 7d2a3096..ceedf35a 100644 --- a/src/Domain/Messaging/MessageHandler/PasswordResetMessageHandler.php +++ b/src/Domain/Messaging/MessageHandler/PasswordResetMessageHandler.php @@ -13,15 +13,11 @@ #[AsMessageHandler] class PasswordResetMessageHandler { - private EmailService $emailService; - private TranslatorInterface $translator; - private string $passwordResetUrl; - - public function __construct(EmailService $emailService, TranslatorInterface $translator, string $passwordResetUrl) - { - $this->emailService = $emailService; - $this->translator = $translator; - $this->passwordResetUrl = $passwordResetUrl; + public function __construct( + private readonly EmailService $emailService, + private readonly TranslatorInterface $translator, + private readonly string $passwordResetUrl + ) { } /** diff --git a/src/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandler.php b/src/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandler.php index 69ec42cb..4a9e3c07 100644 --- a/src/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandler.php +++ b/src/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandler.php @@ -16,15 +16,11 @@ #[AsMessageHandler] class SubscriberConfirmationMessageHandler { - private EmailService $emailService; - private TranslatorInterface $translator; - private string $confirmationUrl; - - public function __construct(EmailService $emailService, TranslatorInterface $translator, string $confirmationUrl) - { - $this->emailService = $emailService; - $this->translator = $translator; - $this->confirmationUrl = $confirmationUrl; + public function __construct( + private readonly EmailService $emailService, + private readonly TranslatorInterface $translator, + private readonly string $confirmationUrl + ) { } /** diff --git a/src/Domain/Messaging/MessageHandler/SubscriptionConfirmationMessageHandler.php b/src/Domain/Messaging/MessageHandler/SubscriptionConfirmationMessageHandler.php index 6ecb965b..2615ae5f 100644 --- a/src/Domain/Messaging/MessageHandler/SubscriptionConfirmationMessageHandler.php +++ b/src/Domain/Messaging/MessageHandler/SubscriptionConfirmationMessageHandler.php @@ -20,24 +20,13 @@ #[AsMessageHandler] class SubscriptionConfirmationMessageHandler { - private EmailService $emailService; - private ConfigProvider $configProvider; - private LoggerInterface $logger; - private UserPersonalizer $userPersonalizer; - private SubscriberListRepository $subscriberListRepository; - public function __construct( - EmailService $emailService, - ConfigProvider $configProvider, - LoggerInterface $logger, - UserPersonalizer $userPersonalizer, - SubscriberListRepository $subscriberListRepository, + private readonly EmailService $emailService, + private readonly ConfigProvider $configProvider, + private readonly LoggerInterface $logger, + private readonly UserPersonalizer $userPersonalizer, + private readonly SubscriberListRepository $subscriberListRepository, ) { - $this->emailService = $emailService; - $this->configProvider = $configProvider; - $this->logger = $logger; - $this->userPersonalizer = $userPersonalizer; - $this->subscriberListRepository = $subscriberListRepository; } /** @@ -63,14 +52,6 @@ public function __invoke(SubscriptionConfirmationMessage $message): void private function getListNames(array $listIds): string { - $listNames = []; - foreach ($listIds as $id) { - $list = $this->subscriberListRepository->find($id); - if ($list) { - $listNames[] = $list->getName(); - } - } - - return implode(', ', $listNames); + return implode(', ', $this->subscriberListRepository->getListNames($listIds)); } } diff --git a/src/Domain/Subscription/Repository/SubscriberListRepository.php b/src/Domain/Subscription/Repository/SubscriberListRepository.php index d8910751..da549724 100644 --- a/src/Domain/Subscription/Repository/SubscriberListRepository.php +++ b/src/Domain/Subscription/Repository/SubscriberListRepository.php @@ -53,4 +53,20 @@ public function getAllActive(): array ->getQuery() ->getResult(); } + + public function getListNames(array $listIds): array + { + if ($listIds === []) { + return []; + } + + $lists = $this->createQueryBuilder('l') + ->select('l.name') + ->where('l.id IN (:ids)') + ->setParameter('ids', $listIds) + ->getQuery() + ->getScalarResult(); + + return array_column($lists, 'name'); + } } diff --git a/src/Domain/Subscription/Service/SubscriberCsvImporter.php b/src/Domain/Subscription/Service/SubscriberCsvImporter.php index f962c9ab..f9db822a 100644 --- a/src/Domain/Subscription/Service/SubscriberCsvImporter.php +++ b/src/Domain/Subscription/Service/SubscriberCsvImporter.php @@ -34,36 +34,17 @@ */ class SubscriberCsvImporter { - private SubscriberManager $subscriberManager; - private SubscriberAttributeManager $attributeManager; - private SubscriptionManager $subscriptionManager; - private SubscriberRepository $subscriberRepository; - private CsvToDtoImporter $csvToDtoImporter; - private EntityManagerInterface $entityManager; - private TranslatorInterface $translator; - private MessageBusInterface $messageBus; - private SubscriberHistoryManager $subscriberHistoryManager; - public function __construct( - SubscriberManager $subscriberManager, - SubscriberAttributeManager $attributeManager, - SubscriptionManager $subscriptionManager, - SubscriberRepository $subscriberRepository, - CsvToDtoImporter $csvToDtoImporter, - EntityManagerInterface $entityManager, - TranslatorInterface $translator, - MessageBusInterface $messageBus, - SubscriberHistoryManager $subscriberHistoryManager, + private readonly SubscriberManager $subscriberManager, + private readonly SubscriberAttributeManager $attributeManager, + private readonly SubscriptionManager $subscriptionManager, + private readonly SubscriberRepository $subscriberRepository, + private readonly CsvToDtoImporter $csvToDtoImporter, + private readonly EntityManagerInterface $entityManager, + private readonly TranslatorInterface $translator, + private readonly MessageBusInterface $messageBus, + private readonly SubscriberHistoryManager $subscriberHistoryManager, ) { - $this->subscriberManager = $subscriberManager; - $this->attributeManager = $attributeManager; - $this->subscriptionManager = $subscriptionManager; - $this->subscriberRepository = $subscriberRepository; - $this->csvToDtoImporter = $csvToDtoImporter; - $this->entityManager = $entityManager; - $this->translator = $translator; - $this->messageBus = $messageBus; - $this->subscriberHistoryManager = $subscriberHistoryManager; } /** From 216978e715d157a63792a613ce38742944bc6ada Mon Sep 17 00:00:00 2001 From: Tatevik Date: Tue, 30 Dec 2025 12:42:01 +0400 Subject: [PATCH 2/2] UserPersonalizer --- config/parameters.yml.dist | 2 + config/services/services.yml | 2 + .../Configuration/Model/OutputFormat.php | 11 +++ .../Service/UserPersonalizer.php | 76 +++++++++++++++++-- .../CampaignProcessorMessageHandler.php | 12 ++- ...SubscriptionConfirmationMessageHandler.php | 12 ++- .../Service/MessagePrecacheService.php | 2 +- .../Repository/SubscriberListRepository.php | 22 ++++++ 8 files changed, 124 insertions(+), 15 deletions(-) create mode 100644 src/Domain/Configuration/Model/OutputFormat.php diff --git a/config/parameters.yml.dist b/config/parameters.yml.dist index 92b33bed..efbdc7e5 100644 --- a/config/parameters.yml.dist +++ b/config/parameters.yml.dist @@ -31,6 +31,8 @@ parameters: env(APP_DEV_EMAIL): 'dev@dev.com' app.powered_by_phplist: '%%env(APP_POWERED_BY_PHPLIST)%%' env(APP_POWERED_BY_PHPLIST): '0' + app.preference_page_show_private_lists: '%%env(PREFERENCEPAGE_SHOW_PRIVATE_LISTS)%%' + env(PREFERENCEPAGE_SHOW_PRIVATE_LISTS): '0' # Email configuration app.mailer_from: '%%env(MAILER_FROM)%%' diff --git a/config/services/services.yml b/config/services/services.yml index cf298621..1517737b 100644 --- a/config/services/services.yml +++ b/config/services/services.yml @@ -123,6 +123,8 @@ services: PhpList\Core\Domain\Configuration\Service\UserPersonalizer: autowire: true autoconfigure: true + arguments: + $preferencePageShowPrivateLists: '%app.preference_page_show_private_lists%' PhpList\Core\Domain\Configuration\Service\LegacyUrlBuilder: autowire: true diff --git a/src/Domain/Configuration/Model/OutputFormat.php b/src/Domain/Configuration/Model/OutputFormat.php new file mode 100644 index 00000000..c413d3ed --- /dev/null +++ b/src/Domain/Configuration/Model/OutputFormat.php @@ -0,0 +1,11 @@ +subscriberRepository->findOneByEmail($email); if (!$user) { @@ -33,18 +39,49 @@ public function personalize(string $value, string $email): string $resolver = new PlaceholderResolver(); $resolver->register('EMAIL', fn() => $user->getEmail()); - $resolver->register('UNSUBSCRIBEURL', function () use ($user) { + $resolver->register('UNSUBSCRIBEURL', function () use ($user, $format) { $base = $this->config->getValue(ConfigOption::UnsubscribeUrl) ?? ''; - return $this->urlBuilder->withUid($base, $user->getUniqueId()) . self::PHP_SPACE; + $url = $this->urlBuilder->withUid($base, $user->getUniqueId()); + + if ($format === OutputFormat::Html) { + $label = $this->translator->trans('Unsubscribe'); + $safeUrl = htmlspecialchars($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + $safeLabel = htmlspecialchars($label, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + + return '' . $safeLabel . '' . self::PHP_SPACE; + } + + return $url . self::PHP_SPACE; }); - $resolver->register('CONFIRMATIONURL', function () use ($user) { + $resolver->register('CONFIRMATIONURL', function () use ($user, $format) { $base = $this->config->getValue(ConfigOption::ConfirmationUrl) ?? ''; - return $this->urlBuilder->withUid($base, $user->getUniqueId()) . self::PHP_SPACE; + $url = $this->urlBuilder->withUid($base, $user->getUniqueId()); + + if ($format === OutputFormat::Html) { + $label = $this->translator->trans('Confirm'); + $safeUrl = htmlspecialchars($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + $safeLabel = htmlspecialchars($label, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + + return '' . $safeLabel . '' . self::PHP_SPACE; + } + + return $url . self::PHP_SPACE; }); - $resolver->register('PREFERENCESURL', function () use ($user) { + + $resolver->register('PREFERENCESURL', function () use ($user, $format) { $base = $this->config->getValue(ConfigOption::PreferencesUrl) ?? ''; - return $this->urlBuilder->withUid($base, $user->getUniqueId()) . self::PHP_SPACE; + $url = $this->urlBuilder->withUid($base, $user->getUniqueId()); + + if ($format === OutputFormat::Html) { + $label = $this->translator->trans('Update preferences'); + $safeUrl = htmlspecialchars($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + $safeLabel = htmlspecialchars($label, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + + return '' . $safeLabel . '' . self::PHP_SPACE; + } + + return $url . self::PHP_SPACE; }); $resolver->register( @@ -54,6 +91,29 @@ public function personalize(string $value, string $email): string $resolver->register('DOMAIN', fn() => $this->config->getValue(ConfigOption::Domain) ?? ''); $resolver->register('WEBSITE', fn() => $this->config->getValue(ConfigOption::Website) ?? ''); + $resolver->register('LISTS', function () use ($user, $format) { + $names = $this->subscriberListRepository->getActiveListNamesForSubscriber( + subscriber: $user, + showPrivate: $this->preferencePageShowPrivateLists + ); + + if ($names === []) { + return $this->translator + ->trans('Sorry, you are not subscribed to any of our newsletters with this email address.'); + } + + $separator = $format === OutputFormat::Html ? '
' : "\n"; + + if ($format === OutputFormat::Html) { + $names = array_map( + static fn(string $name) => htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'), + $names + ); + } + + return implode($separator, $names); + }); + $userAttributes = $this->attributesRepository->getForSubscriber($user); foreach ($userAttributes as $userAttribute) { $resolver->register( diff --git a/src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php b/src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php index 2230ab67..ed6ca612 100644 --- a/src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php +++ b/src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php @@ -8,6 +8,7 @@ use DateTimeImmutable; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Doctrine\ORM\EntityManagerInterface; +use PhpList\Core\Domain\Configuration\Model\OutputFormat; use PhpList\Core\Domain\Configuration\Service\UserPersonalizer; use PhpList\Core\Domain\Messaging\Exception\MessageCacheMissingException; use PhpList\Core\Domain\Messaging\Exception\MessageSizeLimitExceededException; @@ -200,10 +201,15 @@ private function handleEmailSending( subscriber: $subscriber ); $processed->textContent = $this->userPersonalizer->personalize( - $processed->textContent, - $subscriber->getEmail(), + value: $processed->textContent, + email: $subscriber->getEmail(), + format:OutputFormat::Text, + ); + $processed->footer = $this->userPersonalizer->personalize( + value: $processed->footer, + email: $subscriber->getEmail(), + format: OutputFormat::Text, ); - $processed->footer = $this->userPersonalizer->personalize($processed->footer, $subscriber->getEmail()); try { $email = $this->rateLimitedCampaignMailer->composeEmail($campaign, $subscriber, $processed); diff --git a/src/Domain/Messaging/MessageHandler/SubscriptionConfirmationMessageHandler.php b/src/Domain/Messaging/MessageHandler/SubscriptionConfirmationMessageHandler.php index 2615ae5f..73f2fda0 100644 --- a/src/Domain/Messaging/MessageHandler/SubscriptionConfirmationMessageHandler.php +++ b/src/Domain/Messaging/MessageHandler/SubscriptionConfirmationMessageHandler.php @@ -5,6 +5,7 @@ namespace PhpList\Core\Domain\Messaging\MessageHandler; use PhpList\Core\Domain\Configuration\Model\ConfigOption; +use PhpList\Core\Domain\Configuration\Model\OutputFormat; use PhpList\Core\Domain\Configuration\Service\Provider\ConfigProvider; use PhpList\Core\Domain\Configuration\Service\UserPersonalizer; use PhpList\Core\Domain\Messaging\Message\SubscriptionConfirmationMessage; @@ -36,14 +37,19 @@ public function __invoke(SubscriptionConfirmationMessage $message): void { $subject = $this->configProvider->getValue(ConfigOption::SubscribeEmailSubject); $textContent = $this->configProvider->getValue(ConfigOption::SubscribeMessage); - $personalizedTextContent = $this->userPersonalizer->personalize($textContent, $message->getUniqueId()); $listOfLists = $this->getListNames($message->getListIds()); - $replacedTextContent = str_replace('[LISTS]', $listOfLists, $personalizedTextContent); + $replacedTextContent = str_replace('[LISTS]', $listOfLists, $textContent); + + $personalizedTextContent = $this->userPersonalizer->personalize( + value: $replacedTextContent, + email: $message->getUniqueId(), + format: OutputFormat::Text, + ); $email = (new Email()) ->to($message->getEmail()) ->subject($subject) - ->text($replacedTextContent); + ->text($personalizedTextContent); $this->emailService->sendEmail($email); diff --git a/src/Domain/Messaging/Service/MessagePrecacheService.php b/src/Domain/Messaging/Service/MessagePrecacheService.php index 2a0f6b28..2628257e 100644 --- a/src/Domain/Messaging/Service/MessagePrecacheService.php +++ b/src/Domain/Messaging/Service/MessagePrecacheService.php @@ -195,7 +195,7 @@ private function applyRemoteContentIfPresent(MessagePrecacheDto $messagePrecache } else { $this->eventLogManager->log( page: 'unknown page', - entry: 'Error fetching URL: '.$loadedMessageData['sendurl'].' cannot proceed', + entry: 'Error fetching URL: ' . $loadedMessageData['sendurl'] . ' cannot proceed', ); return false; diff --git a/src/Domain/Subscription/Repository/SubscriberListRepository.php b/src/Domain/Subscription/Repository/SubscriberListRepository.php index da549724..988c23e3 100644 --- a/src/Domain/Subscription/Repository/SubscriberListRepository.php +++ b/src/Domain/Subscription/Repository/SubscriberListRepository.php @@ -9,6 +9,7 @@ use PhpList\Core\Domain\Common\Repository\Interfaces\PaginatableRepositoryInterface; use PhpList\Core\Domain\Identity\Model\Administrator; use PhpList\Core\Domain\Messaging\Model\Message; +use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Model\SubscriberList; /** @@ -69,4 +70,25 @@ public function getListNames(array $listIds): array return array_column($lists, 'name'); } + + /** + * Returns the names of lists the given subscriber is subscribed to. + * If $showPrivate is false, only active/public lists are included. + */ + public function getActiveListNamesForSubscriber(Subscriber $subscriber, bool $showPrivate): array + { + $qb = $this->createQueryBuilder('l') + ->select('l.name') + ->innerJoin('l.subscriptions', 's') + ->where('IDENTITY(s.subscriber) = :subscriberId') + ->setParameter('subscriberId', $subscriber->getId()); + + if (!$showPrivate) { + $qb->andWhere('l.active = true'); + } + + $rows = $qb->getQuery()->getScalarResult(); + + return array_column($rows, 'name'); + } }