From b919f001d9dba2689750d14483cb1ba5415ec5ea Mon Sep 17 00:00:00 2001
From: Tatevik
Date: Wed, 17 Sep 2025 13:03:08 +0400
Subject: [PATCH 01/17] MissingMessageIdException
---
.../Exception/MissingMessageIdException.php | 15 +++++++++++++++
src/Domain/Analytics/Service/LinkTrackService.php | 6 +++---
.../Analytics/Service/LinkTrackServiceTest.php | 4 ++--
3 files changed, 20 insertions(+), 5 deletions(-)
create mode 100644 src/Domain/Analytics/Exception/MissingMessageIdException.php
diff --git a/src/Domain/Analytics/Exception/MissingMessageIdException.php b/src/Domain/Analytics/Exception/MissingMessageIdException.php
new file mode 100644
index 00000000..71479ff0
--- /dev/null
+++ b/src/Domain/Analytics/Exception/MissingMessageIdException.php
@@ -0,0 +1,15 @@
+getId();
if ($messageId === null) {
- throw new InvalidArgumentException('Message must have an ID');
+ throw new MissingMessageIdException();
}
$links = $this->extractLinksFromHtml($content->getText() ?? '');
diff --git a/tests/Unit/Domain/Analytics/Service/LinkTrackServiceTest.php b/tests/Unit/Domain/Analytics/Service/LinkTrackServiceTest.php
index 613e2c1f..109fb634 100644
--- a/tests/Unit/Domain/Analytics/Service/LinkTrackServiceTest.php
+++ b/tests/Unit/Domain/Analytics/Service/LinkTrackServiceTest.php
@@ -4,8 +4,8 @@
namespace PhpList\Core\Tests\Unit\Domain\Analytics\Service;
-use InvalidArgumentException;
use PhpList\Core\Core\ConfigProvider;
+use PhpList\Core\Domain\Analytics\Exception\MissingMessageIdException;
use PhpList\Core\Domain\Analytics\Model\LinkTrack;
use PhpList\Core\Domain\Analytics\Repository\LinkTrackRepository;
use PhpList\Core\Domain\Analytics\Service\LinkTrackService;
@@ -172,7 +172,7 @@ public function testExtractAndSaveLinksWithMessageWithoutId(): void
$message->method('getId')->willReturn(null);
$message->method('getContent')->willReturn($messageContent);
- $this->expectException(InvalidArgumentException::class);
+ $this->expectException(MissingMessageIdException::class);
$this->expectExceptionMessage('Message must have an ID');
$this->subject->extractAndSaveLinks($message, $userId);
From 19cf9491af79aff497adcd54d891e63a5f87689f Mon Sep 17 00:00:00 2001
From: Tatevik
Date: Wed, 17 Sep 2025 13:07:28 +0400
Subject: [PATCH 02/17] MailboxConnectionException
---
.../Exception/MailboxConnectionException.php | 23 +++++++++++++++++++
.../Common/Mail/NativeImapMailReader.php | 4 ++--
2 files changed, 25 insertions(+), 2 deletions(-)
create mode 100644 src/Domain/Common/Exception/MailboxConnectionException.php
diff --git a/src/Domain/Common/Exception/MailboxConnectionException.php b/src/Domain/Common/Exception/MailboxConnectionException.php
new file mode 100644
index 00000000..20a927b5
--- /dev/null
+++ b/src/Domain/Common/Exception/MailboxConnectionException.php
@@ -0,0 +1,23 @@
+username, $this->password, $options);
if ($link === false) {
- throw new RuntimeException('Cannot open mailbox: '.(imap_last_error() ?: 'unknown error'));
+ throw new MailboxConnectionException($mailbox);
}
return $link;
From 5a83f030af6ae47eb8a6e82a6295563ee268d397 Mon Sep 17 00:00:00 2001
From: Tatevik
Date: Wed, 17 Sep 2025 13:11:21 +0400
Subject: [PATCH 03/17] BadMethodCallException (style fix)
---
src/Domain/Common/IspRestrictionsProvider.php | 6 ++++++
src/Domain/Common/Repository/CursorPaginationTrait.php | 3 ++-
src/Domain/Common/SystemInfoCollector.php | 7 +++----
.../Configuration/Service/Manager/EventLogManager.php | 1 +
4 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/src/Domain/Common/IspRestrictionsProvider.php b/src/Domain/Common/IspRestrictionsProvider.php
index 4095f5ce..5ae75041 100644
--- a/src/Domain/Common/IspRestrictionsProvider.php
+++ b/src/Domain/Common/IspRestrictionsProvider.php
@@ -37,8 +37,10 @@ private function readConfigFile(): ?string
$contents = file_get_contents($this->confPath);
if ($contents === false) {
$this->logger->warning('Cannot read ISP restrictions file', ['path' => $this->confPath]);
+
return null;
}
+
return $contents;
}
@@ -106,20 +108,24 @@ private function applyKeyValue(
if ($val !== '' && ctype_digit($val)) {
$maxBatch = (int) $val;
}
+
return [$maxBatch, $minBatchPeriod, $lockFile];
}
if ($key === 'minbatchperiod') {
if ($val !== '' && ctype_digit($val)) {
$minBatchPeriod = (int) $val;
}
+
return [$maxBatch, $minBatchPeriod, $lockFile];
}
if ($key === 'lockfile') {
if ($val !== '') {
$lockFile = $val;
}
+
return [$maxBatch, $minBatchPeriod, $lockFile];
}
+
return [$maxBatch, $minBatchPeriod, $lockFile];
}
diff --git a/src/Domain/Common/Repository/CursorPaginationTrait.php b/src/Domain/Common/Repository/CursorPaginationTrait.php
index 8be64ee2..be5164fb 100644
--- a/src/Domain/Common/Repository/CursorPaginationTrait.php
+++ b/src/Domain/Common/Repository/CursorPaginationTrait.php
@@ -4,6 +4,7 @@
namespace PhpList\Core\Domain\Common\Repository;
+use BadMethodCallException;
use PhpList\Core\Domain\Common\Model\Filter\FilterRequestInterface;
use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel;
use RuntimeException;
@@ -38,6 +39,6 @@ public function getFilteredAfterId(int $lastId, int $limit, ?FilterRequestInterf
return $this->getAfterId($lastId, $limit);
}
- throw new RuntimeException('Filter method not implemented');
+ throw new BadMethodCallException('getFilteredAfterId method not implemented');
}
}
diff --git a/src/Domain/Common/SystemInfoCollector.php b/src/Domain/Common/SystemInfoCollector.php
index e66d27b1..56b30579 100644
--- a/src/Domain/Common/SystemInfoCollector.php
+++ b/src/Domain/Common/SystemInfoCollector.php
@@ -16,10 +16,8 @@ class SystemInfoCollector
/**
* @param string[] $configuredKeys keys to include (empty => use defaults)
*/
- public function __construct(
- RequestStack $requestStack,
- array $configuredKeys = []
- ) {
+ public function __construct(RequestStack $requestStack, array $configuredKeys = [])
+ {
$this->requestStack = $requestStack;
$this->configuredKeys = $configuredKeys;
}
@@ -72,6 +70,7 @@ public function collectAsString(): string
foreach ($pairs as $k => $v) {
$lines[] = sprintf('%s = %s', $k, $v);
}
+
return "\n" . implode("\n", $lines);
}
}
diff --git a/src/Domain/Configuration/Service/Manager/EventLogManager.php b/src/Domain/Configuration/Service/Manager/EventLogManager.php
index 374db7ed..d896a8f1 100644
--- a/src/Domain/Configuration/Service/Manager/EventLogManager.php
+++ b/src/Domain/Configuration/Service/Manager/EventLogManager.php
@@ -44,6 +44,7 @@ public function get(
?DateTimeInterface $dateTo = null
): array {
$filter = new EventLogFilter($page, $dateFrom, $dateTo);
+
return $this->repository->getFilteredAfterId($lastId, $limit, $filter);
}
From 8bf308a239f82a16c6dac60043cd193746b6c2d5 Mon Sep 17 00:00:00 2001
From: Tatevik
Date: Thu, 18 Sep 2025 12:17:57 +0400
Subject: [PATCH 04/17] Translate user facing messages
---
resources/translations/messages.en.xlf | 109 +++++++++++-------
src/Domain/Common/I18n/Messages.php | 29 -----
.../Command/CleanUpOldSessionTokens.php | 1 +
.../AdminAttributeDefinitionManager.php | 11 +-
.../Identity/Service/PasswordManager.php | 3 +-
.../Identity/Service/SessionManager.php | 9 +-
.../Command/ProcessBouncesCommand.php | 17 +--
.../Service/Manager/SubscriptionManager.php | 7 +-
.../Identity/Service/SessionManagerTest.php | 5 +-
9 files changed, 98 insertions(+), 93 deletions(-)
delete mode 100644 src/Domain/Common/I18n/Messages.php
diff --git a/resources/translations/messages.en.xlf b/resources/translations/messages.en.xlf
index 7e176e3e..e104fab1 100644
--- a/resources/translations/messages.en.xlf
+++ b/resources/translations/messages.en.xlf
@@ -1,44 +1,71 @@
-
-
-
-
-
- Not authorized
- Not authorized
-
-
-
- Failed admin login attempt for '%login%'
- Failed admin login attempt for '%login%'
-
-
-
- Login attempt for disabled admin '%login%'
- Login attempt for disabled admin '%login%'
-
-
-
-
- Administrator not found
- Administrator not found
-
-
-
-
- Subscriber list not found.
- Subscriber list not found.
-
-
- Subscriber does not exists.
- Subscriber does not exists.
-
-
- Subscription not found for this subscriber and list.
- Subscription not found for this subscriber and list.
-
-
-
-
+
+
+
+
+ Not authorized
+ Not authorized
+
+
+
+ Failed admin login attempt for '%login%'
+ Failed admin login attempt for '%login%'
+
+
+
+ Login attempt for disabled admin '%login%'
+ Login attempt for disabled admin '%login%'
+
+
+
+
+ Administrator not found
+ Administrator not found
+
+
+
+ Attribute definition already exists.
+ Attribute definition already exists.
+
+
+
+
+ PHP IMAP extension not available. Falling back to Webklex IMAP.
+ PHP IMAP extension not available. Falling back to Webklex IMAP.
+
+
+
+ Could not apply force lock. Aborting.
+ Could not apply force lock. Aborting.
+
+
+
+ Another bounce processing is already running. Aborting.
+ Another bounce processing is already running. Aborting.
+
+
+
+ Bounce processing completed.
+ Bounce processing completed.
+
+
+
+
+ Subscriber list not found.
+ Subscriber list not found.
+
+
+
+ Subscriber does not exists.
+ Subscriber does not exists.
+
+
+
+ Subscription not found for this subscriber and list.
+ Subscription not found for this subscriber and list.
+
+
+
+
diff --git a/src/Domain/Common/I18n/Messages.php b/src/Domain/Common/I18n/Messages.php
deleted file mode 100644
index f9e8822f..00000000
--- a/src/Domain/Common/I18n/Messages.php
+++ /dev/null
@@ -1,29 +0,0 @@
-writeln(sprintf('Successfully removed %d expired session token(s).', $deletedCount));
} catch (Throwable $throwable) {
$output->writeln(sprintf('Error removing expired session tokens: %s', $throwable->getMessage()));
+
return Command::FAILURE;
}
diff --git a/src/Domain/Identity/Service/AdminAttributeDefinitionManager.php b/src/Domain/Identity/Service/AdminAttributeDefinitionManager.php
index f0a18e07..24a70de8 100644
--- a/src/Domain/Identity/Service/AdminAttributeDefinitionManager.php
+++ b/src/Domain/Identity/Service/AdminAttributeDefinitionManager.php
@@ -9,25 +9,32 @@
use PhpList\Core\Domain\Identity\Repository\AdminAttributeDefinitionRepository;
use PhpList\Core\Domain\Identity\Exception\AttributeDefinitionCreationException;
use PhpList\Core\Domain\Subscription\Validator\AttributeTypeValidator;
+use Symfony\Contracts\Translation\TranslatorInterface;
class AdminAttributeDefinitionManager
{
private AdminAttributeDefinitionRepository $definitionRepository;
private AttributeTypeValidator $attributeTypeValidator;
+ private TranslatorInterface $translator;
public function __construct(
AdminAttributeDefinitionRepository $definitionRepository,
- AttributeTypeValidator $attributeTypeValidator
+ AttributeTypeValidator $attributeTypeValidator,
+ TranslatorInterface $translator
) {
$this->definitionRepository = $definitionRepository;
$this->attributeTypeValidator = $attributeTypeValidator;
+ $this->translator = $translator;
}
public function create(AdminAttributeDefinitionDto $attributeDefinitionDto): AdminAttributeDefinition
{
$existingAttribute = $this->definitionRepository->findOneByName($attributeDefinitionDto->name);
if ($existingAttribute) {
- throw new AttributeDefinitionCreationException('Attribute definition already exists', 409);
+ throw new AttributeDefinitionCreationException(
+ $this->translator->trans('Attribute definition already exists.'),
+ 409
+ );
}
$this->attributeTypeValidator->validate($attributeDefinitionDto->type);
diff --git a/src/Domain/Identity/Service/PasswordManager.php b/src/Domain/Identity/Service/PasswordManager.php
index 2c7ebe1e..36c88570 100644
--- a/src/Domain/Identity/Service/PasswordManager.php
+++ b/src/Domain/Identity/Service/PasswordManager.php
@@ -5,7 +5,6 @@
namespace PhpList\Core\Domain\Identity\Service;
use DateTime;
-use PhpList\Core\Domain\Common\I18n\Messages;
use PhpList\Core\Domain\Identity\Model\AdminPasswordRequest;
use PhpList\Core\Domain\Identity\Model\Administrator;
use PhpList\Core\Domain\Identity\Repository\AdminPasswordRequestRepository;
@@ -52,7 +51,7 @@ public function generatePasswordResetToken(string $email): string
{
$administrator = $this->administratorRepository->findOneBy(['email' => $email]);
if ($administrator === null) {
- $message = $this->translator->trans(Messages::IDENTITY_ADMIN_NOT_FOUND);
+ $message = $this->translator->trans('Administrator not found');
throw new NotFoundHttpException($message, null, 1500567100);
}
diff --git a/src/Domain/Identity/Service/SessionManager.php b/src/Domain/Identity/Service/SessionManager.php
index 82f52af1..105d3645 100644
--- a/src/Domain/Identity/Service/SessionManager.php
+++ b/src/Domain/Identity/Service/SessionManager.php
@@ -4,7 +4,6 @@
namespace PhpList\Core\Domain\Identity\Service;
-use PhpList\Core\Domain\Common\I18n\Messages;
use Symfony\Contracts\Translation\TranslatorInterface;
use PhpList\Core\Domain\Configuration\Service\Manager\EventLogManager;
use PhpList\Core\Domain\Identity\Model\AdministratorToken;
@@ -35,16 +34,16 @@ public function createSession(string $loginName, string $password): Administrato
{
$administrator = $this->administratorRepository->findOneByLoginCredentials($loginName, $password);
if ($administrator === null) {
- $entry = $this->translator->trans(Messages::AUTH_LOGIN_FAILED, ['login' => $loginName]);
+ $entry = $this->translator->trans("Failed admin login attempt for '%login%'", ['login' => $loginName]);
$this->eventLogManager->log('login', $entry);
- $message = $this->translator->trans(Messages::AUTH_NOT_AUTHORIZED);
+ $message = $this->translator->trans('Not authorized');
throw new UnauthorizedHttpException('', $message, null, 1500567098);
}
if ($administrator->isDisabled()) {
- $entry = $this->translator->trans(Messages::AUTH_LOGIN_DISABLED, ['login' => $loginName]);
+ $entry = $this->translator->trans("Login attempt for disabled admin '%login%'", ['login' => $loginName]);
$this->eventLogManager->log('login', $entry);
- $message = $this->translator->trans(Messages::AUTH_NOT_AUTHORIZED);
+ $message = $this->translator->trans('Not authorized');
throw new UnauthorizedHttpException('', $message, null, 1500567099);
}
diff --git a/src/Domain/Messaging/Command/ProcessBouncesCommand.php b/src/Domain/Messaging/Command/ProcessBouncesCommand.php
index f1e3b403..0d51d4f1 100644
--- a/src/Domain/Messaging/Command/ProcessBouncesCommand.php
+++ b/src/Domain/Messaging/Command/ProcessBouncesCommand.php
@@ -17,14 +17,11 @@
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Contracts\Translation\TranslatorInterface;
#[AsCommand(name: 'phplist:bounces:process', description: 'Process bounce mailbox')]
class ProcessBouncesCommand extends Command
{
- private const IMAP_NOT_AVAILABLE = 'PHP IMAP extension not available. Falling back to Webklex IMAP.';
- private const FORCE_LOCK_FAILED = 'Could not apply force lock. Aborting.';
- private const ALREADY_LOCKED = 'Another bounce processing is already running. Aborting.';
-
protected function configure(): void
{
$this
@@ -48,6 +45,7 @@ public function __construct(
private readonly AdvancedBounceRulesProcessor $advancedRulesProcessor,
private readonly UnidentifiedBounceReprocessor $unidentifiedReprocessor,
private readonly ConsecutiveBounceHandler $consecutiveBounceHandler,
+ private readonly TranslatorInterface $translator,
) {
parent::__construct();
}
@@ -57,14 +55,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$inputOutput = new SymfonyStyle($input, $output);
if (!function_exists('imap_open')) {
- $inputOutput->note(self::IMAP_NOT_AVAILABLE);
+ $inputOutput->note($this->translator->trans(
+ 'PHP IMAP extension not available. Falling back to Webklex IMAP.'
+ ));
}
$force = (bool)$input->getOption('force');
$lock = $this->lockService->acquirePageLock('bounce_processor', $force);
if (($lock ?? 0) === 0) {
- $inputOutput->warning($force ? self::FORCE_LOCK_FAILED : self::ALREADY_LOCKED);
+ $forceLockFailed = $this->translator->trans('Could not apply force lock. Aborting.');
+ $lockFailed = $this->translator->trans('Another bounce processing is already running. Aborting.');
+
+ $inputOutput->warning($force ? $forceLockFailed : $lockFailed);
return $force ? Command::FAILURE : Command::SUCCESS;
}
@@ -88,7 +91,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->consecutiveBounceHandler->handle($inputOutput);
$this->logger->info('Bounce processing completed', ['downloadReport' => $downloadReport]);
- $inputOutput->success('Bounce processing completed.');
+ $inputOutput->success($this->translator->trans('Bounce processing completed.'));
return Command::SUCCESS;
} catch (Exception $e) {
diff --git a/src/Domain/Subscription/Service/Manager/SubscriptionManager.php b/src/Domain/Subscription/Service/Manager/SubscriptionManager.php
index 764106ec..6bed4d5b 100644
--- a/src/Domain/Subscription/Service/Manager/SubscriptionManager.php
+++ b/src/Domain/Subscription/Service/Manager/SubscriptionManager.php
@@ -4,7 +4,6 @@
namespace PhpList\Core\Domain\Subscription\Service\Manager;
-use PhpList\Core\Domain\Common\I18n\Messages;
use PhpList\Core\Domain\Subscription\Exception\SubscriptionCreationException;
use PhpList\Core\Domain\Subscription\Model\Subscriber;
use PhpList\Core\Domain\Subscription\Model\SubscriberList;
@@ -42,7 +41,7 @@ public function addSubscriberToAList(Subscriber $subscriber, int $listId): Subsc
}
$subscriberList = $this->subscriberListRepository->find($listId);
if (!$subscriberList) {
- $message = $this->translator->trans(Messages::SUBSCRIPTION_LIST_NOT_FOUND);
+ $message = $this->translator->trans('Subscriber list not found.');
throw new SubscriptionCreationException($message, 404);
}
@@ -70,7 +69,7 @@ private function createSubscription(SubscriberList $subscriberList, string $emai
{
$subscriber = $this->subscriberRepository->findOneBy(['email' => $email]);
if (!$subscriber) {
- $message = $this->translator->trans(Messages::SUBSCRIPTION_SUBSCRIBER_NOT_FOUND);
+ $message = $this->translator->trans('Subscriber does not exists.');
throw new SubscriptionCreationException($message, 404);
}
@@ -108,7 +107,7 @@ private function deleteSubscription(SubscriberList $subscriberList, string $emai
->findOneBySubscriberEmailAndListId($subscriberList->getId(), $email);
if (!$subscription) {
- $message = $this->translator->trans(Messages::SUBSCRIPTION_NOT_FOUND_FOR_LIST_AND_SUBSCRIBER);
+ $message = $this->translator->trans('Subscription not found for this subscriber and list.');
throw new SubscriptionCreationException($message, 404);
}
diff --git a/tests/Unit/Domain/Identity/Service/SessionManagerTest.php b/tests/Unit/Domain/Identity/Service/SessionManagerTest.php
index 14419b0e..da620f12 100644
--- a/tests/Unit/Domain/Identity/Service/SessionManagerTest.php
+++ b/tests/Unit/Domain/Identity/Service/SessionManagerTest.php
@@ -4,7 +4,6 @@
namespace PhpList\Core\Tests\Unit\Domain\Identity\Service;
-use PhpList\Core\Domain\Common\I18n\Messages;
use PhpList\Core\Domain\Configuration\Service\Manager\EventLogManager;
use PhpList\Core\Domain\Identity\Model\AdministratorToken;
use PhpList\Core\Domain\Identity\Repository\AdministratorRepository;
@@ -36,8 +35,8 @@ public function testCreateSessionWithInvalidCredentialsThrowsExceptionAndLogs():
$translator->expects(self::exactly(2))
->method('trans')
->withConsecutive(
- [Messages::AUTH_LOGIN_FAILED, ['login' => 'admin']],
- [Messages::AUTH_NOT_AUTHORIZED, []]
+ ["Failed admin login attempt for '%login%'", ['login' => 'admin']],
+ ['Not authorized', []]
)
->willReturnOnConsecutiveCalls(
"Failed admin login attempt for 'admin'",
From fe66ee0d431895c933e85649e0f34759c4c43221 Mon Sep 17 00:00:00 2001
From: Tatevik
Date: Thu, 18 Sep 2025 13:52:27 +0400
Subject: [PATCH 05/17] Translate
---
resources/translations/messages.en.xlf | 45 +++++++++++++++++++
.../Repository/CursorPaginationTrait.php | 5 +--
.../Messaging/Command/ProcessQueueCommand.php | 12 +++--
.../Command/SendTestEmailCommand.php | 30 +++++++++----
.../Repository/CursorPaginationTraitTest.php | 6 +--
.../AdminAttributeDefinitionManagerTest.php | 14 +++++-
.../Command/ProcessBouncesCommandTest.php | 5 +++
.../Command/ProcessQueueCommandTest.php | 21 ++++++---
.../Command/SendTestEmailCommandTest.php | 11 +++--
9 files changed, 122 insertions(+), 27 deletions(-)
diff --git a/resources/translations/messages.en.xlf b/resources/translations/messages.en.xlf
index e104fab1..f878d3ec 100644
--- a/resources/translations/messages.en.xlf
+++ b/resources/translations/messages.en.xlf
@@ -45,11 +45,56 @@
Another bounce processing is already running. Aborting.
+
+ Queue is already being processed by another instance.
+ Queue is already being processed by another instance.
+
+
+
+ The system is in maintenance mode, stopping. Try again later.
+ The system is in maintenance mode, stopping. Try again later.
+
+
Bounce processing completed.
Bounce processing completed.
+
+ Recipient email address not provided
+ Recipient email address not provided
+
+
+
+ Invalid email address: %email%
+ Invalid email address: %email%
+
+
+
+ Sending test email synchronously to %email%
+ Sending test email synchronously to %email%
+
+
+
+ Queuing test email for %email%
+ Queuing test email for %email%
+
+
+
+ Test email sent successfully!
+ Test email sent successfully!
+
+
+
+ Test email queued successfully! It will be sent asynchronously.
+ Test email queued successfully! It will be sent asynchronously.
+
+
+
+ Failed to send test email: %error%
+ Failed to send test email: %error%
+
+
Subscriber list not found.
diff --git a/src/Domain/Common/Repository/CursorPaginationTrait.php b/src/Domain/Common/Repository/CursorPaginationTrait.php
index be5164fb..3cf67a72 100644
--- a/src/Domain/Common/Repository/CursorPaginationTrait.php
+++ b/src/Domain/Common/Repository/CursorPaginationTrait.php
@@ -7,7 +7,6 @@
use BadMethodCallException;
use PhpList\Core\Domain\Common\Model\Filter\FilterRequestInterface;
use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel;
-use RuntimeException;
trait CursorPaginationTrait
{
@@ -31,8 +30,8 @@ public function getAfterId(int $lastId, int $limit): array
* Get filtered + paginated messages for a given owner and status.
*
* @return DomainModel[]
- * @throws RuntimeException
- */
+ * @throws BadMethodCallException
+ * */
public function getFilteredAfterId(int $lastId, int $limit, ?FilterRequestInterface $filter = null): array
{
if ($filter === null) {
diff --git a/src/Domain/Messaging/Command/ProcessQueueCommand.php b/src/Domain/Messaging/Command/ProcessQueueCommand.php
index d2c7cbfa..b9a9068a 100644
--- a/src/Domain/Messaging/Command/ProcessQueueCommand.php
+++ b/src/Domain/Messaging/Command/ProcessQueueCommand.php
@@ -15,6 +15,7 @@
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Lock\LockFactory;
+use Symfony\Contracts\Translation\TranslatorInterface;
use Throwable;
#[AsCommand(
@@ -28,13 +29,15 @@ class ProcessQueueCommand extends Command
private MessageProcessingPreparator $messagePreparator;
private CampaignProcessor $campaignProcessor;
private ConfigManager $configManager;
+ private TranslatorInterface $translator;
public function __construct(
MessageRepository $messageRepository,
LockFactory $lockFactory,
MessageProcessingPreparator $messagePreparator,
CampaignProcessor $campaignProcessor,
- ConfigManager $configManager
+ ConfigManager $configManager,
+ TranslatorInterface $translator
) {
parent::__construct();
$this->messageRepository = $messageRepository;
@@ -42,6 +45,7 @@ public function __construct(
$this->messagePreparator = $messagePreparator;
$this->campaignProcessor = $campaignProcessor;
$this->configManager = $configManager;
+ $this->translator = $translator;
}
/**
@@ -51,13 +55,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int
{
$lock = $this->lockFactory->createLock('queue_processor');
if (!$lock->acquire()) {
- $output->writeln('Queue is already being processed by another instance.');
+ $output->writeln($this->translator->trans('Queue is already being processed by another instance.'));
return Command::FAILURE;
}
if ($this->configManager->inMaintenanceMode()) {
- $output->writeln('The system is in maintenance mode, stopping. Try again later.');
+ $output->writeln(
+ $this->translator->trans('The system is in maintenance mode, stopping. Try again later.')
+ );
return Command::FAILURE;
}
diff --git a/src/Domain/Messaging/Command/SendTestEmailCommand.php b/src/Domain/Messaging/Command/SendTestEmailCommand.php
index bb6ba06b..e9670239 100644
--- a/src/Domain/Messaging/Command/SendTestEmailCommand.php
+++ b/src/Domain/Messaging/Command/SendTestEmailCommand.php
@@ -12,6 +12,7 @@
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Mime\Address;
+use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\Mime\Email;
#[AsCommand(
@@ -21,11 +22,13 @@
class SendTestEmailCommand extends Command
{
private EmailService $emailService;
+ private TranslatorInterface $translator;
- public function __construct(EmailService $emailService)
+ public function __construct(EmailService $emailService, TranslatorInterface $translator)
{
parent::__construct();
$this->emailService = $emailService;
+ $this->translator = $translator;
}
protected function configure(): void
@@ -48,13 +51,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int
{
$recipient = $input->getArgument('recipient');
if (!$recipient) {
- $output->writeln('Recipient email address not provided');
+ $output->writeln($this->translator->trans('Recipient email address not provided'));
return Command::FAILURE;
}
if (!filter_var($recipient, FILTER_VALIDATE_EMAIL)) {
- $output->writeln('Invalid email address: ' . $recipient);
+ $output->writeln($this->translator->trans('Invalid email address: %email%', ['%email%' => $recipient]));
return Command::FAILURE;
}
@@ -63,9 +66,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$syncMode = $input->getOption('sync');
if ($syncMode) {
- $output->writeln('Sending test email synchronously to ' . $recipient);
+ $output->writeln($this->translator->trans(
+ 'Sending test email synchronously to %email%',
+ ['%email%' => $recipient]
+ ));
} else {
- $output->writeln('Queuing test email for ' . $recipient);
+ $output->writeln($this->translator->trans(
+ 'Queuing test email for %email%',
+ ['%email%' => $recipient]
+ ));
}
$email = (new Email())
@@ -77,15 +86,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int
if ($syncMode) {
$this->emailService->sendEmailSync($email);
- $output->writeln('Test email sent successfully!');
+ $output->writeln($this->translator->trans('Test email sent successfully!'));
} else {
$this->emailService->sendEmail($email);
- $output->writeln('Test email queued successfully! It will be sent asynchronously.');
+ $output->writeln($this->translator->trans(
+ 'Test email queued successfully! It will be sent asynchronously.'
+ ));
}
return Command::SUCCESS;
} catch (Exception $e) {
- $output->writeln('Failed to send test email: ' . $e->getMessage());
+ $output->writeln($this->translator->trans(
+ 'Failed to send test email: %error%',
+ ['%error%' => $e->getMessage()]
+ ));
return Command::FAILURE;
}
diff --git a/tests/Unit/Domain/Common/Repository/CursorPaginationTraitTest.php b/tests/Unit/Domain/Common/Repository/CursorPaginationTraitTest.php
index 137da779..02cd5c40 100644
--- a/tests/Unit/Domain/Common/Repository/CursorPaginationTraitTest.php
+++ b/tests/Unit/Domain/Common/Repository/CursorPaginationTraitTest.php
@@ -4,12 +4,12 @@
namespace PhpList\Core\Tests\Unit\Domain\Common\Repository;
+use BadMethodCallException;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use PhpList\Core\Domain\Common\Model\Filter\FilterRequestInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
-use RuntimeException;
final class CursorPaginationTraitTest extends TestCase
{
@@ -59,8 +59,8 @@ public function testGetFilteredAfterIdWithFilterThrows(): void
{
$dummyFilter = $this->createMock(FilterRequestInterface::class);
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Filter method not implemented');
+ $this->expectException(BadMethodCallException::class);
+ $this->expectExceptionMessage('getFilteredAfterId method not implemented');
$this->repo->getFilteredAfterId(0, 10, $dummyFilter);
}
diff --git a/tests/Unit/Domain/Identity/Service/AdminAttributeDefinitionManagerTest.php b/tests/Unit/Domain/Identity/Service/AdminAttributeDefinitionManagerTest.php
index e42aba74..1a4deef4 100644
--- a/tests/Unit/Domain/Identity/Service/AdminAttributeDefinitionManagerTest.php
+++ b/tests/Unit/Domain/Identity/Service/AdminAttributeDefinitionManagerTest.php
@@ -12,17 +12,24 @@
use PhpList\Core\Domain\Subscription\Validator\AttributeTypeValidator;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+use Symfony\Contracts\Translation\TranslatorInterface;
class AdminAttributeDefinitionManagerTest extends TestCase
{
private AdminAttributeDefinitionRepository&MockObject $repository;
private AdminAttributeDefinitionManager $subject;
+ private TranslatorInterface&MockObject $translator;
protected function setUp(): void
{
$this->repository = $this->createMock(AdminAttributeDefinitionRepository::class);
$attributeTypeValidator = $this->createMock(AttributeTypeValidator::class);
- $this->subject = new AdminAttributeDefinitionManager($this->repository, $attributeTypeValidator);
+ $this->translator = $this->createMock(TranslatorInterface::class);
+ $this->subject = new AdminAttributeDefinitionManager(
+ definitionRepository: $this->repository,
+ attributeTypeValidator: $attributeTypeValidator,
+ translator: $this->translator,
+ );
}
public function testCreateCreatesNewAttributeDefinition(): void
@@ -76,6 +83,11 @@ public function testCreateThrowsExceptionIfAttributeAlreadyExists(): void
->with('test-attribute')
->willReturn($existingAttribute);
+ $this->translator->expects($this->once())
+ ->method('trans')
+ ->with('Attribute definition already exists.')
+ ->willReturn('Attribute definition already exists.');
+
$this->expectException(AttributeDefinitionCreationException::class);
$this->expectExceptionMessage('Attribute definition already exists');
diff --git a/tests/Unit/Domain/Messaging/Command/ProcessBouncesCommandTest.php b/tests/Unit/Domain/Messaging/Command/ProcessBouncesCommandTest.php
index 50cce9fa..3e8e24b6 100644
--- a/tests/Unit/Domain/Messaging/Command/ProcessBouncesCommandTest.php
+++ b/tests/Unit/Domain/Messaging/Command/ProcessBouncesCommandTest.php
@@ -15,6 +15,8 @@
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Tester\CommandTester;
+use Symfony\Component\Translation\Translator;
+use Symfony\Contracts\Translation\TranslatorInterface;
class ProcessBouncesCommandTest extends TestCase
{
@@ -26,6 +28,7 @@ class ProcessBouncesCommandTest extends TestCase
private ConsecutiveBounceHandler&MockObject $consecutiveBounceHandler;
private CommandTester $commandTester;
+ private TranslatorInterface|MockObject $translator;
protected function setUp(): void
{
@@ -35,6 +38,7 @@ protected function setUp(): void
$this->advancedRulesProcessor = $this->createMock(AdvancedBounceRulesProcessor::class);
$this->unidentifiedReprocessor = $this->createMock(UnidentifiedBounceReprocessor::class);
$this->consecutiveBounceHandler = $this->createMock(ConsecutiveBounceHandler::class);
+ $this->translator = new Translator('en');
$command = new ProcessBouncesCommand(
lockService: $this->lockService,
@@ -43,6 +47,7 @@ protected function setUp(): void
advancedRulesProcessor: $this->advancedRulesProcessor,
unidentifiedReprocessor: $this->unidentifiedReprocessor,
consecutiveBounceHandler: $this->consecutiveBounceHandler,
+ translator: $this->translator,
);
$this->commandTester = new CommandTester($command);
diff --git a/tests/Unit/Domain/Messaging/Command/ProcessQueueCommandTest.php b/tests/Unit/Domain/Messaging/Command/ProcessQueueCommandTest.php
index d76f63c0..d8e837ba 100644
--- a/tests/Unit/Domain/Messaging/Command/ProcessQueueCommandTest.php
+++ b/tests/Unit/Domain/Messaging/Command/ProcessQueueCommandTest.php
@@ -17,6 +17,7 @@
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\LockInterface;
+use Symfony\Component\Translation\Translator;
class ProcessQueueCommandTest extends TestCase
{
@@ -25,6 +26,7 @@ class ProcessQueueCommandTest extends TestCase
private CampaignProcessor&MockObject $campaignProcessor;
private LockInterface&MockObject $lock;
private CommandTester $commandTester;
+ private Translator&MockObject $translator;
protected function setUp(): void
{
@@ -33,17 +35,19 @@ protected function setUp(): void
$this->messageProcessingPreparator = $this->createMock(MessageProcessingPreparator::class);
$this->campaignProcessor = $this->createMock(CampaignProcessor::class);
$this->lock = $this->createMock(LockInterface::class);
+ $this->translator = $this->createMock(Translator::class);
$lockFactory->method('createLock')
->with('queue_processor')
->willReturn($this->lock);
$command = new ProcessQueueCommand(
- $this->messageRepository,
- $lockFactory,
- $this->messageProcessingPreparator,
- $this->campaignProcessor,
- $this->createMock(ConfigManager::class),
+ messageRepository: $this->messageRepository,
+ lockFactory: $lockFactory,
+ messagePreparator: $this->messageProcessingPreparator,
+ campaignProcessor: $this->campaignProcessor,
+ configManager: $this->createMock(ConfigManager::class),
+ translator: $this->translator,
);
$application = new Application();
@@ -61,10 +65,15 @@ public function testExecuteWithLockAlreadyAcquired(): void
$this->messageProcessingPreparator->expects($this->never())
->method('ensureSubscribersHaveUuid');
+ $this->translator->expects($this->once())
+ ->method('trans')
+ ->with('Queue is already being processed by another instance.')
+ ->willReturn('Queue is already being processed by another instance.');
+
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
- $this->assertStringContainsString('Queue is already being processed by another instance', $output);
+ $this->assertStringContainsString('Queue is already being processed by another instance.', $output);
$this->assertEquals(1, $this->commandTester->getStatusCode());
}
diff --git a/tests/Unit/Domain/Messaging/Command/SendTestEmailCommandTest.php b/tests/Unit/Domain/Messaging/Command/SendTestEmailCommandTest.php
index c1b4a92c..4e8bae26 100644
--- a/tests/Unit/Domain/Messaging/Command/SendTestEmailCommandTest.php
+++ b/tests/Unit/Domain/Messaging/Command/SendTestEmailCommandTest.php
@@ -4,6 +4,7 @@
namespace PhpList\Core\Tests\Unit\Domain\Messaging\Command;
+use Exception;
use PhpList\Core\Domain\Messaging\Command\SendTestEmailCommand;
use PhpList\Core\Domain\Messaging\Service\EmailService;
use PHPUnit\Framework\MockObject\MockObject;
@@ -11,16 +12,20 @@
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Mime\Email;
+use Symfony\Component\Translation\Translator;
+use Symfony\Contracts\Translation\TranslatorInterface;
class SendTestEmailCommandTest extends TestCase
{
private EmailService&MockObject $emailService;
private CommandTester $commandTester;
+ private TranslatorInterface $translator;
protected function setUp(): void
{
$this->emailService = $this->createMock(EmailService::class);
- $command = new SendTestEmailCommand($this->emailService);
+ $this->translator = new Translator('en');
+ $command = new SendTestEmailCommand($this->emailService, $this->translator);
$application = new Application();
$application->add($command);
@@ -165,7 +170,7 @@ public function testExecuteWithEmailServiceException(): void
{
$this->emailService->expects($this->once())
->method('sendEmail')
- ->willThrowException(new \Exception('Test exception'));
+ ->willThrowException(new Exception('Test exception'));
$this->commandTester->execute([
'recipient' => 'test@example.com',
@@ -182,7 +187,7 @@ public function testExecuteWithEmailServiceExceptionSync(): void
{
$this->emailService->expects($this->once())
->method('sendEmailSync')
- ->willThrowException(new \Exception('Test sync exception'));
+ ->willThrowException(new Exception('Test sync exception'));
$this->commandTester->execute([
'recipient' => 'test@example.com',
From 08d393efd003c7fe015bb6f00fb790b0d59dca87 Mon Sep 17 00:00:00 2001
From: Tatevik
Date: Fri, 19 Sep 2025 11:40:47 +0400
Subject: [PATCH 06/17] Translate PasswordResetMessageHandler texts
---
resources/translations/messages.en.xlf | 49 +++++++++++++++++++
.../PasswordResetMessageHandler.php | 49 +++++++++++++------
.../PasswordResetMessageHandlerTest.php | 7 ++-
3 files changed, 90 insertions(+), 15 deletions(-)
diff --git a/resources/translations/messages.en.xlf b/resources/translations/messages.en.xlf
index f878d3ec..83a61fdd 100644
--- a/resources/translations/messages.en.xlf
+++ b/resources/translations/messages.en.xlf
@@ -29,6 +29,55 @@
Attribute definition already exists.
+
+ Password Reset Request
+
+
+
+ Hello,
+
+ A password reset has been requested for your account.
+ Please use the following token to reset your password:
+
+ %token%
+
+ If you did not request this password reset, please ignore this email.
+
+ Thank you.
+
+
+ Hello,
+
+ A password reset has been requested for your account.
+ Please use the following token to reset your password:
+
+ %token%
+
+ If you did not request this password reset, please ignore this email.
+
+ Thank you.
+
+
+
+
+ Password Reset Request!
+Hello! A password reset has been requested for your account.
+Please use the following token to reset your password:
+Reset Password
+If you did not request this password reset, please ignore this email.
+Thank you.
]]>
+
+ Password Reset Request!
+ Hello! A password reset has been requested for your account.
+ Please use the following token to reset your password:
+ Reset Password
+ If you did not request this password reset, please ignore this email.
+ Thank you.
+ ]]>
+
+
+
PHP IMAP extension not available. Falling back to Webklex IMAP.
diff --git a/src/Domain/Messaging/MessageHandler/PasswordResetMessageHandler.php b/src/Domain/Messaging/MessageHandler/PasswordResetMessageHandler.php
index acd0e22d..f011d7a4 100644
--- a/src/Domain/Messaging/MessageHandler/PasswordResetMessageHandler.php
+++ b/src/Domain/Messaging/MessageHandler/PasswordResetMessageHandler.php
@@ -8,16 +8,19 @@
use PhpList\Core\Domain\Messaging\Service\EmailService;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Mime\Email;
+use Symfony\Contracts\Translation\TranslatorInterface;
#[AsMessageHandler]
class PasswordResetMessageHandler
{
private EmailService $emailService;
+ private TranslatorInterface $translator;
private string $passwordResetUrl;
- public function __construct(EmailService $emailService, string $passwordResetUrl)
+ public function __construct(EmailService $emailService, TranslatorInterface $translator, string $passwordResetUrl)
{
$this->emailService = $emailService;
+ $this->translator = $translator;
$this->passwordResetUrl = $passwordResetUrl;
}
@@ -28,19 +31,37 @@ public function __invoke(PasswordResetMessage $message): void
{
$confirmationLink = $this->generateLink($message->getToken());
- $subject = 'Password Reset Request';
- $textContent = "Hello,\n\n"
- . "A password reset has been requested for your account.\n"
- . "Please use the following token to reset your password:\n\n"
- . $message->getToken()
- . "\n\nIf you did not request this password reset, please ignore this email.\n\nThank you.";
-
- $htmlContent = 'Password Reset Request!
'
- . 'Hello! A password reset has been requested for your account.
'
- . 'Please use the following token to reset your password:
'
- . 'Reset Password
'
- . 'If you did not request this password reset, please ignore this email.
'
- . 'Thank you.
';
+ $subject = $this->translator->trans('Password Reset Request');
+ $textContent = $this->translator->trans(
+ << $message->getToken()]
+ );
+
+ $htmlContent = $this->translator->trans(
+ <<Password Reset Request!
+Hello! A password reset has been requested for your account.
+Please use the following token to reset your password:
+Reset Password
+If you did not request this password reset, please ignore this email.
+Thank you.
+HTML,
+ [
+ '%token%' => $message->getToken(),
+ '%confirmationLink%' => $confirmationLink,
+ ]
+ );
$email = (new Email())
->to($message->getEmail())
diff --git a/tests/Unit/Domain/Messaging/MessageHandler/PasswordResetMessageHandlerTest.php b/tests/Unit/Domain/Messaging/MessageHandler/PasswordResetMessageHandlerTest.php
index ae7aa184..22f83bfc 100644
--- a/tests/Unit/Domain/Messaging/MessageHandler/PasswordResetMessageHandlerTest.php
+++ b/tests/Unit/Domain/Messaging/MessageHandler/PasswordResetMessageHandlerTest.php
@@ -10,6 +10,7 @@
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Email;
+use Symfony\Component\Translation\Translator;
class PasswordResetMessageHandlerTest extends TestCase
{
@@ -20,7 +21,11 @@ class PasswordResetMessageHandlerTest extends TestCase
protected function setUp(): void
{
$this->emailService = $this->createMock(EmailService::class);
- $this->handler = new PasswordResetMessageHandler($this->emailService, $this->passwordResetUrl);
+ $this->handler = new PasswordResetMessageHandler(
+ $this->emailService,
+ new Translator('en'),
+ $this->passwordResetUrl
+ );
}
public function testInvoke(): void
From 5627a037a2f1599cf8c2854e0bbb96477f5e7451 Mon Sep 17 00:00:00 2001
From: Tatevik
Date: Fri, 19 Sep 2025 11:55:00 +0400
Subject: [PATCH 07/17] Translate SubscriberConfirmationMessageHandler texts
---
resources/translations/messages.en.xlf | 37 +++++++++++++++++
.../SubscriberConfirmationMessageHandler.php | 41 ++++++++++++++-----
...bscriberConfirmationMessageHandlerTest.php | 7 +++-
3 files changed, 74 insertions(+), 11 deletions(-)
diff --git a/resources/translations/messages.en.xlf b/resources/translations/messages.en.xlf
index 83a61fdd..90a998f7 100644
--- a/resources/translations/messages.en.xlf
+++ b/resources/translations/messages.en.xlf
@@ -78,6 +78,43 @@
+
+ Please confirm your subscription
+ Please confirm your subscription
+
+
+
+ Thank you for subscribing!
+
+ Please confirm your subscription by clicking the link below:
+
+ %confirmationLink%
+
+ If you did not request this subscription, please ignore this email.
+
+ Thank you for subscribing!
+
+ Please confirm your subscription by clicking the link below:
+
+ %confirmationLink%
+
+ If you did not request this subscription, please ignore this email.
+
+
+
+
+ Thank you for subscribing!
+Please confirm your subscription by clicking the link below:
+Confirm Subscription
+If you did not request this subscription, please ignore this email.
]]>
+
+ Thank you for subscribing!
+Please confirm your subscription by clicking the link below:
+Confirm Subscription
+If you did not request this subscription, please ignore this email.
]]>
+
+
+
PHP IMAP extension not available. Falling back to Webklex IMAP.
diff --git a/src/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandler.php b/src/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandler.php
index 8c487849..dc31a63d 100644
--- a/src/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandler.php
+++ b/src/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandler.php
@@ -8,6 +8,7 @@
use PhpList\Core\Domain\Messaging\Service\EmailService;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Mime\Email;
+use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Handler for processing asynchronous subscriber confirmation email messages
@@ -16,11 +17,13 @@
class SubscriberConfirmationMessageHandler
{
private EmailService $emailService;
+ private TranslatorInterface $translator;
private string $confirmationUrl;
- public function __construct(EmailService $emailService, string $confirmationUrl)
+ public function __construct(EmailService $emailService, TranslatorInterface $translator, string $confirmationUrl)
{
$this->emailService = $emailService;
+ $this->translator = $translator;
$this->confirmationUrl = $confirmationUrl;
}
@@ -31,18 +34,36 @@ public function __invoke(SubscriberConfirmationMessage $message): void
{
$confirmationLink = $this->generateConfirmationLink($message->getUniqueId());
- $subject = 'Please confirm your subscription';
- $textContent = "Thank you for subscribing!\n\n"
- . "Please confirm your subscription by clicking the link below:\n"
- . $confirmationLink . "\n\n"
- . 'If you did not request this subscription, please ignore this email.';
+ $subject = $this->translator->trans('Please confirm your subscription');
+
+ $textContent = $this->translator->trans(
+ << $confirmationLink
+ ]
+ );
$htmlContent = '';
if ($message->hasHtmlEmail()) {
- $htmlContent = 'Thank you for subscribing!
'
- . 'Please confirm your subscription by clicking the link below:
'
- . 'Confirm Subscription
'
- . 'If you did not request this subscription, please ignore this email.
';
+ $htmlContent = $this->translator->trans(
+ <<Thank you for subscribing!
+Please confirm your subscription by clicking the link below:
+Confirm Subscription
+If you did not request this subscription, please ignore this email.
+HTML,
+ [
+ '%confirmationLink%' => $confirmationLink,
+ ]
+ );
}
$email = (new Email())
diff --git a/tests/Unit/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandlerTest.php b/tests/Unit/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandlerTest.php
index 4bd89243..550a6160 100644
--- a/tests/Unit/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandlerTest.php
+++ b/tests/Unit/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandlerTest.php
@@ -10,6 +10,7 @@
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Email;
+use Symfony\Component\Translation\Translator;
class SubscriberConfirmationMessageHandlerTest extends TestCase
{
@@ -20,7 +21,11 @@ class SubscriberConfirmationMessageHandlerTest extends TestCase
protected function setUp(): void
{
$this->emailService = $this->createMock(EmailService::class);
- $this->handler = new SubscriberConfirmationMessageHandler($this->emailService, $this->confirmationUrl);
+ $this->handler = new SubscriberConfirmationMessageHandler(
+ emailService: $this->emailService,
+ translator: new Translator('en'),
+ confirmationUrl: $this->confirmationUrl
+ );
}
public function testInvokeWithTextEmail(): void
From 8a50af02be1e5f2927e22f3692404dab34f55b9b Mon Sep 17 00:00:00 2001
From: Tatevik
Date: Fri, 19 Sep 2025 12:07:47 +0400
Subject: [PATCH 08/17] MessageBuilder exceptions
---
resources/translations/messages.en.xlf | 12 ++++++------
.../Exception/InvalidContextTypeException.php | 15 +++++++++++++++
.../Exception/InvalidDtoTypeException.php | 15 +++++++++++++++
.../PasswordResetMessageHandler.php | 5 ++---
.../SubscriberConfirmationMessageHandler.php | 8 ++++----
.../Messaging/Service/Builder/MessageBuilder.php | 14 +++++++++++---
.../Service/Builder/MessageContentBuilder.php | 12 ++++++------
.../Service/Builder/MessageFormatBuilder.php | 10 +++++-----
.../Service/Builder/MessageOptionsBuilder.php | 14 +++++++-------
.../Service/Builder/MessageScheduleBuilder.php | 14 +++++++-------
.../BlacklistEmailAndDeleteBounceHandler.php | 4 ++--
11 files changed, 80 insertions(+), 43 deletions(-)
create mode 100644 src/Domain/Messaging/Exception/InvalidContextTypeException.php
create mode 100644 src/Domain/Messaging/Exception/InvalidDtoTypeException.php
diff --git a/resources/translations/messages.en.xlf b/resources/translations/messages.en.xlf
index 90a998f7..2fe0d696 100644
--- a/resources/translations/messages.en.xlf
+++ b/resources/translations/messages.en.xlf
@@ -63,7 +63,7 @@
Password Reset Request!
Hello! A password reset has been requested for your account.
Please use the following token to reset your password:
-Reset Password
+Reset Password
If you did not request this password reset, please ignore this email.
Thank you.
]]>
@@ -71,7 +71,7 @@
Password Reset Request!
Hello! A password reset has been requested for your account.
Please use the following token to reset your password:
- Reset Password
+ Reset Password
If you did not request this password reset, please ignore this email.
Thank you.
]]>
@@ -88,7 +88,7 @@
Please confirm your subscription by clicking the link below:
- %confirmationLink%
+ %confirmation_link%
If you did not request this subscription, please ignore this email.
@@ -96,7 +96,7 @@
Please confirm your subscription by clicking the link below:
- %confirmationLink%
+ %confirmation_link%
If you did not request this subscription, please ignore this email.
@@ -105,12 +105,12 @@
Thank you for subscribing!
Please confirm your subscription by clicking the link below:
-Confirm Subscription
+Confirm Subscription
If you did not request this subscription, please ignore this email.
]]>
Thank you for subscribing!
Please confirm your subscription by clicking the link below:
-Confirm Subscription
+Confirm Subscription
If you did not request this subscription, please ignore this email.
]]>
diff --git a/src/Domain/Messaging/Exception/InvalidContextTypeException.php b/src/Domain/Messaging/Exception/InvalidContextTypeException.php
new file mode 100644
index 00000000..d21d8c8f
--- /dev/null
+++ b/src/Domain/Messaging/Exception/InvalidContextTypeException.php
@@ -0,0 +1,15 @@
+Password Reset Request!
Hello! A password reset has been requested for your account.
Please use the following token to reset your password:
-Reset Password
+Reset Password
If you did not request this password reset, please ignore this email.
Thank you.
HTML,
[
- '%token%' => $message->getToken(),
- '%confirmationLink%' => $confirmationLink,
+ '%confirmation_link%' => $confirmationLink,
]
);
diff --git a/src/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandler.php b/src/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandler.php
index dc31a63d..20a70ba5 100644
--- a/src/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandler.php
+++ b/src/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandler.php
@@ -42,12 +42,12 @@ public function __invoke(SubscriberConfirmationMessage $message): void
Please confirm your subscription by clicking the link below:
- %confirmationLink%
+ %confirmation_link%
If you did not request this subscription, please ignore this email.
TXT,
[
- '%confirmationLink%' => $confirmationLink
+ '%confirmation_link%' => $confirmationLink
]
);
@@ -57,11 +57,11 @@ public function __invoke(SubscriberConfirmationMessage $message): void
<<Thank you for subscribing!
Please confirm your subscription by clicking the link below:
-Confirm Subscription
+Confirm Subscription
If you did not request this subscription, please ignore this email.
HTML,
[
- '%confirmationLink%' => $confirmationLink,
+ '%confirmation_link%' => $confirmationLink,
]
);
}
diff --git a/src/Domain/Messaging/Service/Builder/MessageBuilder.php b/src/Domain/Messaging/Service/Builder/MessageBuilder.php
index 85e74331..bb7fd852 100644
--- a/src/Domain/Messaging/Service/Builder/MessageBuilder.php
+++ b/src/Domain/Messaging/Service/Builder/MessageBuilder.php
@@ -4,7 +4,7 @@
namespace PhpList\Core\Domain\Messaging\Service\Builder;
-use InvalidArgumentException;
+use PhpList\Core\Domain\Messaging\Exception\InvalidContextTypeException;
use PhpList\Core\Domain\Messaging\Model\Dto\MessageContext;
use PhpList\Core\Domain\Messaging\Model\Dto\MessageDtoInterface;
use PhpList\Core\Domain\Messaging\Model\Message;
@@ -24,7 +24,7 @@ public function __construct(
public function build(MessageDtoInterface $createMessageDto, object $context = null): Message
{
if (!$context instanceof MessageContext) {
- throw new InvalidArgumentException('Invalid context type');
+ throw new InvalidContextTypeException(get_debug_type($context));
}
$format = $this->messageFormatBuilder->build($createMessageDto->getFormat());
@@ -47,6 +47,14 @@ public function build(MessageDtoInterface $createMessageDto, object $context = n
$metadata = new Message\MessageMetadata(Message\MessageStatus::Draft);
- return new Message($format, $schedule, $metadata, $content, $options, $context->getOwner(), $template);
+ return new Message(
+ format: $format,
+ schedule: $schedule,
+ metadata: $metadata,
+ content: $content,
+ options: $options,
+ owner: $context->getOwner(),
+ template: $template
+ );
}
}
diff --git a/src/Domain/Messaging/Service/Builder/MessageContentBuilder.php b/src/Domain/Messaging/Service/Builder/MessageContentBuilder.php
index 1e9e442d..806afe00 100644
--- a/src/Domain/Messaging/Service/Builder/MessageContentBuilder.php
+++ b/src/Domain/Messaging/Service/Builder/MessageContentBuilder.php
@@ -4,7 +4,7 @@
namespace PhpList\Core\Domain\Messaging\Service\Builder;
-use InvalidArgumentException;
+use PhpList\Core\Domain\Messaging\Exception\InvalidDtoTypeException;
use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageContentDto;
use PhpList\Core\Domain\Messaging\Model\Message\MessageContent;
@@ -13,14 +13,14 @@ class MessageContentBuilder
public function build(object $dto): MessageContent
{
if (!$dto instanceof MessageContentDto) {
- throw new InvalidArgumentException('Invalid request dto type: ' . get_class($dto));
+ throw new InvalidDtoTypeException(get_debug_type($dto));
}
return new MessageContent(
- $dto->subject,
- $dto->text,
- $dto->textMessage,
- $dto->footer
+ subject: $dto->subject,
+ text: $dto->text,
+ textMessage: $dto->textMessage,
+ footer: $dto->footer
);
}
}
diff --git a/src/Domain/Messaging/Service/Builder/MessageFormatBuilder.php b/src/Domain/Messaging/Service/Builder/MessageFormatBuilder.php
index 7bf9be8b..c6b05fc2 100644
--- a/src/Domain/Messaging/Service/Builder/MessageFormatBuilder.php
+++ b/src/Domain/Messaging/Service/Builder/MessageFormatBuilder.php
@@ -4,7 +4,7 @@
namespace PhpList\Core\Domain\Messaging\Service\Builder;
-use InvalidArgumentException;
+use PhpList\Core\Domain\Messaging\Exception\InvalidDtoTypeException;
use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageFormatDto;
use PhpList\Core\Domain\Messaging\Model\Message\MessageFormat;
@@ -13,13 +13,13 @@ class MessageFormatBuilder
public function build(object $dto): MessageFormat
{
if (!$dto instanceof MessageFormatDto) {
- throw new InvalidArgumentException('Invalid request dto type: ' . get_class($dto));
+ throw new InvalidDtoTypeException(get_debug_type($dto));
}
return new MessageFormat(
- $dto->htmlFormated,
- $dto->sendFormat,
- $dto->formatOptions
+ htmlFormatted: $dto->htmlFormated,
+ sendFormat: $dto->sendFormat,
+ formatOptions: $dto->formatOptions
);
}
}
diff --git a/src/Domain/Messaging/Service/Builder/MessageOptionsBuilder.php b/src/Domain/Messaging/Service/Builder/MessageOptionsBuilder.php
index 0a241f0f..91689d1e 100644
--- a/src/Domain/Messaging/Service/Builder/MessageOptionsBuilder.php
+++ b/src/Domain/Messaging/Service/Builder/MessageOptionsBuilder.php
@@ -4,7 +4,7 @@
namespace PhpList\Core\Domain\Messaging\Service\Builder;
-use InvalidArgumentException;
+use PhpList\Core\Domain\Messaging\Exception\InvalidDtoTypeException;
use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageOptionsDto;
use PhpList\Core\Domain\Messaging\Model\Message\MessageOptions;
@@ -13,15 +13,15 @@ class MessageOptionsBuilder
public function build(object $dto): MessageOptions
{
if (!$dto instanceof MessageOptionsDto) {
- throw new InvalidArgumentException('Invalid request dto type: ' . get_class($dto));
+ throw new InvalidDtoTypeException(get_debug_type($dto));
}
return new MessageOptions(
- $dto->fromField ?? '',
- $dto->toField ?? '',
- $dto->replyTo ?? '',
- $dto->userSelection,
- null,
+ fromField: $dto->fromField ?? '',
+ toField: $dto->toField ?? '',
+ replyTo: $dto->replyTo ?? '',
+ userSelection: $dto->userSelection,
+ rssTemplate: null,
);
}
}
diff --git a/src/Domain/Messaging/Service/Builder/MessageScheduleBuilder.php b/src/Domain/Messaging/Service/Builder/MessageScheduleBuilder.php
index df847eaf..dbe86731 100644
--- a/src/Domain/Messaging/Service/Builder/MessageScheduleBuilder.php
+++ b/src/Domain/Messaging/Service/Builder/MessageScheduleBuilder.php
@@ -5,7 +5,7 @@
namespace PhpList\Core\Domain\Messaging\Service\Builder;
use DateTime;
-use InvalidArgumentException;
+use PhpList\Core\Domain\Messaging\Exception\InvalidDtoTypeException;
use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageScheduleDto;
use PhpList\Core\Domain\Messaging\Model\Message\MessageSchedule;
@@ -14,15 +14,15 @@ class MessageScheduleBuilder
public function build(object $dto): MessageSchedule
{
if (!$dto instanceof MessageScheduleDto) {
- throw new InvalidArgumentException('Invalid request dto type: ' . get_class($dto));
+ throw new InvalidDtoTypeException(get_debug_type($dto));
}
return new MessageSchedule(
- $dto->repeatInterval,
- new DateTime($dto->repeatUntil),
- $dto->requeueInterval,
- new DateTime($dto->requeueUntil),
- new DateTime($dto->embargo)
+ repeatInterval: $dto->repeatInterval,
+ repeatUntil: new DateTime($dto->repeatUntil),
+ requeueInterval: $dto->requeueInterval,
+ requeueUntil: new DateTime($dto->requeueUntil),
+ embargo: new DateTime($dto->embargo)
);
}
}
diff --git a/src/Domain/Messaging/Service/Handler/BlacklistEmailAndDeleteBounceHandler.php b/src/Domain/Messaging/Service/Handler/BlacklistEmailAndDeleteBounceHandler.php
index d32cf68b..b0b086c3 100644
--- a/src/Domain/Messaging/Service/Handler/BlacklistEmailAndDeleteBounceHandler.php
+++ b/src/Domain/Messaging/Service/Handler/BlacklistEmailAndDeleteBounceHandler.php
@@ -34,12 +34,12 @@ public function handle(array $closureData): void
if (!empty($closureData['subscriber'])) {
$this->blacklistService->blacklist(
subscriber: $closureData['subscriber'],
- reason: 'Email address auto blacklisted by bounce rule '.$closureData['ruleId']
+ reason: 'Email address auto blacklisted by bounce rule ' . $closureData['ruleId']
);
$this->subscriberHistoryManager->addHistory(
$closureData['subscriber'],
'Auto Unsubscribed',
- 'User auto unsubscribed for bounce rule '.$closureData['ruleId']
+ 'User auto unsubscribed for bounce rule ' . $closureData['ruleId']
);
}
$this->bounceManager->delete($closureData['bounce']);
From 961e0ad2e230b27e7bed2d666de0e71bad57ca26 Mon Sep 17 00:00:00 2001
From: Tatevik
Date: Fri, 19 Sep 2025 12:37:45 +0400
Subject: [PATCH 09/17] BlacklistEmailAndDeleteBounceHandler
---
resources/translations/messages.en.xlf | 15 ++++++++++++++
.../BlacklistEmailAndDeleteBounceHandler.php | 20 ++++++++++++++-----
...acklistEmailAndDeleteBounceHandlerTest.php | 2 ++
3 files changed, 32 insertions(+), 5 deletions(-)
diff --git a/resources/translations/messages.en.xlf b/resources/translations/messages.en.xlf
index 2fe0d696..235712eb 100644
--- a/resources/translations/messages.en.xlf
+++ b/resources/translations/messages.en.xlf
@@ -115,6 +115,21 @@
+
+ Email address auto blacklisted by bounce rule %rule_id%
+ Email address auto blacklisted by bounce rule %rule_id%
+
+
+
+ Auto Unsubscribed
+ Auto Unsubscribed
+
+
+
+ User auto unsubscribed for bounce rule %rule_id%
+ User auto unsubscribed for bounce rule %rule_id%
+
+
PHP IMAP extension not available. Falling back to Webklex IMAP.
diff --git a/src/Domain/Messaging/Service/Handler/BlacklistEmailAndDeleteBounceHandler.php b/src/Domain/Messaging/Service/Handler/BlacklistEmailAndDeleteBounceHandler.php
index b0b086c3..e3b743cb 100644
--- a/src/Domain/Messaging/Service/Handler/BlacklistEmailAndDeleteBounceHandler.php
+++ b/src/Domain/Messaging/Service/Handler/BlacklistEmailAndDeleteBounceHandler.php
@@ -7,21 +7,25 @@
use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager;
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager;
use PhpList\Core\Domain\Subscription\Service\SubscriberBlacklistService;
+use Symfony\Contracts\Translation\TranslatorInterface;
class BlacklistEmailAndDeleteBounceHandler implements BounceActionHandlerInterface
{
private SubscriberHistoryManager $subscriberHistoryManager;
private BounceManager $bounceManager;
private SubscriberBlacklistService $blacklistService;
+ private TranslatorInterface $translator;
public function __construct(
SubscriberHistoryManager $subscriberHistoryManager,
BounceManager $bounceManager,
- SubscriberBlacklistService $blacklistService
+ SubscriberBlacklistService $blacklistService,
+ TranslatorInterface $translator,
) {
$this->subscriberHistoryManager = $subscriberHistoryManager;
$this->bounceManager = $bounceManager;
$this->blacklistService = $blacklistService;
+ $this->translator = $translator;
}
public function supports(string $action): bool
@@ -32,14 +36,20 @@ public function supports(string $action): bool
public function handle(array $closureData): void
{
if (!empty($closureData['subscriber'])) {
+ $reason = $this->translator->trans('Email address auto blacklisted by bounce rule %rule_id%', [
+ '%rule_id%' => $closureData['ruleId']
+ ]);
$this->blacklistService->blacklist(
subscriber: $closureData['subscriber'],
- reason: 'Email address auto blacklisted by bounce rule ' . $closureData['ruleId']
+ reason: $reason
);
+ $details = $this->translator->trans('User auto unsubscribed for bounce rule %rule_id%', [
+ '%rule_id%' => $closureData['ruleId']
+ ]);
$this->subscriberHistoryManager->addHistory(
- $closureData['subscriber'],
- 'Auto Unsubscribed',
- 'User auto unsubscribed for bounce rule ' . $closureData['ruleId']
+ subscriber: $closureData['subscriber'],
+ message: $this->translator->trans('Auto Unsubscribed'),
+ details: $details
);
}
$this->bounceManager->delete($closureData['bounce']);
diff --git a/tests/Unit/Domain/Messaging/Service/Handler/BlacklistEmailAndDeleteBounceHandlerTest.php b/tests/Unit/Domain/Messaging/Service/Handler/BlacklistEmailAndDeleteBounceHandlerTest.php
index 8f5cdb11..cc0ff38d 100644
--- a/tests/Unit/Domain/Messaging/Service/Handler/BlacklistEmailAndDeleteBounceHandlerTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Handler/BlacklistEmailAndDeleteBounceHandlerTest.php
@@ -12,6 +12,7 @@
use PhpList\Core\Domain\Subscription\Service\SubscriberBlacklistService;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Translation\Translator;
class BlacklistEmailAndDeleteBounceHandlerTest extends TestCase
{
@@ -29,6 +30,7 @@ protected function setUp(): void
subscriberHistoryManager: $this->historyManager,
bounceManager: $this->bounceManager,
blacklistService: $this->blacklistService,
+ translator: new Translator('en')
);
}
From fd4ef3567486ebdf83ef54fbb855195d29f0976d Mon Sep 17 00:00:00 2001
From: Tatevik
Date: Sat, 20 Sep 2025 14:44:08 +0400
Subject: [PATCH 10/17] BlacklistEmailHandler - AdvancedBounceRulesProcessor
---
resources/translations/messages.en.xlf | 104 +++++++++++++++---
.../Service/Handler/BlacklistEmailHandler.php | 16 ++-
.../BlacklistUserAndDeleteBounceHandler.php | 14 ++-
.../Service/Handler/BlacklistUserHandler.php | 14 ++-
...CountConfirmUserAndDeleteBounceHandler.php | 10 +-
.../Service/Handler/RequeueHandler.php | 10 +-
.../UnconfirmUserAndDeleteBounceHandler.php | 10 +-
.../Service/Handler/UnconfirmUserHandler.php | 12 +-
.../Service/Manager/BounceManager.php | 13 ++-
.../AdvancedBounceRulesProcessor.php | 24 ++--
.../Handler/BlacklistEmailHandlerTest.php | 2 +
...lacklistUserAndDeleteBounceHandlerTest.php | 2 +
.../Handler/BlacklistUserHandlerTest.php | 4 +-
...tConfirmUserAndDeleteBounceHandlerTest.php | 2 +
.../Service/Handler/RequeueHandlerTest.php | 11 +-
...nconfirmUserAndDeleteBounceHandlerTest.php | 2 +
.../Handler/UnconfirmUserHandlerTest.php | 6 +-
.../Service/Manager/BounceManagerTest.php | 2 +
.../AdvancedBounceRulesProcessorTest.php | 3 +
19 files changed, 204 insertions(+), 57 deletions(-)
diff --git a/resources/translations/messages.en.xlf b/resources/translations/messages.en.xlf
index 235712eb..d97e6af6 100644
--- a/resources/translations/messages.en.xlf
+++ b/resources/translations/messages.en.xlf
@@ -115,21 +115,6 @@
-
- Email address auto blacklisted by bounce rule %rule_id%
- Email address auto blacklisted by bounce rule %rule_id%
-
-
-
- Auto Unsubscribed
- Auto Unsubscribed
-
-
-
- User auto unsubscribed for bounce rule %rule_id%
- User auto unsubscribed for bounce rule %rule_id%
-
-
PHP IMAP extension not available. Falling back to Webklex IMAP.
@@ -196,6 +181,95 @@
Failed to send test email: %error%
+
+ Email address auto blacklisted by bounce rule %rule_id%
+ Email address auto blacklisted by bounce rule %rule_id%
+
+
+
+ Auto Unsubscribed
+ Auto Unsubscribed
+
+
+
+ User auto unsubscribed for bounce rule %rule_id%
+ User auto unsubscribed for bounce rule %rule_id%
+
+
+
+ email auto unsubscribed for bounce rule %rule_id%
+ email auto unsubscribed for bounce rule %rule_id%
+
+
+
+ Subscriber auto blacklisted by bounce rule %rule_id%
+ Subscriber auto blacklisted by bounce rule %rule_id%
+
+
+
+ User auto unsubscribed for bounce rule %%rule_id%
+ User auto unsubscribed for bounce rule %%rule_id%
+
+
+
+ Auto confirmed
+ Auto confirmed
+
+
+
+ Auto unconfirmed
+ Auto unconfirmed
+
+
+
+ Subscriber auto confirmed for bounce rule %rule_id%
+ Subscriber auto confirmed for bounce rule %rule_id%
+
+
+
+ Requeued campaign; next embargo at %time%
+ Requeued campaign; next embargo at %time%
+
+
+
+ Subscriber auto unconfirmed for bounce rule %rule_id%
+ Subscriber auto unconfirmed for bounce rule %rule_id%
+
+
+
+ Running in test mode, not deleting messages from mailbox
+ Running in test mode, not deleting messages from mailbox
+
+
+
+ Processed messages will be deleted from the mailbox
+ Processed messages will be deleted from the mailbox
+
+
+
+ Processing bounces based on active bounce rules
+ Processing bounces based on active bounce rules
+
+
+
+ No active rules
+ No active rules
+
+
+
+ Processed %processed% out of %total% bounces for advanced bounce rules
+ Processed %processed% out of %total% bounces for advanced bounce rules
+
+
+
+ %processed% bounces processed by advanced processing
+ %processed% bounces processed by advanced processing
+
+
+ %not_processed% bounces were not matched by advanced processing rules
+ %not_processed% bounces were not matched by advanced processing rules
+
+
Subscriber list not found.
diff --git a/src/Domain/Messaging/Service/Handler/BlacklistEmailHandler.php b/src/Domain/Messaging/Service/Handler/BlacklistEmailHandler.php
index 9a92088c..eac3b7a9 100644
--- a/src/Domain/Messaging/Service/Handler/BlacklistEmailHandler.php
+++ b/src/Domain/Messaging/Service/Handler/BlacklistEmailHandler.php
@@ -6,18 +6,22 @@
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager;
use PhpList\Core\Domain\Subscription\Service\SubscriberBlacklistService;
+use Symfony\Contracts\Translation\TranslatorInterface;
class BlacklistEmailHandler implements BounceActionHandlerInterface
{
private SubscriberHistoryManager $subscriberHistoryManager;
private SubscriberBlacklistService $blacklistService;
+ private TranslatorInterface $translator;
public function __construct(
SubscriberHistoryManager $subscriberHistoryManager,
SubscriberBlacklistService $blacklistService,
+ TranslatorInterface $translator,
) {
$this->subscriberHistoryManager = $subscriberHistoryManager;
$this->blacklistService = $blacklistService;
+ $this->translator = $translator;
}
public function supports(string $action): bool
@@ -29,13 +33,17 @@ public function handle(array $closureData): void
{
if (!empty($closureData['subscriber'])) {
$this->blacklistService->blacklist(
- $closureData['subscriber'],
- 'Email address auto blacklisted by bounce rule '.$closureData['ruleId']
+ subscriber: $closureData['subscriber'],
+ reason: $this->translator->trans('Email address auto blacklisted by bounce rule %rule_id%', [
+ '%rule_id%' => $closureData['ruleId']
+ ]),
);
$this->subscriberHistoryManager->addHistory(
$closureData['subscriber'],
- 'Auto Unsubscribed',
- 'email auto unsubscribed for bounce rule '.$closureData['ruleId']
+ $this->translator->trans('Auto Unsubscribed'),
+ $this->translator->trans('email auto unsubscribed for bounce rule %rule_id%', [
+ '%rule_id%' => $closureData['ruleId']
+ ])
);
}
}
diff --git a/src/Domain/Messaging/Service/Handler/BlacklistUserAndDeleteBounceHandler.php b/src/Domain/Messaging/Service/Handler/BlacklistUserAndDeleteBounceHandler.php
index b017fe9c..3fda46c2 100644
--- a/src/Domain/Messaging/Service/Handler/BlacklistUserAndDeleteBounceHandler.php
+++ b/src/Domain/Messaging/Service/Handler/BlacklistUserAndDeleteBounceHandler.php
@@ -7,21 +7,25 @@
use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager;
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager;
use PhpList\Core\Domain\Subscription\Service\SubscriberBlacklistService;
+use Symfony\Contracts\Translation\TranslatorInterface;
class BlacklistUserAndDeleteBounceHandler implements BounceActionHandlerInterface
{
private SubscriberHistoryManager $subscriberHistoryManager;
private BounceManager $bounceManager;
private SubscriberBlacklistService $blacklistService;
+ private TranslatorInterface $translator;
public function __construct(
SubscriberHistoryManager $subscriberHistoryManager,
BounceManager $bounceManager,
SubscriberBlacklistService $blacklistService,
+ TranslatorInterface $translator,
) {
$this->subscriberHistoryManager = $subscriberHistoryManager;
$this->bounceManager = $bounceManager;
$this->blacklistService = $blacklistService;
+ $this->translator = $translator;
}
public function supports(string $action): bool
@@ -34,12 +38,16 @@ public function handle(array $closureData): void
if (!empty($closureData['subscriber']) && !$closureData['blacklisted']) {
$this->blacklistService->blacklist(
subscriber: $closureData['subscriber'],
- reason: 'Subscriber auto blacklisted by bounce rule '.$closureData['ruleId']
+ reason: $this->translator->trans('Subscriber auto blacklisted by bounce rule %rule_id%', [
+ '%rule_id%' => $closureData['ruleId']
+ ])
);
$this->subscriberHistoryManager->addHistory(
subscriber: $closureData['subscriber'],
- message: 'Auto Unsubscribed',
- details: 'User auto unsubscribed for bounce rule '.$closureData['ruleId']
+ message: $this->translator->trans('Auto Unsubscribed'),
+ details: $this->translator->trans('User auto unsubscribed for bounce rule %rule_id%', [
+ '%rule_id%' => $closureData['ruleId']
+ ])
);
}
$this->bounceManager->delete($closureData['bounce']);
diff --git a/src/Domain/Messaging/Service/Handler/BlacklistUserHandler.php b/src/Domain/Messaging/Service/Handler/BlacklistUserHandler.php
index 75c8b810..555ad3bf 100644
--- a/src/Domain/Messaging/Service/Handler/BlacklistUserHandler.php
+++ b/src/Domain/Messaging/Service/Handler/BlacklistUserHandler.php
@@ -6,18 +6,22 @@
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager;
use PhpList\Core\Domain\Subscription\Service\SubscriberBlacklistService;
+use Symfony\Contracts\Translation\TranslatorInterface;
class BlacklistUserHandler implements BounceActionHandlerInterface
{
private SubscriberHistoryManager $subscriberHistoryManager;
private SubscriberBlacklistService $blacklistService;
+ private TranslatorInterface $translator;
public function __construct(
SubscriberHistoryManager $subscriberHistoryManager,
SubscriberBlacklistService $blacklistService,
+ TranslatorInterface $translator,
) {
$this->subscriberHistoryManager = $subscriberHistoryManager;
$this->blacklistService = $blacklistService;
+ $this->translator = $translator;
}
public function supports(string $action): bool
@@ -30,12 +34,16 @@ public function handle(array $closureData): void
if (!empty($closureData['subscriber']) && !$closureData['blacklisted']) {
$this->blacklistService->blacklist(
subscriber: $closureData['subscriber'],
- reason: 'Subscriber auto blacklisted by bounce rule '.$closureData['ruleId']
+ reason: $this->translator->trans('Subscriber auto blacklisted by bounce rule %rule_id%', [
+ '%rule_id%' => $closureData['ruleId']
+ ])
);
$this->subscriberHistoryManager->addHistory(
subscriber: $closureData['subscriber'],
- message: 'Auto Unsubscribed',
- details: 'User auto unsubscribed for bounce rule '.$closureData['ruleId']
+ message: $this->translator->trans('Auto Unsubscribed'),
+ details: $this->translator->trans('User auto unsubscribed for bounce rule %rule_id%', [
+ '%rule_id%' => $closureData['ruleId']
+ ])
);
}
}
diff --git a/src/Domain/Messaging/Service/Handler/DecreaseCountConfirmUserAndDeleteBounceHandler.php b/src/Domain/Messaging/Service/Handler/DecreaseCountConfirmUserAndDeleteBounceHandler.php
index a8ecdfb5..4b7471eb 100644
--- a/src/Domain/Messaging/Service/Handler/DecreaseCountConfirmUserAndDeleteBounceHandler.php
+++ b/src/Domain/Messaging/Service/Handler/DecreaseCountConfirmUserAndDeleteBounceHandler.php
@@ -8,6 +8,7 @@
use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository;
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager;
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager;
+use Symfony\Contracts\Translation\TranslatorInterface;
class DecreaseCountConfirmUserAndDeleteBounceHandler implements BounceActionHandlerInterface
{
@@ -15,17 +16,20 @@ class DecreaseCountConfirmUserAndDeleteBounceHandler implements BounceActionHand
private SubscriberManager $subscriberManager;
private BounceManager $bounceManager;
private SubscriberRepository $subscriberRepository;
+ private TranslatorInterface $translator;
public function __construct(
SubscriberHistoryManager $subscriberHistoryManager,
SubscriberManager $subscriberManager,
BounceManager $bounceManager,
SubscriberRepository $subscriberRepository,
+ TranslatorInterface $translator,
) {
$this->subscriberHistoryManager = $subscriberHistoryManager;
$this->subscriberManager = $subscriberManager;
$this->bounceManager = $bounceManager;
$this->subscriberRepository = $subscriberRepository;
+ $this->translator = $translator;
}
public function supports(string $action): bool
@@ -41,8 +45,10 @@ public function handle(array $closureData): void
$this->subscriberRepository->markConfirmed($closureData['userId']);
$this->subscriberHistoryManager->addHistory(
subscriber: $closureData['subscriber'],
- message: 'Auto confirmed',
- details: 'Subscriber auto confirmed for bounce rule '.$closureData['ruleId']
+ message: $this->translator->trans('Auto confirmed'),
+ details: $this->translator->trans('Subscriber auto confirmed for bounce rule %rule_id%', [
+ '%rule_id%' => $closureData['ruleId']
+ ])
);
}
}
diff --git a/src/Domain/Messaging/Service/Handler/RequeueHandler.php b/src/Domain/Messaging/Service/Handler/RequeueHandler.php
index 6a1d9d95..ddd0035d 100644
--- a/src/Domain/Messaging/Service/Handler/RequeueHandler.php
+++ b/src/Domain/Messaging/Service/Handler/RequeueHandler.php
@@ -11,12 +11,14 @@
use PhpList\Core\Domain\Messaging\Model\Message\MessageStatus;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
class RequeueHandler
{
public function __construct(
private readonly LoggerInterface $logger,
- private readonly EntityManagerInterface $entityManager
+ private readonly EntityManagerInterface $entityManager,
+ private readonly TranslatorInterface $translator,
) {
}
@@ -46,9 +48,9 @@ public function handle(Message $campaign, ?OutputInterface $output = null): bool
$campaign->getMetadata()->setStatus(MessageStatus::Submitted);
$this->entityManager->flush();
- $output?->writeln(sprintf(
- 'Requeued campaign; next embargo at %s',
- $next->format(DateTime::ATOM)
+ $output?->writeln($this->translator->trans(
+ 'Requeued campaign; next embargo at %time%',
+ ['%time%' => $next->format(DateTime::ATOM)],
));
$this->logger->info('Campaign requeued with new embargo', [
'campaign_id' => $campaign->getId(),
diff --git a/src/Domain/Messaging/Service/Handler/UnconfirmUserAndDeleteBounceHandler.php b/src/Domain/Messaging/Service/Handler/UnconfirmUserAndDeleteBounceHandler.php
index 7ca39be8..0653900f 100644
--- a/src/Domain/Messaging/Service/Handler/UnconfirmUserAndDeleteBounceHandler.php
+++ b/src/Domain/Messaging/Service/Handler/UnconfirmUserAndDeleteBounceHandler.php
@@ -7,21 +7,25 @@
use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager;
use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository;
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager;
+use Symfony\Contracts\Translation\TranslatorInterface;
class UnconfirmUserAndDeleteBounceHandler implements BounceActionHandlerInterface
{
private SubscriberHistoryManager $subscriberHistoryManager;
private SubscriberRepository $subscriberRepository;
private BounceManager $bounceManager;
+ private TranslatorInterface $translator;
public function __construct(
SubscriberHistoryManager $subscriberHistoryManager,
SubscriberRepository $subscriberRepository,
BounceManager $bounceManager,
+ TranslatorInterface $translator,
) {
$this->subscriberHistoryManager = $subscriberHistoryManager;
$this->subscriberRepository = $subscriberRepository;
$this->bounceManager = $bounceManager;
+ $this->translator = $translator;
}
public function supports(string $action): bool
@@ -35,8 +39,10 @@ public function handle(array $closureData): void
$this->subscriberRepository->markUnconfirmed($closureData['userId']);
$this->subscriberHistoryManager->addHistory(
subscriber: $closureData['subscriber'],
- message: 'Auto unconfirmed',
- details: 'Subscriber auto unconfirmed for bounce rule '.$closureData['ruleId']
+ message: $this->translator->trans('Auto unconfirmed'),
+ details: $this->translator->trans('Subscriber auto unconfirmed for bounce rule %rule_id%', [
+ '%rule_id%' => $closureData['ruleId']
+ ])
);
}
$this->bounceManager->delete($closureData['bounce']);
diff --git a/src/Domain/Messaging/Service/Handler/UnconfirmUserHandler.php b/src/Domain/Messaging/Service/Handler/UnconfirmUserHandler.php
index a5bdd0fe..971863f3 100644
--- a/src/Domain/Messaging/Service/Handler/UnconfirmUserHandler.php
+++ b/src/Domain/Messaging/Service/Handler/UnconfirmUserHandler.php
@@ -6,18 +6,22 @@
use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository;
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager;
+use Symfony\Contracts\Translation\TranslatorInterface;
class UnconfirmUserHandler implements BounceActionHandlerInterface
{
private SubscriberRepository $subscriberRepository;
private SubscriberHistoryManager $subscriberHistoryManager;
+ private TranslatorInterface $translator;
public function __construct(
SubscriberRepository $subscriberRepository,
SubscriberHistoryManager $subscriberHistoryManager,
+ TranslatorInterface $translator,
) {
$this->subscriberRepository = $subscriberRepository;
$this->subscriberHistoryManager = $subscriberHistoryManager;
+ $this->translator = $translator;
}
public function supports(string $action): bool
@@ -30,9 +34,11 @@ public function handle(array $closureData): void
if (!empty($closureData['subscriber']) && $closureData['confirmed']) {
$this->subscriberRepository->markUnconfirmed($closureData['userId']);
$this->subscriberHistoryManager->addHistory(
- $closureData['subscriber'],
- 'Auto Unconfirmed',
- 'Subscriber auto unconfirmed for bounce rule '.$closureData['ruleId']
+ subscriber: $closureData['subscriber'],
+ message: $this->translator->trans('Auto unconfirmed'),
+ details: $this->translator->trans('Subscriber auto unconfirmed for bounce rule %rule_id%', [
+ '%rule_id%' => $closureData['ruleId']
+ ])
);
}
}
diff --git a/src/Domain/Messaging/Service/Manager/BounceManager.php b/src/Domain/Messaging/Service/Manager/BounceManager.php
index f13c46ff..4945b881 100644
--- a/src/Domain/Messaging/Service/Manager/BounceManager.php
+++ b/src/Domain/Messaging/Service/Manager/BounceManager.php
@@ -14,27 +14,28 @@
use PhpList\Core\Domain\Messaging\Repository\UserMessageBounceRepository;
use PhpList\Core\Domain\Subscription\Model\Subscriber;
use Psr\Log\LoggerInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
class BounceManager
{
- private const TEST_MODE_MESSAGE = 'Running in test mode, not deleting messages from mailbox';
- private const LIVE_MODE_MESSAGE = 'Processed messages will be deleted from the mailbox';
-
private BounceRepository $bounceRepository;
private UserMessageBounceRepository $userMessageBounceRepo;
private EntityManagerInterface $entityManager;
private LoggerInterface $logger;
+ private TranslatorInterface $translator;
public function __construct(
BounceRepository $bounceRepository,
UserMessageBounceRepository $userMessageBounceRepo,
EntityManagerInterface $entityManager,
LoggerInterface $logger,
+ TranslatorInterface $translator,
) {
$this->bounceRepository = $bounceRepository;
$this->userMessageBounceRepo = $userMessageBounceRepo;
$this->entityManager = $entityManager;
$this->logger = $logger;
+ $this->translator = $translator;
}
public function create(
@@ -132,7 +133,9 @@ public function getUserMessageHistoryWithBounces(Subscriber $subscriber): array
public function announceDeletionMode(bool $testMode): void
{
- $message = $testMode ? self::TEST_MODE_MESSAGE : self::LIVE_MODE_MESSAGE;
- $this->logger->info($message);
+ $testModeMessage = $this->translator->trans('Running in test mode, not deleting messages from mailbox');
+ $liveModeMessage = $this->translator->trans('Processed messages will be deleted from the mailbox');
+
+ $this->logger->info($testMode ? $testModeMessage : $liveModeMessage);
}
}
diff --git a/src/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessor.php b/src/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessor.php
index 568bf874..7129401f 100644
--- a/src/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessor.php
+++ b/src/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessor.php
@@ -11,6 +11,7 @@
use PhpList\Core\Domain\Messaging\Service\Manager\BounceRuleManager;
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager;
use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Contracts\Translation\TranslatorInterface;
class AdvancedBounceRulesProcessor
{
@@ -19,16 +20,18 @@ public function __construct(
private readonly BounceRuleManager $ruleManager,
private readonly BounceActionResolver $actionResolver,
private readonly SubscriberManager $subscriberManager,
+ private readonly TranslatorInterface $translator,
) {
}
public function process(SymfonyStyle $io, int $batchSize): void
{
- $io->section('Processing bounces based on active bounce rules');
+ $io->section($this->translator->trans('Processing bounces based on active bounce rules'));
$rules = $this->ruleManager->loadActiveRules();
if (!$rules) {
- $io->writeln('No active rules');
+ $io->writeln($this->translator->trans('No active rules'));
+
return;
}
@@ -69,15 +72,20 @@ public function process(SymfonyStyle $io, int $batchSize): void
$processed++;
}
- $io->writeln(sprintf(
- 'processed %d out of %d bounces for advanced bounce rules',
- min($processed, $total),
- $total
+ $io->writeln($this->translator->trans(
+ 'Processed %processed% out of %total% bounces for advanced bounce rules', [
+ '%processed%' => min($processed, $total),
+ '%total%' => $total,
+ ]
));
}
- $io->writeln(sprintf('%d bounces processed by advanced processing', $matched));
- $io->writeln(sprintf('%d bounces were not matched by advanced processing rules', $notMatched));
+ $io->writeln($this->translator->trans(
+ '%processed% bounces processed by advanced processing', ['%processed%' => $matched]
+ ));
+ $io->writeln($this->translator->trans(
+ '%not_processed% bounces were not matched by advanced processing rules', ['%not_processed%' => $notMatched]
+ ));
}
private function composeText(Bounce $bounce): string
diff --git a/tests/Unit/Domain/Messaging/Service/Handler/BlacklistEmailHandlerTest.php b/tests/Unit/Domain/Messaging/Service/Handler/BlacklistEmailHandlerTest.php
index 54f7362b..cb009022 100644
--- a/tests/Unit/Domain/Messaging/Service/Handler/BlacklistEmailHandlerTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Handler/BlacklistEmailHandlerTest.php
@@ -10,6 +10,7 @@
use PhpList\Core\Domain\Subscription\Service\SubscriberBlacklistService;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Translation\Translator;
class BlacklistEmailHandlerTest extends TestCase
{
@@ -24,6 +25,7 @@ protected function setUp(): void
$this->handler = new BlacklistEmailHandler(
subscriberHistoryManager: $this->historyManager,
blacklistService: $this->blacklistService,
+ translator: new Translator('en'),
);
}
diff --git a/tests/Unit/Domain/Messaging/Service/Handler/BlacklistUserAndDeleteBounceHandlerTest.php b/tests/Unit/Domain/Messaging/Service/Handler/BlacklistUserAndDeleteBounceHandlerTest.php
index af1df32e..0368d695 100644
--- a/tests/Unit/Domain/Messaging/Service/Handler/BlacklistUserAndDeleteBounceHandlerTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Handler/BlacklistUserAndDeleteBounceHandlerTest.php
@@ -12,6 +12,7 @@
use PhpList\Core\Domain\Subscription\Service\SubscriberBlacklistService;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Translation\Translator;
class BlacklistUserAndDeleteBounceHandlerTest extends TestCase
{
@@ -29,6 +30,7 @@ protected function setUp(): void
subscriberHistoryManager: $this->historyManager,
bounceManager: $this->bounceManager,
blacklistService: $this->blacklistService,
+ translator: new Translator('en')
);
}
diff --git a/tests/Unit/Domain/Messaging/Service/Handler/BlacklistUserHandlerTest.php b/tests/Unit/Domain/Messaging/Service/Handler/BlacklistUserHandlerTest.php
index 72fe4584..e25f54c8 100644
--- a/tests/Unit/Domain/Messaging/Service/Handler/BlacklistUserHandlerTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Handler/BlacklistUserHandlerTest.php
@@ -10,6 +10,7 @@
use PhpList\Core\Domain\Subscription\Service\SubscriberBlacklistService;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Translation\Translator;
class BlacklistUserHandlerTest extends TestCase
{
@@ -23,7 +24,8 @@ protected function setUp(): void
$this->blacklistService = $this->createMock(SubscriberBlacklistService::class);
$this->handler = new BlacklistUserHandler(
subscriberHistoryManager: $this->historyManager,
- blacklistService: $this->blacklistService
+ blacklistService: $this->blacklistService,
+ translator: new Translator('en')
);
}
diff --git a/tests/Unit/Domain/Messaging/Service/Handler/DecreaseCountConfirmUserAndDeleteBounceHandlerTest.php b/tests/Unit/Domain/Messaging/Service/Handler/DecreaseCountConfirmUserAndDeleteBounceHandlerTest.php
index 7d82336f..34d707e5 100644
--- a/tests/Unit/Domain/Messaging/Service/Handler/DecreaseCountConfirmUserAndDeleteBounceHandlerTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Handler/DecreaseCountConfirmUserAndDeleteBounceHandlerTest.php
@@ -13,6 +13,7 @@
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Translation\Translator;
class DecreaseCountConfirmUserAndDeleteBounceHandlerTest extends TestCase
{
@@ -33,6 +34,7 @@ protected function setUp(): void
subscriberManager: $this->subscriberManager,
bounceManager: $this->bounceManager,
subscriberRepository: $this->subscriberRepository,
+ translator: new Translator('en'),
);
}
diff --git a/tests/Unit/Domain/Messaging/Service/Handler/RequeueHandlerTest.php b/tests/Unit/Domain/Messaging/Service/Handler/RequeueHandlerTest.php
index 5bfb1114..079d06a8 100644
--- a/tests/Unit/Domain/Messaging/Service/Handler/RequeueHandlerTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Handler/RequeueHandlerTest.php
@@ -19,6 +19,7 @@
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Translation\Translator;
class RequeueHandlerTest extends TestCase
{
@@ -55,7 +56,7 @@ private function createMessage(
public function testReturnsFalseWhenIntervalIsZeroOrNegative(): void
{
- $handler = new RequeueHandler($this->logger, $this->em);
+ $handler = new RequeueHandler($this->logger, $this->em, new Translator('en'));
$message = $this->createMessage(0, null, null);
$this->em->expects($this->never())->method('flush');
@@ -70,7 +71,7 @@ public function testReturnsFalseWhenIntervalIsZeroOrNegative(): void
public function testReturnsFalseWhenNowIsAfterRequeueUntil(): void
{
- $handler = new RequeueHandler($this->logger, $this->em);
+ $handler = new RequeueHandler($this->logger, $this->em, new Translator('en'));
$past = (new DateTime())->sub(new DateInterval('PT5M'));
$message = $this->createMessage(5, $past, null);
@@ -85,7 +86,7 @@ public function testReturnsFalseWhenNowIsAfterRequeueUntil(): void
public function testRequeuesFromFutureEmbargoAndSetsSubmittedStatus(): void
{
- $handler = new RequeueHandler($this->logger, $this->em);
+ $handler = new RequeueHandler($this->logger, $this->em, new Translator('en'));
$embargo = (new DateTime())->add(new DateInterval('PT5M'));
$interval = 10;
$message = $this->createMessage($interval, null, $embargo);
@@ -107,7 +108,7 @@ public function testRequeuesFromFutureEmbargoAndSetsSubmittedStatus(): void
public function testRequeuesFromNowWhenEmbargoIsNullOrPast(): void
{
- $handler = new RequeueHandler($this->logger, $this->em);
+ $handler = new RequeueHandler($this->logger, $this->em, new Translator('en'));
$interval = 3;
$message = $this->createMessage($interval, null, null);
@@ -133,7 +134,7 @@ public function testRequeuesFromNowWhenEmbargoIsNullOrPast(): void
public function testReturnsFalseWhenNextEmbargoExceedsUntil(): void
{
- $handler = new RequeueHandler($this->logger, $this->em);
+ $handler = new RequeueHandler($this->logger, $this->em, new Translator('en'));
$embargo = (new DateTime())->add(new DateInterval('PT1M'));
$interval = 10;
// next would be +10, which exceeds until
diff --git a/tests/Unit/Domain/Messaging/Service/Handler/UnconfirmUserAndDeleteBounceHandlerTest.php b/tests/Unit/Domain/Messaging/Service/Handler/UnconfirmUserAndDeleteBounceHandlerTest.php
index 7a4ac245..6ddc4e3d 100644
--- a/tests/Unit/Domain/Messaging/Service/Handler/UnconfirmUserAndDeleteBounceHandlerTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Handler/UnconfirmUserAndDeleteBounceHandlerTest.php
@@ -12,6 +12,7 @@
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Translation\Translator;
class UnconfirmUserAndDeleteBounceHandlerTest extends TestCase
{
@@ -29,6 +30,7 @@ protected function setUp(): void
subscriberHistoryManager: $this->historyManager,
subscriberRepository: $this->subscriberRepository,
bounceManager: $this->bounceManager,
+ translator: new Translator('en')
);
}
diff --git a/tests/Unit/Domain/Messaging/Service/Handler/UnconfirmUserHandlerTest.php b/tests/Unit/Domain/Messaging/Service/Handler/UnconfirmUserHandlerTest.php
index a395e110..fbbc265a 100644
--- a/tests/Unit/Domain/Messaging/Service/Handler/UnconfirmUserHandlerTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Handler/UnconfirmUserHandlerTest.php
@@ -10,6 +10,7 @@
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Translation\Translator;
class UnconfirmUserHandlerTest extends TestCase
{
@@ -23,7 +24,8 @@ protected function setUp(): void
$this->historyManager = $this->createMock(SubscriberHistoryManager::class);
$this->handler = new UnconfirmUserHandler(
subscriberRepository: $this->subscriberRepository,
- subscriberHistoryManager: $this->historyManager
+ subscriberHistoryManager: $this->historyManager,
+ translator: new Translator('en')
);
}
@@ -41,7 +43,7 @@ public function testHandleMarksUnconfirmedAndAddsHistoryWhenSubscriberPresentAnd
$this->subscriberRepository->expects($this->once())->method('markUnconfirmed')->with(123);
$this->historyManager->expects($this->once())->method('addHistory')->with(
$subscriber,
- 'Auto Unconfirmed',
+ 'Auto unconfirmed',
$this->stringContains('bounce rule 9')
);
diff --git a/tests/Unit/Domain/Messaging/Service/Manager/BounceManagerTest.php b/tests/Unit/Domain/Messaging/Service/Manager/BounceManagerTest.php
index bd1a4a68..0dbde7ad 100644
--- a/tests/Unit/Domain/Messaging/Service/Manager/BounceManagerTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Manager/BounceManagerTest.php
@@ -15,6 +15,7 @@
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
+use Symfony\Component\Translation\Translator;
class BounceManagerTest extends TestCase
{
@@ -35,6 +36,7 @@ protected function setUp(): void
userMessageBounceRepo: $this->userMessageBounceRepository,
entityManager: $this->entityManager,
logger: $this->logger,
+ translator: new Translator('en')
);
}
diff --git a/tests/Unit/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessorTest.php b/tests/Unit/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessorTest.php
index 209fb583..d86e494b 100644
--- a/tests/Unit/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessorTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessorTest.php
@@ -16,6 +16,7 @@
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\Translation\Translator;
class AdvancedBounceRulesProcessorTest extends TestCase
{
@@ -45,6 +46,7 @@ public function testNoActiveRules(): void
ruleManager: $this->ruleManager,
actionResolver: $this->actionResolver,
subscriberManager: $this->subscriberManager,
+ translator: new Translator('en'),
);
$processor->process($this->io, 100);
@@ -170,6 +172,7 @@ public function testProcessingWithMatchesAndNonMatches(): void
ruleManager: $this->ruleManager,
actionResolver: $this->actionResolver,
subscriberManager: $this->subscriberManager,
+ translator: new Translator('en'),
);
$processor->process($this->io, 2);
From b1ef842e6586547fab1d8d587eae022fe28afb0a Mon Sep 17 00:00:00 2001
From: Tatevik
Date: Sat, 20 Sep 2025 16:21:51 +0400
Subject: [PATCH 11/17] AdvancedBounceRulesProcessor
---
resources/translations/messages.en.xlf | 19 ++++++++++++++++++-
.../Service/Processor/MboxBounceProcessor.php | 11 +++++++----
.../Service/Processor/PopBounceProcessor.php | 10 +++++++---
.../AdvancedBounceRulesProcessorTest.php | 18 +++++++++++++-----
.../Processor/MboxBounceProcessorTest.php | 15 +++++++++------
.../Processor/PopBounceProcessorTest.php | 6 ++++--
6 files changed, 58 insertions(+), 21 deletions(-)
diff --git a/resources/translations/messages.en.xlf b/resources/translations/messages.en.xlf
index d97e6af6..ba2b0c1b 100644
--- a/resources/translations/messages.en.xlf
+++ b/resources/translations/messages.en.xlf
@@ -258,7 +258,7 @@
Processed %processed% out of %total% bounces for advanced bounce rules
- Processed %processed% out of %total% bounces for advanced bounce rules
+ Processed %processed% out of %total% bounces for advanced bounce rules
@@ -270,6 +270,23 @@
%not_processed% bounces were not matched by advanced processing rules
+
+ Opening mbox %file%
+ Opening mbox %file%
+
+
+ Connecting to %mailbox%
+ Connecting to %mailbox%
+
+
+ Please do not interrupt this process
+ Please do not interrupt this process
+
+
+ mbox file path must be provided with --mailbox.
+ mbox file path must be provided with --mailbox.
+
+
Subscriber list not found.
diff --git a/src/Domain/Messaging/Service/Processor/MboxBounceProcessor.php b/src/Domain/Messaging/Service/Processor/MboxBounceProcessor.php
index a52b6f2f..d61742d5 100644
--- a/src/Domain/Messaging/Service/Processor/MboxBounceProcessor.php
+++ b/src/Domain/Messaging/Service/Processor/MboxBounceProcessor.php
@@ -8,14 +8,17 @@
use RuntimeException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Contracts\Translation\TranslatorInterface;
class MboxBounceProcessor implements BounceProtocolProcessor
{
private BounceProcessingServiceInterface $processingService;
+ private TranslatorInterface $translator;
- public function __construct(BounceProcessingServiceInterface $processingService)
+ public function __construct(BounceProcessingServiceInterface $processingService, TranslatorInterface $translator)
{
$this->processingService = $processingService;
+ $this->translator = $translator;
}
public function getProtocol(): string
@@ -30,12 +33,12 @@ public function process(InputInterface $input, SymfonyStyle $inputOutput): strin
$file = (string)$input->getOption('mailbox');
if (!$file) {
- $inputOutput->error('mbox file path must be provided with --mailbox.');
+ $inputOutput->error($this->translator->trans('mbox file path must be provided with --mailbox.'));
throw new RuntimeException('Missing --mailbox for mbox protocol');
}
- $inputOutput->section('Opening mbox ' . $file);
- $inputOutput->writeln('Please do not interrupt this process');
+ $inputOutput->section($this->translator->trans('Opening mbox %file%', ['%file%' => $file]));
+ $inputOutput->writeln($this->translator->trans('Please do not interrupt this process'));
return $this->processingService->processMailbox(
mailbox: $file,
diff --git a/src/Domain/Messaging/Service/Processor/PopBounceProcessor.php b/src/Domain/Messaging/Service/Processor/PopBounceProcessor.php
index b6f59f65..b0079774 100644
--- a/src/Domain/Messaging/Service/Processor/PopBounceProcessor.php
+++ b/src/Domain/Messaging/Service/Processor/PopBounceProcessor.php
@@ -7,6 +7,7 @@
use PhpList\Core\Domain\Messaging\Service\BounceProcessingServiceInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Contracts\Translation\TranslatorInterface;
class PopBounceProcessor implements BounceProtocolProcessor
{
@@ -14,17 +15,20 @@ class PopBounceProcessor implements BounceProtocolProcessor
private string $host;
private int $port;
private string $mailboxNames;
+ private TranslatorInterface $translator;
public function __construct(
BounceProcessingServiceInterface $processingService,
string $host,
int $port,
- string $mailboxNames
+ string $mailboxNames,
+ TranslatorInterface $translator
) {
$this->processingService = $processingService;
$this->host = $host;
$this->port = $port;
$this->mailboxNames = $mailboxNames;
+ $this->translator = $translator;
}
public function getProtocol(): string
@@ -44,8 +48,8 @@ public function process(InputInterface $input, SymfonyStyle $inputOutput): strin
$mailboxName = 'INBOX';
}
$mailbox = sprintf('{%s:%s}%s', $this->host, $this->port, $mailboxName);
- $inputOutput->section('Connecting to ' . $mailbox);
- $inputOutput->writeln('Please do not interrupt this process');
+ $inputOutput->section($this->translator->trans('Connecting to %mailbox%', ['%mailbox%' => $mailbox]));
+ $inputOutput->writeln($this->translator->trans('Please do not interrupt this process'));
$downloadReport .= $this->processingService->processMailbox(
mailbox: $mailbox,
diff --git a/tests/Unit/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessorTest.php b/tests/Unit/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessorTest.php
index d86e494b..a4590052 100644
--- a/tests/Unit/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessorTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessorTest.php
@@ -37,16 +37,23 @@ protected function setUp(): void
public function testNoActiveRules(): void
{
- $this->io->expects($this->once())->method('section')->with('Processing bounces based on active bounce rules');
+ $translator = new Translator('en');
+ $this->io
+ ->expects($this->once())
+ ->method('section')
+ ->with($translator->trans('Processing bounces based on active bounce rules'));
$this->ruleManager->method('loadActiveRules')->willReturn([]);
- $this->io->expects($this->once())->method('writeln')->with('No active rules');
+ $this->io
+ ->expects($this->once())
+ ->method('writeln')
+ ->with($translator->trans('No active rules'));
$processor = new AdvancedBounceRulesProcessor(
bounceManager: $this->bounceManager,
ruleManager: $this->ruleManager,
actionResolver: $this->actionResolver,
subscriberManager: $this->subscriberManager,
- translator: new Translator('en'),
+ translator: $translator,
);
$processor->process($this->io, 100);
@@ -161,10 +168,11 @@ public function testProcessingWithMatchesAndNonMatches(): void
return null;
});
+ $translator = new Translator('en');
$this->io
->expects($this->once())
->method('section')
- ->with('Processing bounces based on active bounce rules');
+ ->with($translator->trans('Processing bounces based on active bounce rules'));
$this->io->expects($this->exactly(4))->method('writeln');
$processor = new AdvancedBounceRulesProcessor(
@@ -172,7 +180,7 @@ public function testProcessingWithMatchesAndNonMatches(): void
ruleManager: $this->ruleManager,
actionResolver: $this->actionResolver,
subscriberManager: $this->subscriberManager,
- translator: new Translator('en'),
+ translator: $translator,
);
$processor->process($this->io, 2);
diff --git a/tests/Unit/Domain/Messaging/Service/Processor/MboxBounceProcessorTest.php b/tests/Unit/Domain/Messaging/Service/Processor/MboxBounceProcessorTest.php
index 210e000c..f1c62f6e 100644
--- a/tests/Unit/Domain/Messaging/Service/Processor/MboxBounceProcessorTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Processor/MboxBounceProcessorTest.php
@@ -11,6 +11,7 @@
use RuntimeException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\Translation\Translator;
class MboxBounceProcessorTest extends TestCase
{
@@ -27,13 +28,14 @@ protected function setUp(): void
public function testGetProtocol(): void
{
- $processor = new MboxBounceProcessor($this->service);
+ $processor = new MboxBounceProcessor($this->service, new Translator('en'));
$this->assertSame('mbox', $processor->getProtocol());
}
public function testProcessThrowsWhenMailboxMissing(): void
{
- $processor = new MboxBounceProcessor($this->service);
+ $translator = new Translator('en');
+ $processor = new MboxBounceProcessor($this->service, $translator);
$this->input->method('getOption')->willReturnMap([
['test', false],
@@ -44,7 +46,7 @@ public function testProcessThrowsWhenMailboxMissing(): void
$this->io
->expects($this->once())
->method('error')
- ->with('mbox file path must be provided with --mailbox.');
+ ->with($translator->trans('mbox file path must be provided with --mailbox.'));
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Missing --mailbox for mbox protocol');
@@ -54,7 +56,8 @@ public function testProcessThrowsWhenMailboxMissing(): void
public function testProcessSuccess(): void
{
- $processor = new MboxBounceProcessor($this->service);
+ $translator = new Translator('en');
+ $processor = new MboxBounceProcessor($this->service, $translator);
$this->input->method('getOption')->willReturnMap([
['test', true],
@@ -62,8 +65,8 @@ public function testProcessSuccess(): void
['mailbox', '/var/mail/bounce.mbox'],
]);
- $this->io->expects($this->once())->method('section')->with('Opening mbox /var/mail/bounce.mbox');
- $this->io->expects($this->once())->method('writeln')->with('Please do not interrupt this process');
+ $this->io->expects($this->once())->method('section')->with($translator->trans('Opening mbox %file%', ['%file%' => '/var/mail/bounce.mbox']));
+ $this->io->expects($this->once())->method('writeln')->with($translator->trans('Please do not interrupt this process'));
$this->service->expects($this->once())
->method('processMailbox')
diff --git a/tests/Unit/Domain/Messaging/Service/Processor/PopBounceProcessorTest.php b/tests/Unit/Domain/Messaging/Service/Processor/PopBounceProcessorTest.php
index fad4cfbe..d0141386 100644
--- a/tests/Unit/Domain/Messaging/Service/Processor/PopBounceProcessorTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Processor/PopBounceProcessorTest.php
@@ -10,6 +10,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\Translation\Translator;
class PopBounceProcessorTest extends TestCase
{
@@ -26,13 +27,14 @@ protected function setUp(): void
public function testGetProtocol(): void
{
- $processor = new PopBounceProcessor($this->service, 'mail.example.com', 995, 'INBOX');
+ $processor = new PopBounceProcessor($this->service, 'mail.example.com', 995, 'INBOX', new Translator('en'));
$this->assertSame('pop', $processor->getProtocol());
}
public function testProcessWithMultipleMailboxesAndDefaults(): void
{
- $processor = new PopBounceProcessor($this->service, 'pop.example.com', 110, 'INBOX, ,Custom');
+ $translator = new Translator('en');
+ $processor = new PopBounceProcessor($this->service, 'pop.example.com', 110, 'INBOX, ,Custom', $translator);
$this->input->method('getOption')->willReturnMap([
['test', true],
From a2740f7c27bab871593ea28c6680b304d47e5bd7 Mon Sep 17 00:00:00 2001
From: Tatevik
Date: Sat, 20 Sep 2025 16:42:50 +0400
Subject: [PATCH 12/17] BounceStatus
---
src/Domain/Messaging/Model/BounceStatus.php | 19 +++++++
.../Service/Processor/BounceDataProcessor.php | 53 ++++++++++++-------
2 files changed, 53 insertions(+), 19 deletions(-)
create mode 100644 src/Domain/Messaging/Model/BounceStatus.php
diff --git a/src/Domain/Messaging/Model/BounceStatus.php b/src/Domain/Messaging/Model/BounceStatus.php
new file mode 100644
index 00000000..f24aed85
--- /dev/null
+++ b/src/Domain/Messaging/Model/BounceStatus.php
@@ -0,0 +1,19 @@
+value, $userId);
+ }
+}
diff --git a/src/Domain/Messaging/Service/Processor/BounceDataProcessor.php b/src/Domain/Messaging/Service/Processor/BounceDataProcessor.php
index 6f502a8c..4750e762 100644
--- a/src/Domain/Messaging/Service/Processor/BounceDataProcessor.php
+++ b/src/Domain/Messaging/Service/Processor/BounceDataProcessor.php
@@ -6,6 +6,7 @@
use DateTimeImmutable;
use PhpList\Core\Domain\Messaging\Model\Bounce;
+use PhpList\Core\Domain\Messaging\Model\BounceStatus;
use PhpList\Core\Domain\Messaging\Repository\MessageRepository;
use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager;
use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository;
@@ -44,26 +45,26 @@ public function process(Bounce $bounce, ?string $msgId, ?int $userId, DateTimeIm
if ($msgId === 'systemmessage') {
return $userId ? $this->handleSystemMessageWithUser(
- $bounce,
- $bounceDate,
- $userId,
- $user
- ) : $this->handleSystemMessageUnknownUser($bounce);
+ bounce: $bounce,
+ date: $bounceDate,
+ userId: $userId,
+ userOrNull: $user
+ ) : $this->handleSystemMessageUnknownUser(bounce: $bounce);
}
if ($msgId && $userId) {
- return $this->handleKnownMessageAndUser($bounce, $bounceDate, (int)$msgId, $userId);
+ return $this->handleKnownMessageAndUser(bounce: $bounce, date: $bounceDate, msgId: (int)$msgId, userId: $userId);
}
if ($userId) {
- return $this->handleUserOnly($bounce, $userId);
+ return $this->handleUserOnly(bounce: $bounce, userId: $userId);
}
if ($msgId) {
- return $this->handleMessageOnly($bounce, (int)$msgId);
+ return $this->handleMessageOnly(bounce: $bounce, msgId: (int)$msgId);
}
- $this->bounceManager->update($bounce, 'unidentified bounce', 'not processed');
+ $this->bounceManager->update(bounce: $bounce, status: BounceStatus::Unknown->value, comment: 'not processed');
return false;
}
@@ -76,10 +77,10 @@ private function handleSystemMessageWithUser(
): bool {
$this->bounceManager->update(
bounce: $bounce,
- status: 'bounced system message',
+ status: BounceStatus::SystemMessage->value,
comment: sprintf('%d marked unconfirmed', $userId)
);
- $this->bounceManager->linkUserMessageBounce($bounce, $date, $userId);
+ $this->bounceManager->linkUserMessageBounce(bounce: $bounce, date: $date, subscriberId: $userId);
$this->subscriberRepository->markUnconfirmed($userId);
$this->logger->info('system message bounced, user marked unconfirmed', ['userId' => $userId]);
@@ -96,7 +97,11 @@ private function handleSystemMessageWithUser(
private function handleSystemMessageUnknownUser(Bounce $bounce): bool
{
- $this->bounceManager->update($bounce, 'bounced system message', 'unknown user');
+ $this->bounceManager->update(
+ bounce:$bounce,
+ status: BounceStatus::SystemMessage->value,
+ comment: 'unknown user'
+ );
$this->logger->info('system message bounced, but unknown user');
return true;
@@ -108,20 +113,30 @@ private function handleKnownMessageAndUser(
int $msgId,
int $userId
): bool {
- if (!$this->bounceManager->existsUserMessageBounce($userId, $msgId)) {
- $this->bounceManager->linkUserMessageBounce($bounce, $date, $userId, $msgId);
+ if (!$this->bounceManager->existsUserMessageBounce(subscriberId: $userId, messageId: $msgId)) {
+ $this->bounceManager->linkUserMessageBounce(
+ bounce: $bounce,
+ date: $date,
+ subscriberId: $userId,
+ messageId: $msgId
+ );
$this->bounceManager->update(
bounce: $bounce,
- status: sprintf('bounced list message %d', $msgId),
+ status: BounceStatus::BouncedList->format($msgId),
comment: sprintf('%d bouncecount increased', $userId)
);
$this->messageRepository->incrementBounceCount($msgId);
$this->subscriberRepository->incrementBounceCount($userId);
} else {
- $this->bounceManager->linkUserMessageBounce($bounce, $date, $userId, $msgId);
+ $this->bounceManager->linkUserMessageBounce(
+ bounce: $bounce,
+ date: $date,
+ subscriberId: $userId,
+ messageId: $msgId
+ );
$this->bounceManager->update(
bounce: $bounce,
- status: sprintf('duplicate bounce for %d', $userId),
+ status: BounceStatus::DuplicateBounce->format($userId),
comment: sprintf('duplicate bounce for subscriber %d on message %d', $userId, $msgId)
);
}
@@ -133,7 +148,7 @@ private function handleUserOnly(Bounce $bounce, int $userId): bool
{
$this->bounceManager->update(
bounce: $bounce,
- status: 'bounced unidentified message',
+ status: BounceStatus::UnidentifiedMessage->value,
comment: sprintf('%d bouncecount increased', $userId)
);
$this->subscriberRepository->incrementBounceCount($userId);
@@ -145,7 +160,7 @@ private function handleMessageOnly(Bounce $bounce, int $msgId): bool
{
$this->bounceManager->update(
bounce: $bounce,
- status: sprintf('bounced list message %d', $msgId),
+ status: BounceStatus::BouncedList->format($msgId),
comment: 'unknown user'
);
$this->messageRepository->incrementBounceCount($msgId);
From 4ab076d917cbde513fea64150495dd7e56eb291c Mon Sep 17 00:00:00 2001
From: Tatevik
Date: Sat, 20 Sep 2025 16:50:46 +0400
Subject: [PATCH 13/17] CampaignProcessor
---
resources/translations/messages.en.xlf | 9 +++++++++
.../Service/Processor/CampaignProcessor.php | 14 +++++++++++---
.../Service/Processor/CampaignProcessorTest.php | 7 ++++---
3 files changed, 24 insertions(+), 6 deletions(-)
diff --git a/resources/translations/messages.en.xlf b/resources/translations/messages.en.xlf
index ba2b0c1b..c102fb64 100644
--- a/resources/translations/messages.en.xlf
+++ b/resources/translations/messages.en.xlf
@@ -287,6 +287,15 @@
mbox file path must be provided with --mailbox.
+
+ Invalid email, marking unconfirmed: %email%
+ Invalid email, marking unconfirmed: %email%
+
+
+ Failed to send to: %email%
+ Failed to send to: %email%
+
+
Subscriber list not found.
diff --git a/src/Domain/Messaging/Service/Processor/CampaignProcessor.php b/src/Domain/Messaging/Service/Processor/CampaignProcessor.php
index 92313e28..a5deb074 100644
--- a/src/Domain/Messaging/Service/Processor/CampaignProcessor.php
+++ b/src/Domain/Messaging/Service/Processor/CampaignProcessor.php
@@ -18,6 +18,7 @@
use PhpList\Core\Domain\Subscription\Model\Subscriber;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
use Throwable;
/**
@@ -33,6 +34,7 @@ class CampaignProcessor
private UserMessageRepository $userMessageRepository;
private MaxProcessTimeLimiter $timeLimiter;
private RequeueHandler $requeueHandler;
+ private TranslatorInterface $translator;
public function __construct(
RateLimitedCampaignMailer $mailer,
@@ -42,7 +44,8 @@ public function __construct(
LoggerInterface $logger,
UserMessageRepository $userMessageRepository,
MaxProcessTimeLimiter $timeLimiter,
- RequeueHandler $requeueHandler
+ RequeueHandler $requeueHandler,
+ TranslatorInterface $translator,
) {
$this->mailer = $mailer;
$this->entityManager = $entityManager;
@@ -52,6 +55,7 @@ public function __construct(
$this->userMessageRepository = $userMessageRepository;
$this->timeLimiter = $timeLimiter;
$this->requeueHandler = $requeueHandler;
+ $this->translator = $translator;
}
public function process(Message $campaign, ?OutputInterface $output = null): void
@@ -82,7 +86,9 @@ public function process(Message $campaign, ?OutputInterface $output = null): voi
if (!filter_var($subscriber->getEmail(), FILTER_VALIDATE_EMAIL)) {
$this->updateUserMessageStatus($userMessage, UserMessageStatus::InvalidEmailAddress);
$this->unconfirmSubscriber($subscriber);
- $output?->writeln('Invalid email, marking unconfirmed: ' . $subscriber->getEmail());
+ $output?->writeln($this->translator->trans('Invalid email, marking unconfirmed: %email%', [
+ '%email%' => $subscriber->getEmail(),
+ ]));
continue;
}
@@ -98,7 +104,9 @@ public function process(Message $campaign, ?OutputInterface $output = null): voi
'subscriber_id' => $subscriber->getId(),
'campaign_id' => $campaign->getId(),
]);
- $output?->writeln('Failed to send to: ' . $subscriber->getEmail());
+ $output?->writeln($this->translator->trans('Failed to send to: %email%', [
+ '%email%' => $subscriber->getEmail(),
+ ]));
}
}
diff --git a/tests/Unit/Domain/Messaging/Service/Processor/CampaignProcessorTest.php b/tests/Unit/Domain/Messaging/Service/Processor/CampaignProcessorTest.php
index 26aec09f..e1976202 100644
--- a/tests/Unit/Domain/Messaging/Service/Processor/CampaignProcessorTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Processor/CampaignProcessorTest.php
@@ -22,6 +22,7 @@
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Mime\Email;
+use Symfony\Component\Translation\Translator;
class CampaignProcessorTest extends TestCase
{
@@ -32,7 +33,6 @@ class CampaignProcessorTest extends TestCase
private LoggerInterface|MockObject $logger;
private OutputInterface|MockObject $output;
private CampaignProcessor $campaignProcessor;
- private UserMessageRepository|MockObject $userMessageRepository;
protected function setUp(): void
{
@@ -42,7 +42,7 @@ protected function setUp(): void
$this->messagePreparator = $this->createMock(MessageProcessingPreparator::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->output = $this->createMock(OutputInterface::class);
- $this->userMessageRepository = $this->createMock(UserMessageRepository::class);
+ $userMessageRepository = $this->createMock(UserMessageRepository::class);
$this->campaignProcessor = new CampaignProcessor(
mailer: $this->mailer,
@@ -50,9 +50,10 @@ protected function setUp(): void
subscriberProvider: $this->subscriberProvider,
messagePreparator: $this->messagePreparator,
logger: $this->logger,
- userMessageRepository: $this->userMessageRepository,
+ userMessageRepository: $userMessageRepository,
timeLimiter: $this->createMock(MaxProcessTimeLimiter::class),
requeueHandler: $this->createMock(RequeueHandler::class),
+ translator: new Translator('en'),
);
}
From 1cd588cc6af302fcd42586d5d984199856104e2a Mon Sep 17 00:00:00 2001
From: Tatevik
Date: Sat, 20 Sep 2025 17:03:17 +0400
Subject: [PATCH 14/17] UnidentifiedBounceReprocessor
---
resources/translations/messages.en.xlf | 17 ++++++++
src/Domain/Messaging/Model/BounceStatus.php | 2 +-
.../Service/Processor/BounceDataProcessor.php | 2 +-
.../UnidentifiedBounceReprocessor.php | 41 ++++++++++++-------
.../UnidentifiedBounceReprocessorTest.php | 4 +-
5 files changed, 48 insertions(+), 18 deletions(-)
diff --git a/resources/translations/messages.en.xlf b/resources/translations/messages.en.xlf
index c102fb64..c832b660 100644
--- a/resources/translations/messages.en.xlf
+++ b/resources/translations/messages.en.xlf
@@ -296,6 +296,23 @@
Failed to send to: %email%
+
+ Reprocessing unidentified bounces
+ Reprocessing unidentified bounces
+
+
+ %total% bounces to reprocess
+ %total% bounces to reprocess
+
+
+ %count% out of %total% processed
+ %count% out of %total% processed
+
+
+ %reparsed% bounces were re-processed and %reidentified% bounces were re-identified
+ %reparsed% bounces were re-processed and %reidentified% bounces were re-identified
+
+
Subscriber list not found.
diff --git a/src/Domain/Messaging/Model/BounceStatus.php b/src/Domain/Messaging/Model/BounceStatus.php
index f24aed85..be77473f 100644
--- a/src/Domain/Messaging/Model/BounceStatus.php
+++ b/src/Domain/Messaging/Model/BounceStatus.php
@@ -10,7 +10,7 @@ enum BounceStatus: string
case BouncedList = 'bounced list message %d';
case DuplicateBounce = 'duplicate bounce for %d';
case SystemMessage = 'bounced system message';
- case Unknown = 'unidentified bounce';
+ case UnidentifiedBounce = 'unidentified bounce';
public function format(int $userId): string
{
diff --git a/src/Domain/Messaging/Service/Processor/BounceDataProcessor.php b/src/Domain/Messaging/Service/Processor/BounceDataProcessor.php
index 4750e762..51152f84 100644
--- a/src/Domain/Messaging/Service/Processor/BounceDataProcessor.php
+++ b/src/Domain/Messaging/Service/Processor/BounceDataProcessor.php
@@ -64,7 +64,7 @@ public function process(Bounce $bounce, ?string $msgId, ?int $userId, DateTimeIm
return $this->handleMessageOnly(bounce: $bounce, msgId: (int)$msgId);
}
- $this->bounceManager->update(bounce: $bounce, status: BounceStatus::Unknown->value, comment: 'not processed');
+ $this->bounceManager->update(bounce: $bounce, status: BounceStatus::UnidentifiedBounce->value, comment: 'not processed');
return false;
}
diff --git a/src/Domain/Messaging/Service/Processor/UnidentifiedBounceReprocessor.php b/src/Domain/Messaging/Service/Processor/UnidentifiedBounceReprocessor.php
index 503fc459..8a971639 100644
--- a/src/Domain/Messaging/Service/Processor/UnidentifiedBounceReprocessor.php
+++ b/src/Domain/Messaging/Service/Processor/UnidentifiedBounceReprocessor.php
@@ -5,33 +5,37 @@
namespace PhpList\Core\Domain\Messaging\Service\Processor;
use DateTimeImmutable;
+use PhpList\Core\Domain\Messaging\Model\BounceStatus;
use PhpList\Core\Domain\Messaging\Service\MessageParser;
use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager;
use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Contracts\Translation\TranslatorInterface;
class UnidentifiedBounceReprocessor
{
private BounceManager $bounceManager;
private MessageParser $messageParser;
private BounceDataProcessor $bounceDataProcessor;
-
+ private TranslatorInterface $translator;
public function __construct(
BounceManager $bounceManager,
MessageParser $messageParser,
BounceDataProcessor $bounceDataProcessor,
+ TranslatorInterface $translator,
) {
$this->bounceManager = $bounceManager;
$this->messageParser = $messageParser;
$this->bounceDataProcessor = $bounceDataProcessor;
+ $this->translator = $translator;
}
public function process(SymfonyStyle $inputOutput): void
{
- $inputOutput->section('Reprocessing unidentified bounces');
- $bounces = $this->bounceManager->findByStatus('unidentified bounce');
+ $inputOutput->section($this->translator->trans('Reprocessing unidentified bounces'));
+ $bounces = $this->bounceManager->findByStatus(BounceStatus::UnidentifiedBounce->value);
$total = count($bounces);
- $inputOutput->writeln(sprintf('%d bounces to reprocess', $total));
+ $inputOutput->writeln($this->translator->trans('%total% bounces to reprocess', ['%total%' => $total]));
$count = 0;
$reparsed = 0;
@@ -39,20 +43,23 @@ public function process(SymfonyStyle $inputOutput): void
foreach ($bounces as $bounce) {
$count++;
if ($count % 25 === 0) {
- $inputOutput->writeln(sprintf('%d out of %d processed', $count, $total));
+ $inputOutput->writeln($this->translator->trans('%count% out of %total% processed', [
+ '%count%' => $count,
+ '%total%' => $total
+ ]));
}
- $decodedBody = $this->messageParser->decodeBody($bounce->getHeader(), $bounce->getData());
+ $decodedBody = $this->messageParser->decodeBody(header: $bounce->getHeader(), body: $bounce->getData());
$userId = $this->messageParser->findUserId($decodedBody);
$messageId = $this->messageParser->findMessageId($decodedBody);
if ($userId || $messageId) {
$reparsed++;
if ($this->bounceDataProcessor->process(
- $bounce,
- $messageId,
- $userId,
- new DateTimeImmutable()
+ bounce: $bounce,
+ msgId: $messageId,
+ userId: $userId,
+ bounceDate: new DateTimeImmutable()
)
) {
$reidentified++;
@@ -60,11 +67,15 @@ public function process(SymfonyStyle $inputOutput): void
}
}
- $inputOutput->writeln(sprintf('%d out of %d processed', $count, $total));
- $inputOutput->writeln(sprintf(
- '%d bounces were re-processed and %d bounces were re-identified',
- $reparsed,
- $reidentified
+ $inputOutput->writeln($this->translator->trans('%count% out of %total% processed', [
+ '%count%' => $count,
+ '%total%' => $total
+ ]));
+ $inputOutput->writeln($this->translator->trans(
+ '%reparsed% bounces were re-processed and %reidentified% bounces were re-identified', [
+ '%reparsed%' => $reparsed,
+ '%reidentified%' => $reidentified,
+ ]
));
}
}
diff --git a/tests/Unit/Domain/Messaging/Service/Processor/UnidentifiedBounceReprocessorTest.php b/tests/Unit/Domain/Messaging/Service/Processor/UnidentifiedBounceReprocessorTest.php
index a671e74c..ac1c9173 100644
--- a/tests/Unit/Domain/Messaging/Service/Processor/UnidentifiedBounceReprocessorTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Processor/UnidentifiedBounceReprocessorTest.php
@@ -13,6 +13,7 @@
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\Translation\Translator;
class UnidentifiedBounceReprocessorTest extends TestCase
{
@@ -62,7 +63,8 @@ public function testProcess(): void
$processor = new UnidentifiedBounceReprocessor(
bounceManager: $this->bounceManager,
messageParser: $this->messageParser,
- bounceDataProcessor: $this->dataProcessor
+ bounceDataProcessor: $this->dataProcessor,
+ translator: new Translator('en'),
);
$processor->process($this->io);
}
From abb899f3c89c80dd469bad8ea3bcb0d637d95c42 Mon Sep 17 00:00:00 2001
From: Tatevik
Date: Sat, 20 Sep 2025 17:11:24 +0400
Subject: [PATCH 15/17] Style fix
---
.../Exception/InvalidContextTypeException.php | 2 +-
.../Exception/InvalidDtoTypeException.php | 2 +-
.../PasswordResetMessageHandler.php | 34 ++++++++-----------
.../SubscriberConfirmationMessageHandler.php | 23 +++++--------
.../AdvancedBounceRulesProcessor.php | 12 +++----
.../Service/Processor/BounceDataProcessor.php | 13 +++++--
.../UnidentifiedBounceReprocessor.php | 6 ++--
.../Processor/MboxBounceProcessorTest.php | 10 ++++--
8 files changed, 51 insertions(+), 51 deletions(-)
diff --git a/src/Domain/Messaging/Exception/InvalidContextTypeException.php b/src/Domain/Messaging/Exception/InvalidContextTypeException.php
index d21d8c8f..0732355a 100644
--- a/src/Domain/Messaging/Exception/InvalidContextTypeException.php
+++ b/src/Domain/Messaging/Exception/InvalidContextTypeException.php
@@ -10,6 +10,6 @@ class InvalidContextTypeException extends LogicException
{
public function __construct(string $type)
{
- parent::__construct("Invalid context type: $type");
+ parent::__construct('Invalid context type: ' . $type);
}
}
diff --git a/src/Domain/Messaging/Exception/InvalidDtoTypeException.php b/src/Domain/Messaging/Exception/InvalidDtoTypeException.php
index 273d4e4b..2db09cf5 100644
--- a/src/Domain/Messaging/Exception/InvalidDtoTypeException.php
+++ b/src/Domain/Messaging/Exception/InvalidDtoTypeException.php
@@ -10,6 +10,6 @@ class InvalidDtoTypeException extends LogicException
{
public function __construct(string $type)
{
- parent::__construct("Invalid dto type: $type");
+ parent::__construct('Invalid dto type: ' . $type);
}
}
diff --git a/src/Domain/Messaging/MessageHandler/PasswordResetMessageHandler.php b/src/Domain/Messaging/MessageHandler/PasswordResetMessageHandler.php
index 56f11c01..7d2a3096 100644
--- a/src/Domain/Messaging/MessageHandler/PasswordResetMessageHandler.php
+++ b/src/Domain/Messaging/MessageHandler/PasswordResetMessageHandler.php
@@ -32,36 +32,30 @@ public function __invoke(PasswordResetMessage $message): void
$confirmationLink = $this->generateLink($message->getToken());
$subject = $this->translator->trans('Password Reset Request');
+
$textContent = $this->translator->trans(
- << $message->getToken()]
);
$htmlContent = $this->translator->trans(
- <<Password Reset Request!
-Hello! A password reset has been requested for your account.
-Please use the following token to reset your password:
-Reset Password
-If you did not request this password reset, please ignore this email.
-Thank you.
-HTML,
+ 'Password Reset Request!
' .
+ 'Hello! A password reset has been requested for your account.
' .
+ 'Please use the following token to reset your password:
' .
+ 'Reset Password
' .
+ 'If you did not request this password reset, please ignore this email.
' .
+ 'Thank you.
',
[
'%confirmation_link%' => $confirmationLink,
]
);
+
$email = (new Email())
->to($message->getEmail())
->subject($subject)
diff --git a/src/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandler.php b/src/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandler.php
index 20a70ba5..69ec42cb 100644
--- a/src/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandler.php
+++ b/src/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandler.php
@@ -37,15 +37,10 @@ public function __invoke(SubscriberConfirmationMessage $message): void
$subject = $this->translator->trans('Please confirm your subscription');
$textContent = $this->translator->trans(
- << $confirmationLink
]
@@ -54,12 +49,10 @@ public function __invoke(SubscriberConfirmationMessage $message): void
$htmlContent = '';
if ($message->hasHtmlEmail()) {
$htmlContent = $this->translator->trans(
- <<Thank you for subscribing!
-Please confirm your subscription by clicking the link below:
-Confirm Subscription
-If you did not request this subscription, please ignore this email.
-HTML,
+ 'Thank you for subscribing!
' .
+ 'Please confirm your subscription by clicking the link below:
' .
+ 'Confirm Subscription
' .
+ 'If you did not request this subscription, please ignore this email.
',
[
'%confirmation_link%' => $confirmationLink,
]
diff --git a/src/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessor.php b/src/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessor.php
index 7129401f..0e1c3fe0 100644
--- a/src/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessor.php
+++ b/src/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessor.php
@@ -73,18 +73,18 @@ public function process(SymfonyStyle $io, int $batchSize): void
}
$io->writeln($this->translator->trans(
- 'Processed %processed% out of %total% bounces for advanced bounce rules', [
- '%processed%' => min($processed, $total),
- '%total%' => $total,
- ]
+ 'Processed %processed% out of %total% bounces for advanced bounce rules',
+ ['%processed%' => min($processed, $total), '%total%' => $total]
));
}
$io->writeln($this->translator->trans(
- '%processed% bounces processed by advanced processing', ['%processed%' => $matched]
+ '%processed% bounces processed by advanced processing',
+ ['%processed%' => $matched]
));
$io->writeln($this->translator->trans(
- '%not_processed% bounces were not matched by advanced processing rules', ['%not_processed%' => $notMatched]
+ '%not_processed% bounces were not matched by advanced processing rules',
+ ['%not_processed%' => $notMatched]
));
}
diff --git a/src/Domain/Messaging/Service/Processor/BounceDataProcessor.php b/src/Domain/Messaging/Service/Processor/BounceDataProcessor.php
index 51152f84..7a33a7e9 100644
--- a/src/Domain/Messaging/Service/Processor/BounceDataProcessor.php
+++ b/src/Domain/Messaging/Service/Processor/BounceDataProcessor.php
@@ -53,7 +53,12 @@ public function process(Bounce $bounce, ?string $msgId, ?int $userId, DateTimeIm
}
if ($msgId && $userId) {
- return $this->handleKnownMessageAndUser(bounce: $bounce, date: $bounceDate, msgId: (int)$msgId, userId: $userId);
+ return $this->handleKnownMessageAndUser(
+ bounce: $bounce,
+ date: $bounceDate,
+ msgId: (int)$msgId,
+ userId: $userId
+ );
}
if ($userId) {
@@ -64,7 +69,11 @@ public function process(Bounce $bounce, ?string $msgId, ?int $userId, DateTimeIm
return $this->handleMessageOnly(bounce: $bounce, msgId: (int)$msgId);
}
- $this->bounceManager->update(bounce: $bounce, status: BounceStatus::UnidentifiedBounce->value, comment: 'not processed');
+ $this->bounceManager->update(
+ bounce: $bounce,
+ status: BounceStatus::UnidentifiedBounce->value,
+ comment: 'not processed'
+ );
return false;
}
diff --git a/src/Domain/Messaging/Service/Processor/UnidentifiedBounceReprocessor.php b/src/Domain/Messaging/Service/Processor/UnidentifiedBounceReprocessor.php
index 8a971639..2646ede6 100644
--- a/src/Domain/Messaging/Service/Processor/UnidentifiedBounceReprocessor.php
+++ b/src/Domain/Messaging/Service/Processor/UnidentifiedBounceReprocessor.php
@@ -72,10 +72,8 @@ public function process(SymfonyStyle $inputOutput): void
'%total%' => $total
]));
$inputOutput->writeln($this->translator->trans(
- '%reparsed% bounces were re-processed and %reidentified% bounces were re-identified', [
- '%reparsed%' => $reparsed,
- '%reidentified%' => $reidentified,
- ]
+ '%reparsed% bounces were re-processed and %reidentified% bounces were re-identified',
+ ['%reparsed%' => $reparsed, '%reidentified%' => $reidentified]
));
}
}
diff --git a/tests/Unit/Domain/Messaging/Service/Processor/MboxBounceProcessorTest.php b/tests/Unit/Domain/Messaging/Service/Processor/MboxBounceProcessorTest.php
index f1c62f6e..9bf1c92f 100644
--- a/tests/Unit/Domain/Messaging/Service/Processor/MboxBounceProcessorTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Processor/MboxBounceProcessorTest.php
@@ -65,8 +65,14 @@ public function testProcessSuccess(): void
['mailbox', '/var/mail/bounce.mbox'],
]);
- $this->io->expects($this->once())->method('section')->with($translator->trans('Opening mbox %file%', ['%file%' => '/var/mail/bounce.mbox']));
- $this->io->expects($this->once())->method('writeln')->with($translator->trans('Please do not interrupt this process'));
+ $this->io
+ ->expects($this->once())
+ ->method('section')
+ ->with($translator->trans('Opening mbox %file%', ['%file%' => '/var/mail/bounce.mbox']));
+ $this->io
+ ->expects($this->once())
+ ->method('writeln')
+ ->with($translator->trans('Please do not interrupt this process'));
$this->service->expects($this->once())
->method('processMailbox')
From 0a378e9d43c53bd48d0adeb80d6675eabfa13e06 Mon Sep 17 00:00:00 2001
From: Tatevik
Date: Sat, 20 Sep 2025 17:23:14 +0400
Subject: [PATCH 16/17] Test fix
---
.../Service/Builder/MessageBuilderTest.php | 26 ++++++++++---------
.../Builder/MessageContentBuilderTest.php | 4 +--
.../Builder/MessageFormatBuilderTest.php | 4 +--
.../Builder/MessageOptionsBuilderTest.php | 4 +--
.../Builder/MessageScheduleBuilderTest.php | 4 +--
5 files changed, 22 insertions(+), 20 deletions(-)
diff --git a/tests/Unit/Domain/Messaging/Service/Builder/MessageBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/MessageBuilderTest.php
index d99d041a..d08ee9a1 100644
--- a/tests/Unit/Domain/Messaging/Service/Builder/MessageBuilderTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Builder/MessageBuilderTest.php
@@ -4,8 +4,8 @@
namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Builder;
-use InvalidArgumentException;
use PhpList\Core\Domain\Identity\Model\Administrator;
+use PhpList\Core\Domain\Messaging\Exception\InvalidContextTypeException;
use PhpList\Core\Domain\Messaging\Model\Dto\CreateMessageDto;
use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageContentDto;
use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageFormatDto;
@@ -14,6 +14,8 @@
use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageScheduleDto;
use PhpList\Core\Domain\Messaging\Model\Dto\MessageContext;
use PhpList\Core\Domain\Messaging\Model\Message;
+use PhpList\Core\Domain\Messaging\Model\Message\MessageContent;
+use PhpList\Core\Domain\Messaging\Model\Message\MessageSchedule;
use PhpList\Core\Domain\Messaging\Repository\TemplateRepository;
use PhpList\Core\Domain\Messaging\Service\Builder\MessageBuilder;
use PhpList\Core\Domain\Messaging\Service\Builder\MessageContentBuilder;
@@ -40,11 +42,11 @@ protected function setUp(): void
$this->optionsBuilder = $this->createMock(MessageOptionsBuilder::class);
$this->builder = new MessageBuilder(
- $templateRepository,
- $this->formatBuilder,
- $this->scheduleBuilder,
- $this->contentBuilder,
- $this->optionsBuilder
+ templateRepository: $templateRepository,
+ messageFormatBuilder: $this->formatBuilder,
+ messageScheduleBuilder: $this->scheduleBuilder,
+ messageContentBuilder: $this->contentBuilder,
+ messageOptionsBuilder: $this->optionsBuilder
);
}
@@ -92,12 +94,12 @@ private function mockBuildCalls(CreateMessageDto $createMessageDto): void
$this->scheduleBuilder->expects($this->once())
->method('build')
->with($createMessageDto->schedule)
- ->willReturn($this->createMock(\PhpList\Core\Domain\Messaging\Model\Message\MessageSchedule::class));
+ ->willReturn($this->createMock(MessageSchedule::class));
$this->contentBuilder->expects($this->once())
->method('build')
->with($createMessageDto->content)
- ->willReturn($this->createMock(\PhpList\Core\Domain\Messaging\Model\Message\MessageContent::class));
+ ->willReturn($this->createMock(MessageContent::class));
$this->optionsBuilder->expects($this->once())
->method('build')
@@ -113,12 +115,12 @@ public function testBuildsNewMessage(): void
$this->mockBuildCalls($request);
- $this->builder->build($request, $context);
+ $this->builder->build(createMessageDto: $request, context: $context);
}
public function testThrowsExceptionOnInvalidContext(): void
{
- $this->expectException(InvalidArgumentException::class);
+ $this->expectException(InvalidContextTypeException::class);
$this->builder->build($this->createMock(CreateMessageDto::class), new \stdClass());
}
@@ -139,11 +141,11 @@ public function testUpdatesExistingMessage(): void
$existingMessage
->expects($this->once())
->method('setSchedule')
- ->with($this->isInstanceOf(\PhpList\Core\Domain\Messaging\Model\Message\MessageSchedule::class));
+ ->with($this->isInstanceOf(MessageSchedule::class));
$existingMessage
->expects($this->once())
->method('setContent')
- ->with($this->isInstanceOf(\PhpList\Core\Domain\Messaging\Model\Message\MessageContent::class));
+ ->with($this->isInstanceOf(MessageContent::class));
$existingMessage
->expects($this->once())
->method('setOptions')
diff --git a/tests/Unit/Domain/Messaging/Service/Builder/MessageContentBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/MessageContentBuilderTest.php
index 21f90692..62475884 100644
--- a/tests/Unit/Domain/Messaging/Service/Builder/MessageContentBuilderTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Builder/MessageContentBuilderTest.php
@@ -4,7 +4,7 @@
namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Builder;
-use InvalidArgumentException;
+use PhpList\Core\Domain\Messaging\Exception\InvalidDtoTypeException;
use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageContentDto;
use PhpList\Core\Domain\Messaging\Service\Builder\MessageContentBuilder;
use PHPUnit\Framework\TestCase;
@@ -37,7 +37,7 @@ public function testBuildsMessageContentSuccessfully(): void
public function testThrowsExceptionOnInvalidDto(): void
{
- $this->expectException(InvalidArgumentException::class);
+ $this->expectException(InvalidDtoTypeException::class);
$invalidDto = new \stdClass();
$this->builder->build($invalidDto);
diff --git a/tests/Unit/Domain/Messaging/Service/Builder/MessageFormatBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/MessageFormatBuilderTest.php
index 1bd576f5..17d93eae 100644
--- a/tests/Unit/Domain/Messaging/Service/Builder/MessageFormatBuilderTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Builder/MessageFormatBuilderTest.php
@@ -4,7 +4,7 @@
namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Builder;
-use InvalidArgumentException;
+use PhpList\Core\Domain\Messaging\Exception\InvalidDtoTypeException;
use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageFormatDto;
use PhpList\Core\Domain\Messaging\Service\Builder\MessageFormatBuilder;
use PHPUnit\Framework\TestCase;
@@ -30,7 +30,7 @@ public function testBuildsMessageFormatSuccessfully(): void
public function testThrowsExceptionOnInvalidDto(): void
{
- $this->expectException(InvalidArgumentException::class);
+ $this->expectException(InvalidDtoTypeException::class);
$invalidDto = new \stdClass();
$this->builder->build($invalidDto);
diff --git a/tests/Unit/Domain/Messaging/Service/Builder/MessageOptionsBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/MessageOptionsBuilderTest.php
index 754177a2..e2de8398 100644
--- a/tests/Unit/Domain/Messaging/Service/Builder/MessageOptionsBuilderTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Builder/MessageOptionsBuilderTest.php
@@ -4,7 +4,7 @@
namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Builder;
-use InvalidArgumentException;
+use PhpList\Core\Domain\Messaging\Exception\InvalidDtoTypeException;
use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageOptionsDto;
use PhpList\Core\Domain\Messaging\Service\Builder\MessageOptionsBuilder;
use PHPUnit\Framework\TestCase;
@@ -37,7 +37,7 @@ public function testBuildsMessageOptionsSuccessfully(): void
public function testThrowsExceptionOnInvalidDto(): void
{
- $this->expectException(InvalidArgumentException::class);
+ $this->expectException(InvalidDtoTypeException::class);
$invalidDto = new \stdClass();
$this->builder->build($invalidDto);
diff --git a/tests/Unit/Domain/Messaging/Service/Builder/MessageScheduleBuilderTest.php b/tests/Unit/Domain/Messaging/Service/Builder/MessageScheduleBuilderTest.php
index 25a89052..8e9e5fb8 100644
--- a/tests/Unit/Domain/Messaging/Service/Builder/MessageScheduleBuilderTest.php
+++ b/tests/Unit/Domain/Messaging/Service/Builder/MessageScheduleBuilderTest.php
@@ -5,7 +5,7 @@
namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Builder;
use DateTime;
-use InvalidArgumentException;
+use PhpList\Core\Domain\Messaging\Exception\InvalidDtoTypeException;
use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageScheduleDto;
use PhpList\Core\Domain\Messaging\Service\Builder\MessageScheduleBuilder;
use PHPUnit\Framework\TestCase;
@@ -40,7 +40,7 @@ public function testBuildsMessageScheduleSuccessfully(): void
public function testThrowsExceptionOnInvalidDto(): void
{
- $this->expectException(InvalidArgumentException::class);
+ $this->expectException(InvalidDtoTypeException::class);
$invalidDto = new \stdClass();
$this->builder->build($invalidDto);
From fb30e723c5a0434be27cd07e006449b1030a0b2a Mon Sep 17 00:00:00 2001
From: Tatevik
Date: Mon, 22 Sep 2025 11:50:55 +0400
Subject: [PATCH 17/17] ConsecutiveBounceHandler
---
resources/translations/messages.en.xlf | 118 +++++++++++++++++-
.../Exception/ImapConnectionException.php | 16 +++
.../Exception/OpenMboxFileException.php | 16 +++
.../Service/ConsecutiveBounceHandler.php | 26 ++--
.../Service/MaxProcessTimeLimiter.php | 10 +-
.../Service/MessageProcessingPreparator.php | 14 ++-
.../Service/NativeBounceProcessingService.php | 11 +-
.../Messaging/Service/SendRateLimiter.php | 8 +-
.../WebklexBounceProcessingService.php | 13 +-
.../Validator/TemplateImageValidator.php | 21 +++-
.../Validator/TemplateLinkValidator.php | 12 +-
.../CouldNotReadUploadedFileException.php | 12 ++
.../SubscriberAttributeCreationException.php | 2 +-
.../Subscription/Service/CsvImporter.php | 6 +-
.../Manager/AttributeDefinitionManager.php | 16 ++-
.../Service/Manager/SubscribePageManager.php | 4 +-
.../Manager/SubscriberAttributeManager.php | 6 +-
.../Service/Manager/SubscriberManager.php | 6 +-
.../Service/SubscriberBlacklistService.php | 6 +-
.../Service/SubscriberCsvImporter.php | 24 +++-
.../Validator/AttributeTypeValidator.php | 17 ++-
.../Service/ConsecutiveBounceHandlerTest.php | 10 +-
.../Service/MaxProcessTimeLimiterTest.php | 5 +-
.../MessageProcessingPreparatorTest.php | 15 ++-
.../Messaging/Service/SendRateLimiterTest.php | 4 +
.../Validator/TemplateImageValidatorTest.php | 3 +-
.../Validator/TemplateLinkValidatorTest.php | 3 +-
.../AttributeDefinitionManagerTest.php | 31 ++++-
.../Manager/SubscribePageManagerTest.php | 2 +
.../SubscriberAttributeManagerTest.php | 11 +-
.../Service/Manager/SubscriberManagerTest.php | 2 +
.../Service/SubscriberCsvImporterTest.php | 12 +-
.../Validator/AttributeTypeValidatorTest.php | 3 +-
33 files changed, 379 insertions(+), 86 deletions(-)
create mode 100644 src/Domain/Messaging/Exception/ImapConnectionException.php
create mode 100644 src/Domain/Messaging/Exception/OpenMboxFileException.php
create mode 100644 src/Domain/Subscription/Exception/CouldNotReadUploadedFileException.php
diff --git a/resources/translations/messages.en.xlf b/resources/translations/messages.en.xlf
index c832b660..6f128be1 100644
--- a/resources/translations/messages.en.xlf
+++ b/resources/translations/messages.en.xlf
@@ -313,13 +313,80 @@
%reparsed% bounces were re-processed and %reidentified% bounces were re-identified
+
+ Identifying consecutive bounces
+ Identifying consecutive bounces
+
+
+ Nothing to do
+ Nothing to do
+
+
+ Processed %processed% out of %total% subscribers
+ Processed %processed% out of %total% subscribers
+
+
+ Total of %total% subscribers processed
+ Total of %total% subscribers processed
+
+
+ Subscriber auto unconfirmed for %count% consecutive bounces
+ Subscriber auto unconfirmed for %count% consecutive bounces
+
+
+ %count% consecutive bounces, threshold reached
+ %count% consecutive bounces, threshold reached
+
+
+
+ Reached max processing time; stopping cleanly.
+ Reached max processing time; stopping cleanly.
+
+
+
+ Giving a UUID to %count% subscribers, this may take a while
+ Giving a UUID to %count% subscribers, this may take a while
+
+
+ Giving a UUID to %count% campaigns
+ Giving a UUID to %count% campaigns
+
+
+
+ Batch limit reached, sleeping %sleep%s to respect MAILQUEUE_BATCH_PERIOD
+ Batch limit reached, sleeping %sleep%s to respect MAILQUEUE_BATCH_PERIOD
+
+
+
+ Value must be an array of image URLs.
+ Value must be an array of image URLs.
+
+
+ Image "%url%" is not a full URL.
+ Image "%url%" is not a full URL.
+
+
+ Image "%url%" does not exist (HTTP %code%)
+ Image "%url%" does not exist (HTTP %code%)
+
+
+ Image "%url%" could not be validated: %message%
+ Image "%url%" could not be validated: %message%
+
+
+
+ Not full URLs: %urls%
+ Not full URLs: %urls%
+
+
+
Subscriber list not found.
Subscriber list not found.
-
+
Subscriber does not exists.
Subscriber does not exists.
@@ -328,6 +395,55 @@
Subscription not found for this subscriber and list.
Subscription not found for this subscriber and list.
+
+ Attribute definition already exists
+ Attribute definition already exists
+
+
+ Another attribute with this name already exists.
+ Another attribute with this name already exists.
+
+
+
+ Subscribe page not found
+ Subscribe page not found
+
+
+ Value is required
+ Value is required
+
+
+ Subscriber not found
+ Subscriber not found
+
+
+ Unexpected error: %error%
+ Unexpected error: %error%
+
+
+ Added to blacklist for reason %reason%
+ Added to blacklist for reason %reason%
+
+
+ Could not read the uploaded file.
+ Could not read the uploaded file.
+
+
+ Error processing %email%: %error%
+ Error processing %email%: %error%
+
+
+ General import error: %error%
+ General import error: %error%
+
+
+ Value must be a string.
+ Value must be a string.
+
+
+ Invalid attribute type: "%type%". Valid types are: %valid_types%
+ Invalid attribute type: "%type%". Valid types are: %valid_types%
+
diff --git a/src/Domain/Messaging/Exception/ImapConnectionException.php b/src/Domain/Messaging/Exception/ImapConnectionException.php
new file mode 100644
index 00000000..8e5295e2
--- /dev/null
+++ b/src/Domain/Messaging/Exception/ImapConnectionException.php
@@ -0,0 +1,16 @@
+subscriberRepository = $subscriberRepository;
$this->subscriberHistoryManager = $subscriberHistoryManager;
$this->blacklistService = $blacklistService;
+ $this->translator = $translator;
$this->unsubscribeThreshold = $unsubscribeThreshold;
$this->blacklistThreshold = $blacklistThreshold;
}
public function handle(SymfonyStyle $io): void
{
- $io->section('Identifying consecutive bounces');
+ $io->section($this->translator->trans('Identifying consecutive bounces'));
$users = $this->subscriberRepository->distinctUsersWithBouncesConfirmedNotBlacklisted();
$total = count($users);
if ($total === 0) {
- $io->writeln('Nothing to do');
+ $io->writeln($this->translator->trans('Nothing to do'));
+
return;
}
@@ -57,11 +62,14 @@ public function handle(SymfonyStyle $io): void
$processed++;
if ($processed % 5 === 0) {
- $io->writeln(\sprintf('processed %d out of %d subscribers', $processed, $total));
+ $io->writeln($this->translator->trans('Processed %processed% out of %total% subscribers', [
+ '%processed%' => $processed,
+ '%total%' => $total,
+ ]));
}
}
- $io->writeln(\sprintf('total of %d subscribers processed', $total));
+ $io->writeln($this->translator->trans('Total of %total% subscribers processed', ['%total%' => $total]));
}
private function processUser(Subscriber $user): void
@@ -123,15 +131,19 @@ private function applyThresholdActions($user, int $consecutive, bool $alreadyUns
$this->subscriberRepository->markUnconfirmed($user->getId());
$this->subscriberHistoryManager->addHistory(
subscriber: $user,
- message: 'Auto Unconfirmed',
- details: sprintf('Subscriber auto unconfirmed for %d consecutive bounces', $consecutive)
+ message: $this->translator->trans('Auto unconfirmed'),
+ details: $this->translator->trans('Subscriber auto unconfirmed for %count% consecutive bounces', [
+ '%count%' => $consecutive
+ ])
);
}
if ($this->blacklistThreshold > 0 && $consecutive >= $this->blacklistThreshold) {
$this->blacklistService->blacklist(
subscriber: $user,
- reason: sprintf('%d consecutive bounces, threshold reached', $consecutive)
+ reason: $this->translator->trans('%count% consecutive bounces, threshold reached', [
+ '%count%' => $consecutive
+ ])
);
return true;
}
diff --git a/src/Domain/Messaging/Service/MaxProcessTimeLimiter.php b/src/Domain/Messaging/Service/MaxProcessTimeLimiter.php
index c5269aaa..b3de16f9 100644
--- a/src/Domain/Messaging/Service/MaxProcessTimeLimiter.php
+++ b/src/Domain/Messaging/Service/MaxProcessTimeLimiter.php
@@ -6,6 +6,7 @@
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Limits the total processing time of a long-running operation.
@@ -15,8 +16,11 @@ class MaxProcessTimeLimiter
private float $startedAt = 0.0;
private int $maxSeconds;
- public function __construct(private readonly LoggerInterface $logger, ?int $maxSeconds = null)
- {
+ public function __construct(
+ private readonly LoggerInterface $logger,
+ private readonly TranslatorInterface $translator,
+ ?int $maxSeconds = null
+ ) {
$this->maxSeconds = $maxSeconds ?? 600;
}
@@ -36,7 +40,7 @@ public function shouldStop(?OutputInterface $output = null): bool
$elapsed = microtime(true) - $this->startedAt;
if ($elapsed >= $this->maxSeconds) {
$this->logger->warning(sprintf('Reached max processing time of %d seconds', $this->maxSeconds));
- $output?->writeln('Reached max processing time; stopping cleanly.');
+ $output?->writeln($this->translator->trans('Reached max processing time; stopping cleanly.'));
return true;
}
diff --git a/src/Domain/Messaging/Service/MessageProcessingPreparator.php b/src/Domain/Messaging/Service/MessageProcessingPreparator.php
index c602f7d4..9faa72fb 100644
--- a/src/Domain/Messaging/Service/MessageProcessingPreparator.php
+++ b/src/Domain/Messaging/Service/MessageProcessingPreparator.php
@@ -10,6 +10,7 @@
use PhpList\Core\Domain\Messaging\Repository\MessageRepository;
use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository;
use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
class MessageProcessingPreparator
{
@@ -20,17 +21,20 @@ class MessageProcessingPreparator
private SubscriberRepository $subscriberRepository;
private MessageRepository $messageRepository;
private LinkTrackService $linkTrackService;
+ private TranslatorInterface $translator;
public function __construct(
EntityManagerInterface $entityManager,
SubscriberRepository $subscriberRepository,
MessageRepository $messageRepository,
- LinkTrackService $linkTrackService
+ LinkTrackService $linkTrackService,
+ TranslatorInterface $translator,
) {
$this->entityManager = $entityManager;
$this->subscriberRepository = $subscriberRepository;
$this->messageRepository = $messageRepository;
$this->linkTrackService = $linkTrackService;
+ $this->translator = $translator;
}
public function ensureSubscribersHaveUuid(OutputInterface $output): void
@@ -39,7 +43,9 @@ public function ensureSubscribersHaveUuid(OutputInterface $output): void
$numSubscribers = count($subscribersWithoutUuid);
if ($numSubscribers > 0) {
- $output->writeln(sprintf('Giving a UUID to %d subscribers, this may take a while', $numSubscribers));
+ $output->writeln($this->translator->trans('Giving a UUID to %count% subscribers, this may take a while', [
+ '%count%' => $numSubscribers
+ ]));
foreach ($subscribersWithoutUuid as $subscriber) {
$subscriber->setUniqueId(bin2hex(random_bytes(16)));
}
@@ -53,7 +59,9 @@ public function ensureCampaignsHaveUuid(OutputInterface $output): void
$numCampaigns = count($campaignsWithoutUuid);
if ($numCampaigns > 0) {
- $output->writeln(sprintf('Giving a UUID to %d campaigns', $numCampaigns));
+ $output->writeln($this->translator->trans('Giving a UUID to %count% campaigns', [
+ '%count%' => $numCampaigns
+ ]));
foreach ($campaignsWithoutUuid as $campaign) {
$campaign->setUuid(bin2hex(random_bytes(18)));
}
diff --git a/src/Domain/Messaging/Service/NativeBounceProcessingService.php b/src/Domain/Messaging/Service/NativeBounceProcessingService.php
index eee5bb98..0cdc7cb4 100644
--- a/src/Domain/Messaging/Service/NativeBounceProcessingService.php
+++ b/src/Domain/Messaging/Service/NativeBounceProcessingService.php
@@ -6,10 +6,10 @@
use IMAP\Connection;
use PhpList\Core\Domain\Common\Mail\NativeImapMailReader;
+use PhpList\Core\Domain\Messaging\Exception\OpenMboxFileException;
use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager;
use PhpList\Core\Domain\Messaging\Service\Processor\BounceDataProcessor;
use Psr\Log\LoggerInterface;
-use RuntimeException;
use Throwable;
class NativeBounceProcessingService implements BounceProcessingServiceInterface
@@ -69,9 +69,12 @@ private function openOrFail(string $mailbox, bool $testMode): Connection
{
try {
return $this->mailReader->open($mailbox, $testMode ? 0 : CL_EXPUNGE);
- } catch (Throwable $e) {
- $this->logger->error('Cannot open mailbox file: '.$e->getMessage());
- throw new RuntimeException('Cannot open mbox file');
+ } catch (Throwable $throwable) {
+ $this->logger->error('Cannot open mailbox file', [
+ 'mailbox' => $mailbox,
+ 'error' => $throwable->getMessage(),
+ ]);
+ throw new OpenMboxFileException($throwable);
}
}
diff --git a/src/Domain/Messaging/Service/SendRateLimiter.php b/src/Domain/Messaging/Service/SendRateLimiter.php
index 378b80d5..2590e721 100644
--- a/src/Domain/Messaging/Service/SendRateLimiter.php
+++ b/src/Domain/Messaging/Service/SendRateLimiter.php
@@ -9,6 +9,7 @@
use PhpList\Core\Domain\Common\IspRestrictionsProvider;
use PhpList\Core\Domain\Messaging\Repository\UserMessageRepository;
use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Encapsulates batching and throttling logic for sending emails respecting
@@ -26,6 +27,7 @@ class SendRateLimiter
public function __construct(
private readonly IspRestrictionsProvider $ispRestrictionsProvider,
private readonly UserMessageRepository $userMessageRepository,
+ private readonly TranslatorInterface $translator,
private readonly ?int $mailqueueBatchSize = null,
private readonly ?int $mailqueueBatchPeriod = null,
private readonly ?int $mailqueueThrottle = null,
@@ -76,9 +78,9 @@ public function awaitTurn(?OutputInterface $output = null): bool
$elapsed = microtime(true) - $this->batchStart;
$remaining = (int)ceil($this->batchPeriod - $elapsed);
if ($remaining > 0) {
- $output?->writeln(sprintf(
- 'Batch limit reached, sleeping %ds to respect MAILQUEUE_BATCH_PERIOD',
- $remaining
+ $output?->writeln($this->translator->trans(
+ 'Batch limit reached, sleeping %sleep%s to respect MAILQUEUE_BATCH_PERIOD',
+ ['%sleep%' => $remaining]
));
sleep($remaining);
}
diff --git a/src/Domain/Messaging/Service/WebklexBounceProcessingService.php b/src/Domain/Messaging/Service/WebklexBounceProcessingService.php
index 01a94aff..09a1c14a 100644
--- a/src/Domain/Messaging/Service/WebklexBounceProcessingService.php
+++ b/src/Domain/Messaging/Service/WebklexBounceProcessingService.php
@@ -6,10 +6,10 @@
use DateTimeImmutable;
use DateTimeInterface;
+use PhpList\Core\Domain\Messaging\Exception\ImapConnectionException;
use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager;
use PhpList\Core\Domain\Messaging\Service\Processor\BounceDataProcessor;
use Psr\Log\LoggerInterface;
-use RuntimeException;
use Throwable;
use Webklex\PHPIMAP\Client;
use Webklex\PHPIMAP\Folder;
@@ -50,7 +50,7 @@ public function __construct(
*
* $mailbox: IMAP host; if you pass "host#FOLDER", FOLDER will be used instead of INBOX.
*
- * @throws RuntimeException If connection to the IMAP server cannot be established.
+ * @throws ImapConnectionException If connection to the IMAP server cannot be established.
*/
public function processMailbox(
string $mailbox,
@@ -61,9 +61,12 @@ public function processMailbox(
try {
$client->connect();
- } catch (Throwable $e) {
- $this->logger->error('Cannot connect to mailbox: '.$e->getMessage());
- throw new RuntimeException('Cannot connect to IMAP server');
+ } catch (Throwable $throwable) {
+ $this->logger->error('Cannot connect to mailbox', [
+ 'mailbox' => $mailbox,
+ 'error' => $throwable->getMessage()
+ ]);
+ throw new ImapConnectionException($throwable);
}
try {
diff --git a/src/Domain/Messaging/Validator/TemplateImageValidator.php b/src/Domain/Messaging/Validator/TemplateImageValidator.php
index 11bcc329..5e50e075 100644
--- a/src/Domain/Messaging/Validator/TemplateImageValidator.php
+++ b/src/Domain/Messaging/Validator/TemplateImageValidator.php
@@ -9,18 +9,21 @@
use PhpList\Core\Domain\Common\Model\ValidationContext;
use PhpList\Core\Domain\Common\Validator\ValidatorInterface;
use Symfony\Component\Validator\Exception\ValidatorException;
+use Symfony\Contracts\Translation\TranslatorInterface;
use Throwable;
class TemplateImageValidator implements ValidatorInterface
{
- public function __construct(private readonly ClientInterface $httpClient)
- {
+ public function __construct(
+ private readonly ClientInterface $httpClient,
+ private readonly TranslatorInterface $translator,
+ ) {
}
public function validate(mixed $value, ValidationContext $context = null): void
{
if (!is_array($value)) {
- throw new InvalidArgumentException('Value must be an array of image URLs.');
+ throw new InvalidArgumentException($this->translator->trans('Value must be an array of image URLs.'));
}
$checkFull = $context?->get('checkImages', false);
@@ -42,7 +45,7 @@ private function validateFullUrls(array $urls): array
foreach ($urls as $url) {
if (!preg_match('#^https?://#i', $url)) {
- $errors[] = sprintf('Image "%s" is not a full URL.', $url);
+ $errors[] = $this->translator->trans('Image "%url%" is not a full URL.', ['%url%' => $url]);
}
}
@@ -61,10 +64,16 @@ private function validateExistence(array $urls): array
try {
$response = $this->httpClient->request('HEAD', $url);
if ($response->getStatusCode() !== 200) {
- $errors[] = sprintf('Image "%s" does not exist (HTTP %s)', $url, $response->getStatusCode());
+ $errors[] = $this->translator->trans('Image "%url%" does not exist (HTTP %code%)', [
+ '%url%' => $url,
+ '%code%' => $response->getStatusCode()
+ ]);
}
} catch (Throwable $e) {
- $errors[] = sprintf('Image "%s" could not be validated: %s', $url, $e->getMessage());
+ $errors[] = $this->translator->trans('Image "%url%" could not be validated: %message%', [
+ '%url%' => $url,
+ '%message%' => $e->getMessage()
+ ]);
}
}
diff --git a/src/Domain/Messaging/Validator/TemplateLinkValidator.php b/src/Domain/Messaging/Validator/TemplateLinkValidator.php
index 18c772df..621f35a7 100644
--- a/src/Domain/Messaging/Validator/TemplateLinkValidator.php
+++ b/src/Domain/Messaging/Validator/TemplateLinkValidator.php
@@ -8,9 +8,14 @@
use PhpList\Core\Domain\Common\Model\ValidationContext;
use PhpList\Core\Domain\Common\Validator\ValidatorInterface;
use Symfony\Component\Validator\Exception\ValidatorException;
+use Symfony\Contracts\Translation\TranslatorInterface;
class TemplateLinkValidator implements ValidatorInterface
{
+ public function __construct(private readonly TranslatorInterface $translator)
+ {
+ }
+
private const PLACEHOLDERS = [
'[PREFERENCESURL]',
'[UNSUBSCRIBEURL]',
@@ -37,10 +42,9 @@ public function validate(mixed $value, ValidationContext $context = null): void
}
if (!empty($invalid)) {
- throw new ValidatorException(sprintf(
- 'Not full URLs: %s',
- implode(', ', $invalid)
- ));
+ throw new ValidatorException(
+ $this->translator->trans('Not full URLs: %urls%', ['%urls%' => implode(', ', $invalid)]),
+ );
}
}
diff --git a/src/Domain/Subscription/Exception/CouldNotReadUploadedFileException.php b/src/Domain/Subscription/Exception/CouldNotReadUploadedFileException.php
new file mode 100644
index 00000000..9a9d9c8b
--- /dev/null
+++ b/src/Domain/Subscription/Exception/CouldNotReadUploadedFileException.php
@@ -0,0 +1,12 @@
+statusCode = $statusCode;
diff --git a/src/Domain/Subscription/Service/CsvImporter.php b/src/Domain/Subscription/Service/CsvImporter.php
index 3b3729e3..01fb51ea 100644
--- a/src/Domain/Subscription/Service/CsvImporter.php
+++ b/src/Domain/Subscription/Service/CsvImporter.php
@@ -8,6 +8,7 @@
use PhpList\Core\Domain\Subscription\Model\Dto\ImportSubscriberDto;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use League\Csv\Exception as CsvException;
+use Symfony\Contracts\Translation\TranslatorInterface;
use Throwable;
class CsvImporter
@@ -15,6 +16,7 @@ class CsvImporter
public function __construct(
private readonly CsvRowToDtoMapper $rowMapper,
private readonly ValidatorInterface $validator,
+ private readonly TranslatorInterface $translator,
) {
}
@@ -46,7 +48,9 @@ public function import(string $csvFilePath): array
$validDtos[] = $dto;
} catch (Throwable $e) {
- $errors[$index + 1][] = 'Unexpected error: ' . $e->getMessage();
+ $errors[$index + 1][] = $this->translator->trans('Unexpected error: %error%', [
+ '%error%' => $e->getMessage()
+ ]);
}
}
diff --git a/src/Domain/Subscription/Service/Manager/AttributeDefinitionManager.php b/src/Domain/Subscription/Service/Manager/AttributeDefinitionManager.php
index d8983e65..d91956c6 100644
--- a/src/Domain/Subscription/Service/Manager/AttributeDefinitionManager.php
+++ b/src/Domain/Subscription/Service/Manager/AttributeDefinitionManager.php
@@ -9,25 +9,32 @@
use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition;
use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeDefinitionRepository;
use PhpList\Core\Domain\Subscription\Validator\AttributeTypeValidator;
+use Symfony\Contracts\Translation\TranslatorInterface;
class AttributeDefinitionManager
{
private SubscriberAttributeDefinitionRepository $definitionRepository;
private AttributeTypeValidator $attributeTypeValidator;
+ private TranslatorInterface $translator;
public function __construct(
SubscriberAttributeDefinitionRepository $definitionRepository,
- AttributeTypeValidator $attributeTypeValidator
+ AttributeTypeValidator $attributeTypeValidator,
+ TranslatorInterface $translator,
) {
$this->definitionRepository = $definitionRepository;
$this->attributeTypeValidator = $attributeTypeValidator;
+ $this->translator = $translator;
}
public function create(AttributeDefinitionDto $attributeDefinitionDto): SubscriberAttributeDefinition
{
$existingAttribute = $this->definitionRepository->findOneByName($attributeDefinitionDto->name);
if ($existingAttribute) {
- throw new AttributeDefinitionCreationException('Attribute definition already exists', 409);
+ throw new AttributeDefinitionCreationException(
+ message: $this->translator->trans('Attribute definition already exists'),
+ statusCode: 409
+ );
}
$this->attributeTypeValidator->validate($attributeDefinitionDto->type);
@@ -50,7 +57,10 @@ public function update(
): SubscriberAttributeDefinition {
$existingAttribute = $this->definitionRepository->findOneByName($attributeDefinitionDto->name);
if ($existingAttribute && $existingAttribute->getId() !== $attributeDefinition->getId()) {
- throw new AttributeDefinitionCreationException('Another attribute with this name already exists.', 409);
+ throw new AttributeDefinitionCreationException(
+ message: $this->translator->trans('Another attribute with this name already exists.'),
+ statusCode: 409
+ );
}
$this->attributeTypeValidator->validate($attributeDefinitionDto->type);
diff --git a/src/Domain/Subscription/Service/Manager/SubscribePageManager.php b/src/Domain/Subscription/Service/Manager/SubscribePageManager.php
index 8e429dc4..b0017e6c 100644
--- a/src/Domain/Subscription/Service/Manager/SubscribePageManager.php
+++ b/src/Domain/Subscription/Service/Manager/SubscribePageManager.php
@@ -11,6 +11,7 @@
use PhpList\Core\Domain\Subscription\Repository\SubscriberPageDataRepository;
use PhpList\Core\Domain\Subscription\Repository\SubscriberPageRepository;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Contracts\Translation\TranslatorInterface;
class SubscribePageManager
{
@@ -18,6 +19,7 @@ public function __construct(
private readonly SubscriberPageRepository $pageRepository,
private readonly SubscriberPageDataRepository $pageDataRepository,
private readonly EntityManagerInterface $entityManager,
+ private readonly TranslatorInterface $translator,
) {
}
@@ -41,7 +43,7 @@ public function getPage(int $id): SubscribePage
/** @var SubscribePage|null $page */
$page = $this->pageRepository->find($id);
if (!$page) {
- throw new NotFoundHttpException('Subscribe page not found');
+ throw new NotFoundHttpException($this->translator->trans('Subscribe page not found'));
}
return $page;
diff --git a/src/Domain/Subscription/Service/Manager/SubscriberAttributeManager.php b/src/Domain/Subscription/Service/Manager/SubscriberAttributeManager.php
index cf83ca75..4446e0bf 100644
--- a/src/Domain/Subscription/Service/Manager/SubscriberAttributeManager.php
+++ b/src/Domain/Subscription/Service/Manager/SubscriberAttributeManager.php
@@ -10,18 +10,22 @@
use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition;
use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeValue;
use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeValueRepository;
+use Symfony\Contracts\Translation\TranslatorInterface;
class SubscriberAttributeManager
{
private SubscriberAttributeValueRepository $attributeRepository;
private EntityManagerInterface $entityManager;
+ private TranslatorInterface $translator;
public function __construct(
SubscriberAttributeValueRepository $attributeRepository,
EntityManagerInterface $entityManager,
+ TranslatorInterface $translator,
) {
$this->attributeRepository = $attributeRepository;
$this->entityManager = $entityManager;
+ $this->translator = $translator;
}
public function createOrUpdate(
@@ -38,7 +42,7 @@ public function createOrUpdate(
$value = $value ?? $definition->getDefaultValue();
if ($value === null) {
- throw new SubscriberAttributeCreationException('Value is required', 400);
+ throw new SubscriberAttributeCreationException($this->translator->trans('Value is required'));
}
$subscriberAttribute->setValue($value);
diff --git a/src/Domain/Subscription/Service/Manager/SubscriberManager.php b/src/Domain/Subscription/Service/Manager/SubscriberManager.php
index 73531fbb..25d7045a 100644
--- a/src/Domain/Subscription/Service/Manager/SubscriberManager.php
+++ b/src/Domain/Subscription/Service/Manager/SubscriberManager.php
@@ -15,6 +15,7 @@
use PhpList\Core\Domain\Subscription\Service\SubscriberDeletionService;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Messenger\MessageBusInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
class SubscriberManager
{
@@ -22,17 +23,20 @@ class SubscriberManager
private EntityManagerInterface $entityManager;
private MessageBusInterface $messageBus;
private SubscriberDeletionService $subscriberDeletionService;
+ private TranslatorInterface $translator;
public function __construct(
SubscriberRepository $subscriberRepository,
EntityManagerInterface $entityManager,
MessageBusInterface $messageBus,
SubscriberDeletionService $subscriberDeletionService,
+ TranslatorInterface $translator
) {
$this->subscriberRepository = $subscriberRepository;
$this->entityManager = $entityManager;
$this->messageBus = $messageBus;
$this->subscriberDeletionService = $subscriberDeletionService;
+ $this->translator = $translator;
}
public function createSubscriber(CreateSubscriberDto $subscriberDto): Subscriber
@@ -91,7 +95,7 @@ public function markAsConfirmedByUniqueId(string $uniqueId): Subscriber
{
$subscriber = $this->subscriberRepository->findOneByUniqueId($uniqueId);
if (!$subscriber) {
- throw new NotFoundHttpException('Subscriber not found');
+ throw new NotFoundHttpException($this->translator->trans('Subscriber not found'));
}
$subscriber->setConfirmed(true);
diff --git a/src/Domain/Subscription/Service/SubscriberBlacklistService.php b/src/Domain/Subscription/Service/SubscriberBlacklistService.php
index d9ca5ea6..3a40f042 100644
--- a/src/Domain/Subscription/Service/SubscriberBlacklistService.php
+++ b/src/Domain/Subscription/Service/SubscriberBlacklistService.php
@@ -9,6 +9,7 @@
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberBlacklistManager;
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager;
use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Contracts\Translation\TranslatorInterface;
class SubscriberBlacklistService
{
@@ -16,17 +17,20 @@ class SubscriberBlacklistService
private SubscriberBlacklistManager $blacklistManager;
private SubscriberHistoryManager $historyManager;
private RequestStack $requestStack;
+ private TranslatorInterface $translator;
public function __construct(
EntityManagerInterface $entityManager,
SubscriberBlacklistManager $blacklistManager,
SubscriberHistoryManager $historyManager,
RequestStack $requestStack,
+ TranslatorInterface $translator,
) {
$this->entityManager = $entityManager;
$this->blacklistManager = $blacklistManager;
$this->historyManager = $historyManager;
$this->requestStack = $requestStack;
+ $this->translator = $translator;
}
/**
@@ -55,7 +59,7 @@ public function blacklist(Subscriber $subscriber, string $reason): void
$this->historyManager->addHistory(
subscriber: $subscriber,
message: 'Added to blacklist',
- details: sprintf('Added to blacklist for reason %s', $reason)
+ details: $this->translator->trans('Added to blacklist for reason %reason%', ['%reason%' => $reason])
);
if (isset($GLOBALS['plugins']) && is_array($GLOBALS['plugins'])) {
diff --git a/src/Domain/Subscription/Service/SubscriberCsvImporter.php b/src/Domain/Subscription/Service/SubscriberCsvImporter.php
index 4c58f22c..c88b935e 100644
--- a/src/Domain/Subscription/Service/SubscriberCsvImporter.php
+++ b/src/Domain/Subscription/Service/SubscriberCsvImporter.php
@@ -5,6 +5,7 @@
namespace PhpList\Core\Domain\Subscription\Service;
use Doctrine\ORM\EntityManagerInterface;
+use PhpList\Core\Domain\Subscription\Exception\CouldNotReadUploadedFileException;
use PhpList\Core\Domain\Subscription\Model\Dto\ImportSubscriberDto;
use PhpList\Core\Domain\Subscription\Model\Dto\SubscriberImportOptions;
use PhpList\Core\Domain\Subscription\Model\Subscriber;
@@ -13,8 +14,8 @@
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberAttributeManager;
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager;
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriptionManager;
-use RuntimeException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
+use Symfony\Contracts\Translation\TranslatorInterface;
use Throwable;
/**
@@ -30,6 +31,7 @@ class SubscriberCsvImporter
private CsvImporter $csvImporter;
private SubscriberAttributeDefinitionRepository $attrDefinitionRepository;
private EntityManagerInterface $entityManager;
+ private TranslatorInterface $translator;
public function __construct(
SubscriberManager $subscriberManager,
@@ -38,7 +40,8 @@ public function __construct(
SubscriberRepository $subscriberRepository,
CsvImporter $csvImporter,
SubscriberAttributeDefinitionRepository $attrDefinitionRepository,
- EntityManagerInterface $entityManager
+ EntityManagerInterface $entityManager,
+ TranslatorInterface $translator,
) {
$this->subscriberManager = $subscriberManager;
$this->attributeManager = $attributeManager;
@@ -47,6 +50,7 @@ public function __construct(
$this->csvImporter = $csvImporter;
$this->attrDefinitionRepository = $attrDefinitionRepository;
$this->entityManager = $entityManager;
+ $this->translator = $translator;
}
/**
@@ -55,7 +59,7 @@ public function __construct(
* @param UploadedFile $file The uploaded CSV file
* @param SubscriberImportOptions $options
* @return array Import statistics
- * @throws RuntimeException When the uploaded file cannot be read or for any other errors during import
+ * @throws CouldNotReadUploadedFileException When the uploaded file cannot be read during import
*/
public function importFromCsv(UploadedFile $file, SubscriberImportOptions $options): array
{
@@ -69,7 +73,9 @@ public function importFromCsv(UploadedFile $file, SubscriberImportOptions $optio
try {
$path = $file->getRealPath();
if ($path === false) {
- throw new RuntimeException('Could not read the uploaded file.');
+ throw new CouldNotReadUploadedFileException(
+ $this->translator->trans('Could not read the uploaded file.')
+ );
}
$result = $this->csvImporter->import($path);
@@ -81,7 +87,10 @@ public function importFromCsv(UploadedFile $file, SubscriberImportOptions $optio
$this->entityManager->flush();
}
} catch (Throwable $e) {
- $stats['errors'][] = 'Error processing ' . $dto->email . ': ' . $e->getMessage();
+ $stats['errors'][] = $this->translator->trans(
+ 'Error processing %email%: %error%',
+ ['%email%' => $dto->email, '%error%' => $e->getMessage()]
+ );
$stats['skipped']++;
}
}
@@ -91,7 +100,10 @@ public function importFromCsv(UploadedFile $file, SubscriberImportOptions $optio
$stats['skipped']++;
}
} catch (Throwable $e) {
- $stats['errors'][] = 'General import error: ' . $e->getMessage();
+ $stats['errors'][] = $this->translator->trans(
+ 'General import error: %error%',
+ ['%error%' => $e->getMessage()]
+ );
}
return $stats;
diff --git a/src/Domain/Subscription/Validator/AttributeTypeValidator.php b/src/Domain/Subscription/Validator/AttributeTypeValidator.php
index 3923cdfc..36bcd45d 100644
--- a/src/Domain/Subscription/Validator/AttributeTypeValidator.php
+++ b/src/Domain/Subscription/Validator/AttributeTypeValidator.php
@@ -8,9 +8,14 @@
use PhpList\Core\Domain\Common\Model\ValidationContext;
use PhpList\Core\Domain\Common\Validator\ValidatorInterface;
use Symfony\Component\Validator\Exception\ValidatorException;
+use Symfony\Contracts\Translation\TranslatorInterface;
class AttributeTypeValidator implements ValidatorInterface
{
+ public function __construct(private readonly TranslatorInterface $translator)
+ {
+ }
+
private const VALID_TYPES = [
'textline',
'checkbox',
@@ -25,15 +30,17 @@ class AttributeTypeValidator implements ValidatorInterface
public function validate(mixed $value, ValidationContext $context = null): void
{
if (!is_string($value)) {
- throw new InvalidArgumentException('Value must be a string.');
+ throw new InvalidArgumentException($this->translator->trans('Value must be a string.'));
}
$errors = [];
if (!in_array($value, self::VALID_TYPES, true)) {
- $errors[] = sprintf(
- 'Invalid attribute type: "%s". Valid types are: %s',
- $value,
- implode(', ', self::VALID_TYPES)
+ $errors[] = $this->translator->trans(
+ 'Invalid attribute type: "%type%". Valid types are: %valid_types%',
+ [
+ '%type%' => $value,
+ '%valid_types%' => implode(', ', self::VALID_TYPES),
+ ]
);
}
diff --git a/tests/Unit/Domain/Messaging/Service/ConsecutiveBounceHandlerTest.php b/tests/Unit/Domain/Messaging/Service/ConsecutiveBounceHandlerTest.php
index 1cb1b6d2..5fc375cd 100644
--- a/tests/Unit/Domain/Messaging/Service/ConsecutiveBounceHandlerTest.php
+++ b/tests/Unit/Domain/Messaging/Service/ConsecutiveBounceHandlerTest.php
@@ -14,6 +14,7 @@
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\Translation\Translator;
class ConsecutiveBounceHandlerTest extends TestCase
{
@@ -43,6 +44,7 @@ protected function setUp(): void
subscriberRepository: $this->subscriberRepository,
subscriberHistoryManager: $this->subscriberHistoryManager,
blacklistService: $this->blacklistService,
+ translator: new Translator('en'),
unsubscribeThreshold: $unsubscribeThreshold,
blacklistThreshold: $blacklistThreshold,
);
@@ -89,14 +91,14 @@ public function testUnsubscribeAtThresholdAddsHistoryAndMarksUnconfirmedOnce():
->method('addHistory')
->with(
$user,
- 'Auto Unconfirmed',
+ 'Auto unconfirmed',
$this->stringContains('2 consecutive bounces')
);
$this->blacklistService->expects($this->never())->method('blacklist');
$this->io->expects($this->once())->method('section')->with('Identifying consecutive bounces');
- $this->io->expects($this->once())->method('writeln')->with('total of 1 subscribers processed');
+ $this->io->expects($this->once())->method('writeln')->with('Total of 1 subscribers processed');
$this->handler->handle($this->io);
}
@@ -132,7 +134,7 @@ public function testBlacklistAtThresholdStopsProcessingAndAlsoUnsubscribesIfReac
->method('addHistory')
->with(
$user,
- 'Auto Unconfirmed',
+ 'Auto unconfirmed',
$this->stringContains('consecutive bounces')
);
@@ -164,7 +166,7 @@ public function testDuplicateBouncesAreIgnoredInCounting(): void
$this->subscriberRepository->expects($this->once())->method('markUnconfirmed')->with(55);
$this->subscriberHistoryManager->expects($this->once())->method('addHistory')->with(
$user,
- 'Auto Unconfirmed',
+ 'Auto unconfirmed',
$this->stringContains('2 consecutive bounces')
);
$this->blacklistService->expects($this->never())->method('blacklist');
diff --git a/tests/Unit/Domain/Messaging/Service/MaxProcessTimeLimiterTest.php b/tests/Unit/Domain/Messaging/Service/MaxProcessTimeLimiterTest.php
index 5944ca3e..57b2f07f 100644
--- a/tests/Unit/Domain/Messaging/Service/MaxProcessTimeLimiterTest.php
+++ b/tests/Unit/Domain/Messaging/Service/MaxProcessTimeLimiterTest.php
@@ -9,6 +9,7 @@
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Translation\Translator;
class MaxProcessTimeLimiterTest extends TestCase
{
@@ -21,7 +22,7 @@ protected function setUp(): void
public function testShouldNotStopWhenMaxSecondsIsZero(): void
{
- $limiter = new MaxProcessTimeLimiter(logger: $this->logger, maxSeconds: 0);
+ $limiter = new MaxProcessTimeLimiter(logger: $this->logger, translator: new Translator('en'), maxSeconds: 0);
$output = $this->createMock(OutputInterface::class);
$output->expects($this->never())->method('writeln');
@@ -34,7 +35,7 @@ public function testShouldNotStopWhenMaxSecondsIsZero(): void
public function testShouldStopAfterThresholdAndLogAndOutput(): void
{
- $limiter = new MaxProcessTimeLimiter(logger: $this->logger, maxSeconds: 1);
+ $limiter = new MaxProcessTimeLimiter(logger: $this->logger, translator: new Translator('en'), maxSeconds: 1);
$output = $this->createMock(OutputInterface::class);
$output->expects($this->once())
diff --git a/tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php b/tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php
index c2c0d0a5..85066691 100644
--- a/tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php
+++ b/tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php
@@ -16,6 +16,7 @@
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Translation\Translator;
class MessageProcessingPreparatorTest extends TestCase
{
@@ -35,10 +36,11 @@ protected function setUp(): void
$this->output = $this->createMock(OutputInterface::class);
$this->preparator = new MessageProcessingPreparator(
- $this->entityManager,
- $this->subscriberRepository,
- $this->messageRepository,
- $this->linkTrackService
+ entityManager: $this->entityManager,
+ subscriberRepository: $this->subscriberRepository,
+ messageRepository: $this->messageRepository,
+ linkTrackService: $this->linkTrackService,
+ translator: new Translator('en'),
);
}
@@ -189,7 +191,10 @@ public function testProcessMessageLinksWithLinksExtracted(): void
$savedLinks = [$linkTrack1, $linkTrack2];
$this->linkTrackService->method('isExtractAndSaveLinksApplicable')->willReturn(true);
- $this->linkTrackService->method('extractAndSaveLinks')->with($message, $userId)->willReturn($savedLinks);
+ $this->linkTrackService
+ ->method('extractAndSaveLinks')
+ ->with($message, $userId)
+ ->willReturn($savedLinks);
$message->method('getContent')->willReturn($content);
diff --git a/tests/Unit/Domain/Messaging/Service/SendRateLimiterTest.php b/tests/Unit/Domain/Messaging/Service/SendRateLimiterTest.php
index e9ba27c0..e29f6929 100644
--- a/tests/Unit/Domain/Messaging/Service/SendRateLimiterTest.php
+++ b/tests/Unit/Domain/Messaging/Service/SendRateLimiterTest.php
@@ -11,6 +11,7 @@
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Translation\Translator;
class SendRateLimiterTest extends TestCase
{
@@ -27,6 +28,7 @@ public function testInitializesLimitsFromConfigOnly(): void
$limiter = new SendRateLimiter(
ispRestrictionsProvider: $this->ispProvider,
userMessageRepository: $this->createMock(UserMessageRepository::class),
+ translator: new Translator('en'),
mailqueueBatchSize: 5,
mailqueueBatchPeriod: 10,
mailqueueThrottle: 2
@@ -44,6 +46,7 @@ public function testBatchLimitTriggersWaitMessageAndResetsCounters(): void
$limiter = new SendRateLimiter(
ispRestrictionsProvider: $this->ispProvider,
userMessageRepository: $this->createMock(UserMessageRepository::class),
+ translator: new Translator('en'),
mailqueueBatchSize: 10,
mailqueueBatchPeriod: 1,
mailqueueThrottle: 0
@@ -71,6 +74,7 @@ public function testThrottleSleepsPerMessagePathIsCallable(): void
$limiter = new SendRateLimiter(
ispRestrictionsProvider: $this->ispProvider,
userMessageRepository: $this->createMock(UserMessageRepository::class),
+ translator: new Translator('en'),
mailqueueBatchSize: 0,
mailqueueBatchPeriod: 0,
mailqueueThrottle: 1
diff --git a/tests/Unit/Domain/Messaging/Validator/TemplateImageValidatorTest.php b/tests/Unit/Domain/Messaging/Validator/TemplateImageValidatorTest.php
index 88af2c8c..40e1064a 100644
--- a/tests/Unit/Domain/Messaging/Validator/TemplateImageValidatorTest.php
+++ b/tests/Unit/Domain/Messaging/Validator/TemplateImageValidatorTest.php
@@ -12,6 +12,7 @@
use PhpList\Core\Domain\Messaging\Validator\TemplateImageValidator;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Translation\Translator;
use Symfony\Component\Validator\Exception\ValidatorException;
class TemplateImageValidatorTest extends TestCase
@@ -22,7 +23,7 @@ class TemplateImageValidatorTest extends TestCase
protected function setUp(): void
{
$this->httpClient = $this->createMock(ClientInterface::class);
- $this->validator = new TemplateImageValidator($this->httpClient);
+ $this->validator = new TemplateImageValidator($this->httpClient, new Translator('en'));
}
public function testThrowsExceptionIfValueIsNotArray(): void
diff --git a/tests/Unit/Domain/Messaging/Validator/TemplateLinkValidatorTest.php b/tests/Unit/Domain/Messaging/Validator/TemplateLinkValidatorTest.php
index d0ab6566..5767f193 100644
--- a/tests/Unit/Domain/Messaging/Validator/TemplateLinkValidatorTest.php
+++ b/tests/Unit/Domain/Messaging/Validator/TemplateLinkValidatorTest.php
@@ -7,6 +7,7 @@
use PhpList\Core\Domain\Common\Model\ValidationContext;
use PhpList\Core\Domain\Messaging\Validator\TemplateLinkValidator;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Translation\Translator;
use Symfony\Component\Validator\Exception\ValidatorException;
class TemplateLinkValidatorTest extends TestCase
@@ -15,7 +16,7 @@ class TemplateLinkValidatorTest extends TestCase
protected function setUp(): void
{
- $this->validator = new TemplateLinkValidator();
+ $this->validator = new TemplateLinkValidator(new Translator('en'));
}
public function testSkipsValidationIfNotString(): void
diff --git a/tests/Unit/Domain/Subscription/Service/Manager/AttributeDefinitionManagerTest.php b/tests/Unit/Domain/Subscription/Service/Manager/AttributeDefinitionManagerTest.php
index 279a6ff7..7e7bcfb7 100644
--- a/tests/Unit/Domain/Subscription/Service/Manager/AttributeDefinitionManagerTest.php
+++ b/tests/Unit/Domain/Subscription/Service/Manager/AttributeDefinitionManagerTest.php
@@ -11,6 +11,7 @@
use PhpList\Core\Domain\Subscription\Service\Manager\AttributeDefinitionManager;
use PhpList\Core\Domain\Subscription\Validator\AttributeTypeValidator;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Translation\Translator;
class AttributeDefinitionManagerTest extends TestCase
{
@@ -18,7 +19,11 @@ public function testCreateAttributeDefinition(): void
{
$repository = $this->createMock(SubscriberAttributeDefinitionRepository::class);
$validator = $this->createMock(AttributeTypeValidator::class);
- $manager = new AttributeDefinitionManager($repository, $validator);
+ $manager = new AttributeDefinitionManager(
+ definitionRepository: $repository,
+ attributeTypeValidator: $validator,
+ translator: new Translator('en')
+ );
$dto = new AttributeDefinitionDto(
name: 'Country',
@@ -51,7 +56,11 @@ public function testCreateThrowsWhenAttributeAlreadyExists(): void
{
$repository = $this->createMock(SubscriberAttributeDefinitionRepository::class);
$validator = $this->createMock(AttributeTypeValidator::class);
- $manager = new AttributeDefinitionManager($repository, $validator);
+ $manager = new AttributeDefinitionManager(
+ definitionRepository: $repository,
+ attributeTypeValidator: $validator,
+ translator: new Translator('en'),
+ );
$dto = new AttributeDefinitionDto(
name: 'Country',
@@ -78,7 +87,11 @@ public function testUpdateAttributeDefinition(): void
{
$repository = $this->createMock(SubscriberAttributeDefinitionRepository::class);
$validator = $this->createMock(AttributeTypeValidator::class);
- $manager = new AttributeDefinitionManager($repository, $validator);
+ $manager = new AttributeDefinitionManager(
+ definitionRepository: $repository,
+ attributeTypeValidator: $validator,
+ translator: new Translator('en'),
+ );
$attribute = new SubscriberAttributeDefinition();
$attribute->setName('Old');
@@ -113,7 +126,11 @@ public function testUpdateThrowsWhenAnotherAttributeWithSameNameExists(): void
{
$repository = $this->createMock(SubscriberAttributeDefinitionRepository::class);
$validator = $this->createMock(AttributeTypeValidator::class);
- $manager = new AttributeDefinitionManager($repository, $validator);
+ $manager = new AttributeDefinitionManager(
+ definitionRepository: $repository,
+ attributeTypeValidator: $validator,
+ translator: new Translator('en'),
+ );
$dto = new AttributeDefinitionDto(
name: 'Existing',
@@ -144,7 +161,11 @@ public function testDeleteAttributeDefinition(): void
{
$repository = $this->createMock(SubscriberAttributeDefinitionRepository::class);
$validator = $this->createMock(AttributeTypeValidator::class);
- $manager = new AttributeDefinitionManager($repository, $validator);
+ $manager = new AttributeDefinitionManager(
+ definitionRepository: $repository,
+ attributeTypeValidator: $validator,
+ translator: new Translator('en'),
+ );
$attribute = new SubscriberAttributeDefinition();
diff --git a/tests/Unit/Domain/Subscription/Service/Manager/SubscribePageManagerTest.php b/tests/Unit/Domain/Subscription/Service/Manager/SubscribePageManagerTest.php
index 422c78a7..6add5016 100644
--- a/tests/Unit/Domain/Subscription/Service/Manager/SubscribePageManagerTest.php
+++ b/tests/Unit/Domain/Subscription/Service/Manager/SubscribePageManagerTest.php
@@ -14,6 +14,7 @@
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Component\Translation\Translator;
class SubscribePageManagerTest extends TestCase
{
@@ -32,6 +33,7 @@ protected function setUp(): void
pageRepository: $this->pageRepository,
pageDataRepository: $this->pageDataRepository,
entityManager: $this->entityManager,
+ translator: new Translator('en'),
);
}
diff --git a/tests/Unit/Domain/Subscription/Service/Manager/SubscriberAttributeManagerTest.php b/tests/Unit/Domain/Subscription/Service/Manager/SubscriberAttributeManagerTest.php
index 355de90f..a827ab3f 100644
--- a/tests/Unit/Domain/Subscription/Service/Manager/SubscriberAttributeManagerTest.php
+++ b/tests/Unit/Domain/Subscription/Service/Manager/SubscriberAttributeManagerTest.php
@@ -12,6 +12,7 @@
use PhpList\Core\Domain\Subscription\Repository\SubscriberAttributeValueRepository;
use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberAttributeManager;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Translation\Translator;
class SubscriberAttributeManagerTest extends TestCase
{
@@ -34,7 +35,7 @@ public function testCreateNewSubscriberAttribute(): void
return $attr->getValue() === 'US';
}));
- $manager = new SubscriberAttributeManager($subscriberAttrRepo, $entityManager);
+ $manager = new SubscriberAttributeManager($subscriberAttrRepo, $entityManager, new Translator('en'));
$attribute = $manager->createOrUpdate($subscriber, $definition, 'US');
self::assertInstanceOf(SubscriberAttributeValue::class, $attribute);
@@ -60,7 +61,7 @@ public function testUpdateExistingSubscriberAttribute(): void
->method('persist')
->with($existing);
- $manager = new SubscriberAttributeManager($subscriberAttrRepo, $entityManager);
+ $manager = new SubscriberAttributeManager($subscriberAttrRepo, $entityManager, new Translator('en'));
$result = $manager->createOrUpdate($subscriber, $definition, 'Updated');
self::assertSame('Updated', $result->getValue());
@@ -76,7 +77,7 @@ public function testCreateFailsWhenValueAndDefaultAreNull(): void
$subscriberAttrRepo->method('findOneBySubscriberAndAttribute')->willReturn(null);
- $manager = new SubscriberAttributeManager($subscriberAttrRepo, $entityManager);
+ $manager = new SubscriberAttributeManager($subscriberAttrRepo, $entityManager, new Translator('en'));
$this->expectException(SubscriberAttributeCreationException::class);
$this->expectExceptionMessage('Value is required');
@@ -95,7 +96,7 @@ public function testGetSubscriberAttribute(): void
->with(5, 10)
->willReturn($expected);
- $manager = new SubscriberAttributeManager($subscriberAttrRepo, $entityManager);
+ $manager = new SubscriberAttributeManager($subscriberAttrRepo, $entityManager, new Translator('en'));
$result = $manager->getSubscriberAttribute(5, 10);
self::assertSame($expected, $result);
@@ -111,7 +112,7 @@ public function testDeleteSubscriberAttribute(): void
->method('remove')
->with($attribute);
- $manager = new SubscriberAttributeManager($subscriberAttrRepo, $entityManager);
+ $manager = new SubscriberAttributeManager($subscriberAttrRepo, $entityManager, new Translator('en'));
$manager->delete($attribute);
self::assertTrue(true);
diff --git a/tests/Unit/Domain/Subscription/Service/Manager/SubscriberManagerTest.php b/tests/Unit/Domain/Subscription/Service/Manager/SubscriberManagerTest.php
index b7a99366..f96f32e2 100644
--- a/tests/Unit/Domain/Subscription/Service/Manager/SubscriberManagerTest.php
+++ b/tests/Unit/Domain/Subscription/Service/Manager/SubscriberManagerTest.php
@@ -15,6 +15,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
+use Symfony\Component\Translation\Translator;
class SubscriberManagerTest extends TestCase
{
@@ -35,6 +36,7 @@ protected function setUp(): void
entityManager: $this->entityManager,
messageBus: $this->messageBus,
subscriberDeletionService: $subscriberDeletionService,
+ translator: new Translator('en'),
);
}
diff --git a/tests/Unit/Domain/Subscription/Service/SubscriberCsvImporterTest.php b/tests/Unit/Domain/Subscription/Service/SubscriberCsvImporterTest.php
index 0bacd756..f825f704 100644
--- a/tests/Unit/Domain/Subscription/Service/SubscriberCsvImporterTest.php
+++ b/tests/Unit/Domain/Subscription/Service/SubscriberCsvImporterTest.php
@@ -19,36 +19,36 @@
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\File\UploadedFile;
+use Symfony\Component\Translation\Translator;
class SubscriberCsvImporterTest extends TestCase
{
private SubscriberManager&MockObject $subscriberManagerMock;
private SubscriberAttributeManager&MockObject $attributeManagerMock;
- private SubscriptionManager&MockObject $subscriptionManagerMock;
private SubscriberRepository&MockObject $subscriberRepositoryMock;
private CsvImporter&MockObject $csvImporterMock;
private SubscriberAttributeDefinitionRepository&MockObject $attributeDefinitionRepositoryMock;
- private EntityManagerInterface $entityManager;
private SubscriberCsvImporter $subject;
protected function setUp(): void
{
$this->subscriberManagerMock = $this->createMock(SubscriberManager::class);
$this->attributeManagerMock = $this->createMock(SubscriberAttributeManager::class);
- $this->subscriptionManagerMock = $this->createMock(SubscriptionManager::class);
+ $subscriptionManagerMock = $this->createMock(SubscriptionManager::class);
$this->subscriberRepositoryMock = $this->createMock(SubscriberRepository::class);
$this->csvImporterMock = $this->createMock(CsvImporter::class);
$this->attributeDefinitionRepositoryMock = $this->createMock(SubscriberAttributeDefinitionRepository::class);
- $this->entityManager = $this->createMock(EntityManagerInterface::class);
+ $entityManager = $this->createMock(EntityManagerInterface::class);
$this->subject = new SubscriberCsvImporter(
subscriberManager: $this->subscriberManagerMock,
attributeManager: $this->attributeManagerMock,
- subscriptionManager: $this->subscriptionManagerMock,
+ subscriptionManager: $subscriptionManagerMock,
subscriberRepository: $this->subscriberRepositoryMock,
csvImporter: $this->csvImporterMock,
attrDefinitionRepository: $this->attributeDefinitionRepositoryMock,
- entityManager: $this->entityManager,
+ entityManager: $entityManager,
+ translator: new Translator('en'),
);
}
diff --git a/tests/Unit/Domain/Subscription/Validator/AttributeTypeValidatorTest.php b/tests/Unit/Domain/Subscription/Validator/AttributeTypeValidatorTest.php
index c0ab3a5a..cf691324 100644
--- a/tests/Unit/Domain/Subscription/Validator/AttributeTypeValidatorTest.php
+++ b/tests/Unit/Domain/Subscription/Validator/AttributeTypeValidatorTest.php
@@ -7,6 +7,7 @@
use InvalidArgumentException;
use PhpList\Core\Domain\Subscription\Validator\AttributeTypeValidator;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Translation\Translator;
use Symfony\Component\Validator\Exception\ValidatorException;
class AttributeTypeValidatorTest extends TestCase
@@ -15,7 +16,7 @@ class AttributeTypeValidatorTest extends TestCase
protected function setUp(): void
{
- $this->validator = new AttributeTypeValidator();
+ $this->validator = new AttributeTypeValidator(new Translator('en'));
}
public function testValidatesValidType(): void