Skip to content

Commit 525741a

Browse files
committed
Use precached message data
1 parent 4492e02 commit 525741a

File tree

9 files changed

+239
-75
lines changed

9 files changed

+239
-75
lines changed

src/Domain/Analytics/Service/LinkTrackService.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PhpList\Core\Domain\Analytics\Model\LinkTrack;
1010
use PhpList\Core\Domain\Analytics\Repository\LinkTrackRepository;
1111
use PhpList\Core\Domain\Messaging\Model\Message;
12+
use PhpList\Core\Domain\Messaging\Model\Message\MessageContent;
1213

1314
class LinkTrackService
1415
{
@@ -38,15 +39,12 @@ public function isExtractAndSaveLinksApplicable(): bool
3839
* @return LinkTrack[] The saved LinkTrack entities
3940
* @throws MissingMessageIdException
4041
*/
41-
public function extractAndSaveLinks(Message $message, int $userId): array
42+
public function extractAndSaveLinks(MessageContent $content, int $userId, ?int $messageId = null): array
4243
{
4344
if (!$this->isExtractAndSaveLinksApplicable()) {
4445
return [];
4546
}
4647

47-
$content = $message->getContent();
48-
$messageId = $message->getId();
49-
5048
if ($messageId === null) {
5149
throw new MissingMessageIdException();
5250
}

src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use PhpList\Core\Domain\Messaging\Service\Handler\RequeueHandler;
1818
use PhpList\Core\Domain\Messaging\Service\Manager\MessageDataManager;
1919
use PhpList\Core\Domain\Messaging\Service\MaxProcessTimeLimiter;
20+
use PhpList\Core\Domain\Messaging\Service\MessagePrecacheService;
2021
use PhpList\Core\Domain\Messaging\Service\MessageProcessingPreparator;
2122
use PhpList\Core\Domain\Messaging\Service\RateLimitedCampaignMailer;
2223
use PhpList\Core\Domain\Configuration\Service\Manager\EventLogManager;
@@ -54,6 +55,7 @@ public function __construct(
5455
private readonly MessageRepository $messageRepository,
5556
private readonly EventLogManager $eventLogManager,
5657
private readonly MessageDataManager $messageDataManager,
58+
private readonly MessagePrecacheService $precacheService,
5759
?int $maxMailSize = null,
5860
) {
5961
$this->maxMailSize = $maxMailSize ?? 0;
@@ -71,6 +73,8 @@ public function __invoke(CampaignProcessorMessage|SyncCampaignProcessorMessage $
7173
return;
7274
}
7375

76+
$messageContent = $this->precacheService->getOrCacheBaseMessageContent($campaign);
77+
7478
$this->updateMessageStatus($campaign, MessageStatus::Prepared);
7579
$subscribers = $this->subscriberProvider->getSubscribersForMessage($campaign);
7680

@@ -100,7 +104,7 @@ public function __invoke(CampaignProcessorMessage|SyncCampaignProcessorMessage $
100104
continue;
101105
}
102106

103-
$this->handleEmailSending($campaign, $subscriber, $userMessage);
107+
$this->handleEmailSending($campaign, $subscriber, $userMessage, $messageContent);
104108
}
105109

106110
if ($stoppedEarly && $this->requeueHandler->handle($campaign)) {
@@ -131,7 +135,7 @@ private function updateUserMessageStatus(UserMessage $userMessage, UserMessageSt
131135
$this->entityManager->flush();
132136
}
133137

134-
private function handleInvalidEmail(UserMessage $userMessage, Subscriber $subscriber, mixed $campaign): void
138+
private function handleInvalidEmail(UserMessage $userMessage, Subscriber $subscriber, Message $campaign): void
135139
{
136140
$this->updateUserMessageStatus($userMessage, UserMessageStatus::InvalidEmailAddress);
137141
$this->unconfirmSubscriber($subscriber);
@@ -148,13 +152,16 @@ private function handleInvalidEmail(UserMessage $userMessage, Subscriber $subscr
148152
);
149153
}
150154

151-
private function handleEmailSending(mixed $campaign, Subscriber $subscriber, UserMessage $userMessage): void
152-
{
153-
$processed = $this->messagePreparator->processMessageLinks($campaign, $subscriber);
154-
// todo: precacheMessage
155+
private function handleEmailSending(
156+
Message $campaign,
157+
Subscriber $subscriber,
158+
UserMessage $userMessage,
159+
Message\MessageContent $precachedContent,
160+
): void {
161+
$processed = $this->messagePreparator->processMessageLinks($campaign->getId(), $precachedContent, $subscriber);
155162

156163
try {
157-
$email = $this->mailer->composeEmail($processed, $subscriber);
164+
$email = $this->mailer->composeEmail($campaign, $subscriber, $processed);
158165
$this->mailer->send($email);
159166
$this->checkMessageSizeOrSuspendCampaign($campaign, $email, $subscriber->hasHtmlEmail());
160167
$this->updateUserMessageStatus($userMessage, UserMessageStatus::Sent);
@@ -225,7 +232,7 @@ private function calculateEmailSize(Email $email): int
225232
foreach ($email->toIterable() as $line) {
226233
$size += strlen($line);
227234
}
228-
// todo: setMessageData($messageid, $sizename, $mail->mailsize);
235+
229236
return $size;
230237
}
231238
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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 Psr\SimpleCache\CacheInterface;
9+
10+
class MessagePrecacheService
11+
{
12+
public function __construct(private readonly CacheInterface $cache)
13+
{
14+
}
15+
16+
/**
17+
* Retrieve the base (unpersonalized) message content for a campaign from cache,
18+
* or cache it on first access. Legacy-like behavior: handle [URL:] token fetch
19+
* and basic placeholder replacements.
20+
*/
21+
public function getOrCacheBaseMessageContent(Message $campaign): Message\MessageContent
22+
{
23+
$cacheKey = sprintf('messaging.message.base.%d', $campaign->getId());
24+
25+
if ($this->cache->has($cacheKey) && $this->getFromCache($cacheKey)) {
26+
return $this->getFromCache($cacheKey);
27+
}
28+
29+
$content = $campaign->getContent();
30+
$subject = $content->getSubject();
31+
$html = $content->getText();
32+
$text = $content->getTextMessage();
33+
$footer = $content->getFooter();
34+
35+
// If content contains a [URL:...] token, try to fetch and replace with remote content
36+
if (is_string($html) && preg_match('/\[URL:([^\s\]]+)\]/i', $html, $match)) {
37+
$remoteUrl = $match[1];
38+
$fetched = $this->fetchRemoteContent($remoteUrl);
39+
if ($fetched !== null) {
40+
$html = str_replace($match[0], $fetched, $html);
41+
}
42+
}
43+
44+
// Replace basic placeholders [subject],[id],[fromname],[fromemail]
45+
$replacements = $this->buildBasicReplacements($campaign, $subject);
46+
$html = $this->applyReplacements($html, $replacements);
47+
$text = $this->applyReplacements($text, $replacements);
48+
$footer = $this->applyReplacements($footer, $replacements);
49+
50+
$snapshot = [
51+
'subject' => $subject,
52+
'text' => $html,
53+
'textMessage' => $text,
54+
'footer' => $footer,
55+
];
56+
57+
$this->cache->set($cacheKey, $snapshot);
58+
59+
return new Message\MessageContent($subject, $html, $text, $footer);
60+
}
61+
62+
private function fetchRemoteContent(string $url): ?string
63+
{
64+
$ctx = stream_context_create([
65+
'http' => ['timeout' => 5],
66+
'https' => ['timeout' => 5],
67+
]);
68+
69+
// Ignore warnings from file_get_contents only inside this block
70+
set_error_handler(static function () {
71+
return true;
72+
});
73+
74+
try {
75+
$data = file_get_contents($url, false, $ctx);
76+
} finally {
77+
restore_error_handler();
78+
}
79+
80+
if ($data === false) {
81+
return null;
82+
}
83+
84+
return $data;
85+
}
86+
87+
private function buildBasicReplacements(Message $campaign, string $subject): array
88+
{
89+
[$fromName, $fromEmail] = $this->parseFromField($campaign->getOptions()->getFromField());
90+
return [
91+
'[subject]' => $subject,
92+
'[id]' => (string)($campaign->getId() ?? ''),
93+
'[fromname]' => $fromName,
94+
'[fromemail]' => $fromEmail,
95+
];
96+
}
97+
98+
private function parseFromField(string $fromField): array
99+
{
100+
$email = '';
101+
if (preg_match('/([^\s<>"]+@[^\s<>"]+)/', $fromField, $match)) {
102+
$email = str_replace(['<', '>'], '', $match[0]);
103+
}
104+
$name = trim(str_replace([$email, '"'], ['', ''], $fromField));
105+
$name = trim(str_replace(['<', '>'], '', $name));
106+
return [$name, $email];
107+
}
108+
109+
private function applyReplacements(?string $input, array $replacements): ?string
110+
{
111+
if ($input === null) {
112+
return null;
113+
}
114+
return str_ireplace(array_keys($replacements), array_values($replacements), $input);
115+
}
116+
117+
private function getFromCache(string $cacheKey): ?Message\MessageContent
118+
{
119+
$cached = $this->cache->get($cacheKey);
120+
if (is_array($cached)
121+
&& array_key_exists('subject', $cached)
122+
&& array_key_exists('text', $cached)
123+
&& array_key_exists('textMessage', $cached)
124+
&& array_key_exists('footer', $cached)
125+
) {
126+
return new Message\MessageContent(
127+
$cached['subject'],
128+
$cached['text'],
129+
$cached['textMessage'],
130+
$cached['footer']
131+
);
132+
}
133+
134+
return null;
135+
}
136+
}

src/Domain/Messaging/Service/MessageProcessingPreparator.php

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PhpList\Core\Domain\Analytics\Service\LinkTrackService;
88
use PhpList\Core\Domain\Configuration\Service\UserPersonalizer;
99
use PhpList\Core\Domain\Messaging\Model\Message;
10+
use PhpList\Core\Domain\Messaging\Model\Message\MessageContent;
1011
use PhpList\Core\Domain\Messaging\Repository\MessageRepository;
1112
use PhpList\Core\Domain\Subscription\Model\Subscriber;
1213
use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository;
@@ -60,19 +61,21 @@ public function ensureCampaignsHaveUuid(OutputInterface $output): void
6061
/**
6162
* Process message content to extract URLs and replace them with link track URLs
6263
*/
63-
public function processMessageLinks(Message $message, Subscriber $subscriber): Message
64-
{
64+
public function processMessageLinks(
65+
int $campaignId,
66+
MessageContent $content,
67+
Subscriber $subscriber
68+
): MessageContent {
6569
if (!$this->linkTrackService->isExtractAndSaveLinksApplicable()) {
66-
return $message;
70+
return $content;
6771
}
6872

69-
$savedLinks = $this->linkTrackService->extractAndSaveLinks($message, $subscriber->getId());
73+
$savedLinks = $this->linkTrackService->extractAndSaveLinks($content, $subscriber->getId(), $campaignId);
7074

7175
if (empty($savedLinks)) {
72-
return $message;
76+
return $content;
7377
}
7478

75-
$content = $message->getContent();
7679
$htmlText = $content->getText();
7780
$footer = $content->getFooter();
7881
// todo: check other configured data that should be used in mail formatting/creation
@@ -88,7 +91,7 @@ public function processMessageLinks(Message $message, Subscriber $subscriber): M
8891
$content->setFooter($footer);
8992
}
9093

91-
return $message;
94+
return $content;
9295
}
9396

9497
private function replaceLinks(array $savedLinks, string $htmlText): string

src/Domain/Messaging/Service/RateLimitedCampaignMailer.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public function __construct(MailerInterface $mailer, SendRateLimiter $limiter)
2020
$this->limiter = $limiter;
2121
}
2222

23-
public function composeEmail(Message $processed, Subscriber $subscriber): Email
23+
public function composeEmail(Message $processed, Subscriber $subscriber, Message\MessageContent $content): Email
2424
{
2525
$email = new Email();
2626
if ($processed->getOptions()->getFromField() !== '') {
@@ -33,10 +33,10 @@ public function composeEmail(Message $processed, Subscriber $subscriber): Email
3333

3434
return $email
3535
->to($subscriber->getEmail())
36-
->subject($processed->getContent()->getSubject())
36+
->subject($content->getSubject())
3737
// todo: check HTML2Text functionality
38-
->text($processed->getContent()->getTextMessage())
39-
->html($processed->getContent()->getText());
38+
->text($content->getTextMessage())
39+
->html($content->getText());
4040
}
4141

4242
/**

tests/Unit/Domain/Analytics/Service/LinkTrackServiceTest.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public function testExtractAndSaveLinksWithNoLinks(): void
4444

4545
$this->linkTrackRepository->expects(self::never())->method('persist');
4646

47-
$result = $this->subject->extractAndSaveLinks($message, $userId);
47+
$result = $this->subject->extractAndSaveLinks($messageContent, $userId, $messageId);
4848

4949
self::assertEmpty($result);
5050
}
@@ -71,7 +71,7 @@ public function testExtractAndSaveLinksWithLinks(): void
7171
return null;
7272
});
7373

74-
$result = $this->subject->extractAndSaveLinks($message, $userId);
74+
$result = $this->subject->extractAndSaveLinks($messageContent, $userId, $messageId);
7575

7676
self::assertCount(2, $result);
7777
self::assertSame('https://example.com', $result[0]->getUrl());
@@ -100,7 +100,7 @@ public function testExtractAndSaveLinksWithFooter(): void
100100
return null;
101101
});
102102

103-
$result = $this->subject->extractAndSaveLinks($message, $userId);
103+
$result = $this->subject->extractAndSaveLinks($messageContent, $userId, $messageId);
104104

105105
self::assertCount(2, $result);
106106
self::assertSame('https://example.com', $result[0]->getUrl());
@@ -128,7 +128,7 @@ public function testExtractAndSaveLinksWithDuplicateLinks(): void
128128
return null;
129129
});
130130

131-
$result = $this->subject->extractAndSaveLinks($message, $userId);
131+
$result = $this->subject->extractAndSaveLinks($messageContent, $userId, $messageId);
132132

133133
self::assertCount(1, $result);
134134
self::assertSame('https://example.com', $result[0]->getUrl());
@@ -155,7 +155,7 @@ public function testExtractAndSaveLinksWithNullText(): void
155155
return null;
156156
});
157157

158-
$result = $this->subject->extractAndSaveLinks($message, $userId);
158+
$result = $this->subject->extractAndSaveLinks($messageContent, $userId, $messageId);
159159

160160
self::assertCount(1, $result);
161161
self::assertSame('https://footer.com', $result[0]->getUrl());
@@ -175,7 +175,7 @@ public function testExtractAndSaveLinksWithMessageWithoutId(): void
175175
$this->expectException(MissingMessageIdException::class);
176176
$this->expectExceptionMessage('Message must have an ID');
177177

178-
$this->subject->extractAndSaveLinks($message, $userId);
178+
$this->subject->extractAndSaveLinks($messageContent, $userId, $message->getId());
179179
}
180180

181181
public function testIsExtractAndSaveLinksApplicableWhenClickTrackIsTrue(): void
@@ -221,7 +221,7 @@ public function testExtractAndSaveLinksWithExistingLink(): void
221221
$this->linkTrackRepository->expects(self::never())
222222
->method('persist');
223223

224-
$result = $this->subject->extractAndSaveLinks($message, $userId);
224+
$result = $this->subject->extractAndSaveLinks($messageContent, $userId, $message->getId());
225225

226226
self::assertCount(1, $result);
227227
self::assertSame($existingLinkTrack, $result[0]);

0 commit comments

Comments
 (0)