diff --git a/resources/translations/messages.en.xlf b/resources/translations/messages.en.xlf index 7e176e3e..6f128be1 100644 --- a/resources/translations/messages.en.xlf +++ b/resources/translations/messages.en.xlf @@ -1,44 +1,450 @@ - - - - - - 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. + + + + 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.

+ ]]> +
+
+ + + Please confirm your subscription + Please confirm your subscription + + + + Thank you for subscribing! + + Please confirm your subscription by clicking the link below: + + %confirmation_link% + + If you did not request this subscription, please ignore this email. + + Thank you for subscribing! + + Please confirm your subscription by clicking the link below: + + %confirmation_link% + + 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. + 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. + + + + 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% + + + + 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 + + + + 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. + + + + Invalid email, marking unconfirmed: %email% + Invalid email, marking unconfirmed: %email% + + + Failed to send to: %email% + 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 + + + + 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. + + + + 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/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/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 @@ +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/Mail/NativeImapMailReader.php b/src/Domain/Common/Mail/NativeImapMailReader.php index 472fea54..ba90151d 100644 --- a/src/Domain/Common/Mail/NativeImapMailReader.php +++ b/src/Domain/Common/Mail/NativeImapMailReader.php @@ -6,7 +6,7 @@ use DateTimeImmutable; use IMAP\Connection; -use RuntimeException; +use PhpList\Core\Domain\Common\Exception\MailboxConnectionException; class NativeImapMailReader { @@ -24,7 +24,7 @@ public function open(string $mailbox, int $options = 0): Connection $link = imap_open($mailbox, $this->username, $this->password, $options); if ($link === false) { - throw new RuntimeException('Cannot open mailbox: '.(imap_last_error() ?: 'unknown error')); + throw new MailboxConnectionException($mailbox); } return $link; diff --git a/src/Domain/Common/Repository/CursorPaginationTrait.php b/src/Domain/Common/Repository/CursorPaginationTrait.php index 8be64ee2..3cf67a72 100644 --- a/src/Domain/Common/Repository/CursorPaginationTrait.php +++ b/src/Domain/Common/Repository/CursorPaginationTrait.php @@ -4,9 +4,9 @@ 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; trait CursorPaginationTrait { @@ -30,14 +30,14 @@ 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) { 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); } diff --git a/src/Domain/Identity/Command/CleanUpOldSessionTokens.php b/src/Domain/Identity/Command/CleanUpOldSessionTokens.php index 348ea025..364d5ea9 100644 --- a/src/Domain/Identity/Command/CleanUpOldSessionTokens.php +++ b/src/Domain/Identity/Command/CleanUpOldSessionTokens.php @@ -35,6 +35,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->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/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/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 @@ +emailService = $emailService; + $this->translator = $translator; $this->passwordResetUrl = $passwordResetUrl; } @@ -28,19 +31,30 @@ 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( + "Hello,\n\n" . + "A password reset has been requested for your account.\n" . + "Please use the following token to reset your password:\n\n" . + "%token%\n\n" . + "If you did not request this password reset, please ignore this email.\n\n" . + 'Thank you.', + ['%token%' => $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.

', + [ + '%confirmation_link%' => $confirmationLink, + ] + ); + $email = (new Email()) ->to($message->getEmail()) diff --git a/src/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandler.php b/src/Domain/Messaging/MessageHandler/SubscriberConfirmationMessageHandler.php index 8c487849..69ec42cb 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,29 @@ 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( + "Thank you for subscribing!\n\n" . + "Please confirm your subscription by clicking the link below:\n\n" . + "%confirmation_link%\n\n" . + 'If you did not request this subscription, please ignore this email.', + [ + '%confirmation_link%' => $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.

', + [ + '%confirmation_link%' => $confirmationLink, + ] + ); } $email = (new Email()) diff --git a/src/Domain/Messaging/Model/BounceStatus.php b/src/Domain/Messaging/Model/BounceStatus.php new file mode 100644 index 00000000..be77473f --- /dev/null +++ b/src/Domain/Messaging/Model/BounceStatus.php @@ -0,0 +1,19 @@ +value, $userId); + } +} 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/ConsecutiveBounceHandler.php b/src/Domain/Messaging/Service/ConsecutiveBounceHandler.php index 0805c156..91c4c041 100644 --- a/src/Domain/Messaging/Service/ConsecutiveBounceHandler.php +++ b/src/Domain/Messaging/Service/ConsecutiveBounceHandler.php @@ -13,6 +13,7 @@ use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager; use PhpList\Core\Domain\Subscription\Service\SubscriberBlacklistService; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Contracts\Translation\TranslatorInterface; class ConsecutiveBounceHandler { @@ -20,6 +21,7 @@ class ConsecutiveBounceHandler private SubscriberRepository $subscriberRepository; private SubscriberHistoryManager $subscriberHistoryManager; private SubscriberBlacklistService $blacklistService; + private TranslatorInterface $translator; private int $unsubscribeThreshold; private int $blacklistThreshold; @@ -28,6 +30,7 @@ public function __construct( SubscriberRepository $subscriberRepository, SubscriberHistoryManager $subscriberHistoryManager, SubscriberBlacklistService $blacklistService, + TranslatorInterface $translator, int $unsubscribeThreshold, int $blacklistThreshold, ) { @@ -35,19 +38,21 @@ public function __construct( $this->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/Handler/BlacklistEmailAndDeleteBounceHandler.php b/src/Domain/Messaging/Service/Handler/BlacklistEmailAndDeleteBounceHandler.php index d32cf68b..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/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/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/Processor/AdvancedBounceRulesProcessor.php b/src/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessor.php index 568bf874..0e1c3fe0 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/src/Domain/Messaging/Service/Processor/BounceDataProcessor.php b/src/Domain/Messaging/Service/Processor/BounceDataProcessor.php index 6f502a8c..7a33a7e9 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,35 @@ 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::UnidentifiedBounce->value, + comment: 'not processed' + ); return false; } @@ -76,10 +86,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 +106,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 +122,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 +157,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 +169,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); 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/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/src/Domain/Messaging/Service/Processor/UnidentifiedBounceReprocessor.php b/src/Domain/Messaging/Service/Processor/UnidentifiedBounceReprocessor.php index 503fc459..2646ede6 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,13 @@ 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/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/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/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/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); 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/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'", 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', 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 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 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); 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/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') ); } 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/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/Processor/AdvancedBounceRulesProcessorTest.php b/tests/Unit/Domain/Messaging/Service/Processor/AdvancedBounceRulesProcessorTest.php index 209fb583..a4590052 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 { @@ -36,15 +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: $translator, ); $processor->process($this->io, 100); @@ -159,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( @@ -170,6 +180,7 @@ public function testProcessingWithMatchesAndNonMatches(): void ruleManager: $this->ruleManager, actionResolver: $this->actionResolver, subscriberManager: $this->subscriberManager, + translator: $translator, ); $processor->process($this->io, 2); 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'), ); } diff --git a/tests/Unit/Domain/Messaging/Service/Processor/MboxBounceProcessorTest.php b/tests/Unit/Domain/Messaging/Service/Processor/MboxBounceProcessorTest.php index 210e000c..9bf1c92f 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,14 @@ 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], 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); } 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