Skip to content

Commit 2f911ca

Browse files
committed
RateLimitedCampaignMailer
1 parent 9e1ad9c commit 2f911ca

File tree

7 files changed

+91
-56
lines changed

7 files changed

+91
-56
lines changed

config/services/providers.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@ services:
66
PhpList\Core\Core\ConfigProvider:
77
arguments:
88
$config: '%app.config%'
9+
10+
PhpList\Core\Domain\Common\IspRestrictionsProvider:
11+
autowire: true
12+
autoconfigure: true
13+
arguments:
14+
$confPath: '%app.phplist_isp_conf_path%'

config/services/services.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,9 @@ services:
5252
autowire: true
5353
autoconfigure: true
5454

55-
PhpList\Core\Domain\Common\IspRestrictionsProvider:
55+
PhpList\Core\Domain\Messaging\Service\RateLimitedCampaignMailer:
5656
autowire: true
5757
autoconfigure: true
58-
arguments:
59-
$confPath: '%app.phplist_isp_conf_path%'
6058

6159
PhpList\Core\Domain\Messaging\Service\ConsecutiveBounceHandler:
6260
autowire: true

src/Domain/Common/IspRestrictionsProvider.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ class IspRestrictionsProvider
1212
public function __construct(
1313
private readonly string $confPath,
1414
private readonly LoggerInterface $logger,
15-
) {}
15+
) {
16+
}
1617

1718
public function load(): IspRestrictions
1819
{

src/Domain/Common/Model/IspRestrictions.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ public function __construct(
1111
public readonly ?int $minBatchPeriod,
1212
public readonly ?string $lockFile,
1313
public readonly array $raw = [],
14-
)
15-
{
14+
) {
1615
}
1716

1817
public function isEmpty(): bool

src/Domain/Messaging/Service/Processor/CampaignProcessor.php

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,81 +8,68 @@
88
use PhpList\Core\Domain\Messaging\Model\Message;
99
use PhpList\Core\Domain\Messaging\Model\UserMessage;
1010
use PhpList\Core\Domain\Messaging\Model\Message\UserMessageStatus;
11+
use PhpList\Core\Domain\Messaging\Model\Message\MessageStatus;
1112
use PhpList\Core\Domain\Messaging\Repository\UserMessageRepository;
13+
use PhpList\Core\Domain\Messaging\Service\RateLimitedCampaignMailer;
1214
use PhpList\Core\Domain\Messaging\Service\MessageProcessingPreparator;
13-
use PhpList\Core\Domain\Messaging\Service\SendRateLimiter;
1415
use PhpList\Core\Domain\Subscription\Service\Provider\SubscriberProvider;
1516
use Psr\Log\LoggerInterface;
1617
use Symfony\Component\Console\Output\OutputInterface;
17-
use Symfony\Component\Mailer\MailerInterface;
18-
use Symfony\Component\Mime\Email;
1918
use Throwable;
2019

2120
class CampaignProcessor
2221
{
23-
private MailerInterface $mailer;
22+
private RateLimitedCampaignMailer $mailer;
2423
private EntityManagerInterface $entityManager;
2524
private SubscriberProvider $subscriberProvider;
2625
private MessageProcessingPreparator $messagePreparator;
2726
private LoggerInterface $logger;
28-
private SendRateLimiter $rateLimiter;
2927
private UserMessageRepository $userMessageRepository;
3028

3129
public function __construct(
32-
MailerInterface $mailer,
30+
RateLimitedCampaignMailer $mailer,
3331
EntityManagerInterface $entityManager,
3432
SubscriberProvider $subscriberProvider,
3533
MessageProcessingPreparator $messagePreparator,
3634
LoggerInterface $logger,
37-
SendRateLimiter $rateLimiter,
3835
UserMessageRepository $userMessageRepository
3936
) {
4037
$this->mailer = $mailer;
4138
$this->entityManager = $entityManager;
4239
$this->subscriberProvider = $subscriberProvider;
4340
$this->messagePreparator = $messagePreparator;
4441
$this->logger = $logger;
45-
$this->rateLimiter = $rateLimiter;
4642
$this->userMessageRepository = $userMessageRepository;
4743
}
4844

4945
public function process(Message $campaign, ?OutputInterface $output = null): void
5046
{
51-
$this->updateMessageStatus($campaign, Message\MessageStatus::Prepared);
47+
$this->updateMessageStatus($campaign, MessageStatus::Prepared);
5248
$subscribers = $this->subscriberProvider->getSubscribersForMessage($campaign);
5349

54-
$this->updateMessageStatus($campaign, Message\MessageStatus::InProcess);
50+
$this->updateMessageStatus($campaign, MessageStatus::InProcess);
5551

5652
foreach ($subscribers as $subscriber) {
5753
$existing = $this->userMessageRepository->findOneByUserAndMessage($subscriber, $campaign);
58-
if ($existing && $existing->getStatus() !== UserMessageStatus::Todo->value) {
54+
if ($existing && $existing->getStatus() !== UserMessageStatus::Todo) {
5955
continue;
6056
}
6157

6258
$userMessage = $existing ?? new UserMessage($subscriber, $campaign);
6359
$userMessage->setStatus(UserMessageStatus::Active);
6460
$this->userMessageRepository->save($userMessage);
6561

66-
$this->rateLimiter->awaitTurn($output);
67-
6862
if (!filter_var($subscriber->getEmail(), FILTER_VALIDATE_EMAIL)) {
6963
$this->updateUserMessageStatus($userMessage, UserMessageStatus::InvalidEmailAddress);
7064
continue;
7165
}
7266

73-
$this->messagePreparator->processMessageLinks($campaign, $subscriber->getId());
74-
75-
$email = (new Email())
76-
77-
->to($subscriber->getEmail())
78-
->subject($campaign->getContent()->getSubject())
79-
->text($campaign->getContent()->getTextMessage())
80-
->html($campaign->getContent()->getText());
67+
$processed = $this->messagePreparator->processMessageLinks($campaign, $subscriber->getId());
8168

8269
try {
70+
$email = $this->mailer->composeEmail($processed, $subscriber);
8371
$this->mailer->send($email);
8472
$this->updateUserMessageStatus($userMessage, UserMessageStatus::Sent);
85-
$this->rateLimiter->afterSend();
8673
} catch (Throwable $e) {
8774
$this->updateUserMessageStatus($userMessage, UserMessageStatus::NotSent);
8875
$this->logger->error($e->getMessage(), [
@@ -93,17 +80,18 @@ public function process(Message $campaign, ?OutputInterface $output = null): voi
9380
}
9481
}
9582

96-
$this->updateMessageStatus($campaign, Message\MessageStatus::Sent);
83+
$this->updateMessageStatus($campaign, MessageStatus::Sent);
9784
}
9885

99-
private function updateMessageStatus(Message $message, Message\MessageStatus $status): void
86+
private function updateMessageStatus(Message $message, MessageStatus $status): void
10087
{
10188
$message->getMetadata()->setStatus($status);
10289
$this->entityManager->flush();
10390
}
10491

105-
private function updateUserMessageStatus(UserMessage $userMessage, Message\UserMessageStatus $status): void
92+
private function updateUserMessageStatus(UserMessage $userMessage, UserMessageStatus $status): void
10693
{
10794
$userMessage->setStatus($status);
10895
$this->entityManager->flush();
109-
}}
96+
}
97+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Domain\Messaging\Service;
6+
7+
use PhpList\Core\Domain\Messaging\Model\Message;
8+
use PhpList\Core\Domain\Subscription\Model\Subscriber;
9+
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
10+
use Symfony\Component\Mailer\MailerInterface;
11+
use Symfony\Component\Mime\Email;
12+
13+
class RateLimitedCampaignMailer
14+
{
15+
private MailerInterface $mailer;
16+
private SendRateLimiter $limiter;
17+
public function __construct(MailerInterface $mailer, SendRateLimiter $limiter)
18+
{
19+
$this->mailer = $mailer;
20+
$this->limiter = $limiter;
21+
}
22+
23+
public function composeEmail(Message $processed, Subscriber $subscriber): Email
24+
{
25+
return (new Email())
26+
27+
->to($subscriber->getEmail())
28+
->subject($processed->getContent()->getSubject())
29+
->text($processed->getContent()->getTextMessage())
30+
->html($processed->getContent()->getText());
31+
}
32+
33+
/**
34+
* @throws TransportExceptionInterface
35+
*/
36+
public function send(Email $email): void
37+
{
38+
$this->limiter->awaitTurn();
39+
$this->mailer->send($email);
40+
$this->limiter->afterSend();
41+
}
42+
}

tests/Unit/Domain/Messaging/Service/Processor/CampaignProcessorTest.php

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,48 +12,42 @@
1212
use PhpList\Core\Domain\Messaging\Repository\UserMessageRepository;
1313
use PhpList\Core\Domain\Messaging\Service\MessageProcessingPreparator;
1414
use PhpList\Core\Domain\Messaging\Service\Processor\CampaignProcessor;
15-
use PhpList\Core\Domain\Messaging\Service\SendRateLimiter;
15+
use PhpList\Core\Domain\Messaging\Service\RateLimitedCampaignMailer;
1616
use PhpList\Core\Domain\Subscription\Model\Subscriber;
1717
use PhpList\Core\Domain\Subscription\Service\Provider\SubscriberProvider;
1818
use PHPUnit\Framework\MockObject\MockObject;
1919
use PHPUnit\Framework\TestCase;
2020
use Psr\Log\LoggerInterface;
2121
use Symfony\Component\Console\Output\OutputInterface;
22-
use Symfony\Component\Mailer\MailerInterface;
2322
use Symfony\Component\Mime\Email;
2423

2524
class CampaignProcessorTest extends TestCase
2625
{
27-
private MailerInterface&MockObject $mailer;
28-
private EntityManagerInterface&MockObject $entityManager;
29-
private SubscriberProvider&MockObject $subscriberProvider;
30-
private MessageProcessingPreparator&MockObject $messagePreparator;
31-
private LoggerInterface&MockObject $logger;
32-
private OutputInterface&MockObject $output;
26+
private RateLimitedCampaignMailer|MockObject $mailer;
27+
private EntityManagerInterface|MockObject $entityManager;
28+
private SubscriberProvider|MockObject $subscriberProvider;
29+
private MessageProcessingPreparator|MockObject $messagePreparator;
30+
private LoggerInterface|MockObject $logger;
31+
private OutputInterface|MockObject $output;
3332
private CampaignProcessor $campaignProcessor;
34-
private SendRateLimiter&MockObject $rateLimiter;
35-
private UserMessageRepository&MockObject $userMessageRepository;
33+
private UserMessageRepository|MockObject $userMessageRepository;
3634

3735
protected function setUp(): void
3836
{
39-
$this->mailer = $this->createMock(MailerInterface::class);
37+
$this->mailer = $this->createMock(RateLimitedCampaignMailer::class);
4038
$this->entityManager = $this->createMock(EntityManagerInterface::class);
4139
$this->subscriberProvider = $this->createMock(SubscriberProvider::class);
4240
$this->messagePreparator = $this->createMock(MessageProcessingPreparator::class);
4341
$this->logger = $this->createMock(LoggerInterface::class);
4442
$this->output = $this->createMock(OutputInterface::class);
45-
$this->rateLimiter = $this->createMock(SendRateLimiter::class);
4643
$this->userMessageRepository = $this->createMock(UserMessageRepository::class);
47-
$this->rateLimiter->method('awaitTurn');
48-
$this->rateLimiter->method('afterSend');
4944

5045
$this->campaignProcessor = new CampaignProcessor(
5146
mailer: $this->mailer,
5247
entityManager: $this->entityManager,
5348
subscriberProvider: $this->subscriberProvider,
5449
messagePreparator: $this->messagePreparator,
5550
logger: $this->logger,
56-
rateLimiter: $this->rateLimiter,
5751
userMessageRepository: $this->userMessageRepository,
5852
);
5953
}
@@ -131,16 +125,23 @@ public function testProcessWithValidSubscriberEmail(): void
131125
->with($campaign, 1)
132126
->willReturn($campaign);
133127

128+
$this->mailer->expects($this->once())
129+
->method('composeEmail')
130+
->with($campaign, $subscriber)
131+
->willReturnCallback(function ($processed, $sub) use ($campaign, $subscriber) {
132+
$this->assertSame($campaign, $processed);
133+
$this->assertSame($subscriber, $sub);
134+
return (new Email())
135+
136+
137+
->subject('Test Subject')
138+
->text('Test text message')
139+
->html('<p>Test HTML message</p>');
140+
});
141+
134142
$this->mailer->expects($this->once())
135143
->method('send')
136-
->with($this->callback(function (Email $email) {
137-
$this->assertEquals('[email protected]', $email->getTo()[0]->getAddress());
138-
$this->assertEquals('[email protected]', $email->getFrom()[0]->getAddress());
139-
$this->assertEquals('Test Subject', $email->getSubject());
140-
$this->assertEquals('Test text message', $email->getTextBody());
141-
$this->assertEquals('<p>Test HTML message</p>', $email->getHtmlBody());
142-
return true;
143-
}));
144+
->with($this->isInstanceOf(Email::class));
144145

145146
$metadata->expects($this->atLeastOnce())
146147
->method('setStatus');
@@ -281,7 +282,7 @@ public function testProcessWithNullOutput(): void
281282
/**
282283
* Creates a mock for the Message class with content
283284
*/
284-
private function createCampaignMock(): Message&MockObject
285+
private function createCampaignMock(): Message|MockObject
285286
{
286287
$campaign = $this->createMock(Message::class);
287288
$content = $this->createMock(MessageContent::class);

0 commit comments

Comments
 (0)