PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.34.2
MailPoet – Newsletters, Email Marketing, and Automation v5.34.2
5.34.2 5.34.1 5.34.0 5.33.1 5.33.0 5.32.0 5.31.0 5.30.0 5.29.0 5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / lib / Newsletter / Scheduler / LatestNewsletterScheduler.php
mailpoet / lib / Newsletter / Scheduler Last commit date
AutomaticEmailScheduler.php 2 years ago AutomationEmailScheduler.php 1 year ago LatestNewsletterScheduler.php 2 months ago PostNotificationScheduler.php 2 months ago ReEngagementScheduler.php 5 days ago Scheduler.php 1 year ago WelcomeScheduler.php 2 years ago index.php 3 years ago
LatestNewsletterScheduler.php
309 lines
1 <?php declare(strict_types = 1);
2
3 namespace MailPoet\Newsletter\Scheduler;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Automation\Engine\Data\AutomationRun;
9 use MailPoet\Cron\Workers\SendingQueue\SendingQueue;
10 use MailPoet\Entities\NewsletterEntity;
11 use MailPoet\Entities\ScheduledTaskEntity;
12 use MailPoet\Entities\ScheduledTaskSubscriberEntity;
13 use MailPoet\Entities\SendingQueueEntity;
14 use MailPoet\Entities\StatisticsNewsletterEntity;
15 use MailPoet\Entities\SubscriberEntity;
16 use MailPoet\InvalidStateException;
17 use MailPoet\Newsletter\NewslettersRepository;
18 use MailPoet\Newsletter\Sending\NewsletterReplayMetadata;
19 use MailPoet\Newsletter\Sending\ScheduledTaskSubscribersRepository;
20 use MailPoetVendor\Carbon\Carbon;
21 use MailPoetVendor\Doctrine\ORM\EntityManager;
22 use MailPoetVendor\Doctrine\ORM\Query\Expr\Join;
23
24 class LatestNewsletterScheduler {
25 public const OUTCOME_SCHEDULED = 'scheduled';
26 public const OUTCOME_DUPLICATE = 'duplicate';
27 public const OUTCOME_SKIPPED_NO_NEWSLETTER = 'skipped-no-newsletter';
28
29 private EntityManager $entityManager;
30
31 private NewslettersRepository $newslettersRepository;
32
33 private ScheduledTaskSubscribersRepository $scheduledTaskSubscribersRepository;
34
35 public function __construct(
36 EntityManager $entityManager,
37 NewslettersRepository $newslettersRepository,
38 ScheduledTaskSubscribersRepository $scheduledTaskSubscribersRepository
39 ) {
40 $this->entityManager = $entityManager;
41 $this->newslettersRepository = $newslettersRepository;
42 $this->scheduledTaskSubscribersRepository = $scheduledTaskSubscribersRepository;
43 }
44
45 /**
46 * @param array{id:mixed,run_id:mixed,step_id:mixed,run_number:mixed} $automationMeta
47 * @return array{outcome: string, newsletter: NewsletterEntity|null, task_subscriber: ScheduledTaskSubscriberEntity|null}
48 */
49 public function schedule(SubscriberEntity $subscriber, int $segmentId, array $automationMeta): array {
50 $source = $this->newslettersRepository->findLatestSentStandardForSegment($segmentId);
51 if (!$source) {
52 return [
53 'outcome' => self::OUTCOME_SKIPPED_NO_NEWSLETTER,
54 'newsletter' => null,
55 'task_subscriber' => null,
56 ];
57 }
58
59 $newsletter = $source['newsletter'];
60 $subscriberId = $subscriber->getId();
61 $newsletterId = $newsletter->getId();
62 if (!$subscriberId || !$newsletterId) {
63 throw InvalidStateException::create();
64 }
65
66 $lockName = sprintf('mailpoet_latest_replay_%d_%d', $subscriberId, $newsletterId);
67 $this->acquireLock($lockName);
68 try {
69 return $this->entityManager->wrapInTransaction(function() use ($subscriber, $automationMeta, $source) {
70 $newsletter = $source['newsletter'];
71 $subscriberId = $subscriber->getId();
72 $newsletterId = $newsletter->getId();
73 if (!$subscriberId || !$newsletterId) {
74 throw InvalidStateException::create();
75 }
76
77 if (
78 $this->hasSuccessfulProcessedSend($newsletter, $subscriber)
79 || $this->hasStatisticsNewsletter($newsletter, $subscriber)
80 || $this->hasPendingNonReplayTaskSubscriber($newsletter, $subscriber)
81 ) {
82 return [
83 'outcome' => self::OUTCOME_DUPLICATE,
84 'newsletter' => $newsletter,
85 'task_subscriber' => null,
86 ];
87 }
88
89 $existingReplay = $this->findExistingReplayTaskSubscriber($newsletter, $subscriber);
90 if ($existingReplay instanceof ScheduledTaskSubscriberEntity) {
91 $task = $existingReplay->getTask();
92 $meta = $task ? $task->getMeta() : [];
93 $isSameRun = ($meta[NewsletterReplayMetadata::AUTOMATION]['run_id'] ?? null) === ($automationMeta['run_id'] ?? null);
94 return [
95 'outcome' => $isSameRun ? self::OUTCOME_SCHEDULED : self::OUTCOME_DUPLICATE,
96 'newsletter' => $newsletter,
97 'task_subscriber' => $isSameRun ? $existingReplay : null,
98 ];
99 }
100
101 $taskSubscriber = $this->createReplaySendingTask($source, $subscriber, $automationMeta);
102 return [
103 'outcome' => self::OUTCOME_SCHEDULED,
104 'newsletter' => $newsletter,
105 'task_subscriber' => $taskSubscriber,
106 ];
107 });
108 } finally {
109 $this->releaseLock($lockName);
110 }
111 }
112
113 public function getScheduledTaskSubscriber(NewsletterEntity $newsletter, SubscriberEntity $subscriber, AutomationRun $run): ?ScheduledTaskSubscriberEntity {
114 $results = $this->entityManager->createQueryBuilder()
115 ->select('sts')
116 ->from(ScheduledTaskSubscriberEntity::class, 'sts')
117 ->join('sts.task', 'st')
118 ->join('st.sendingQueue', 'sq')
119 ->where('sq.newsletter = :newsletter')
120 ->andWhere('sts.subscriber = :subscriber')
121 ->andWhere('st.createdAt >= :runCreatedAt')
122 ->setParameter('newsletter', $newsletter)
123 ->setParameter('subscriber', $subscriber)
124 ->setParameter('runCreatedAt', $run->getCreatedAt())
125 ->getQuery()
126 ->getResult();
127
128 foreach ($results as $scheduledTaskSubscriber) {
129 if (!$scheduledTaskSubscriber instanceof ScheduledTaskSubscriberEntity) {
130 continue;
131 }
132 $task = $scheduledTaskSubscriber->getTask();
133 if (!$task instanceof ScheduledTaskEntity || !NewsletterReplayMetadata::isLatestNewsletterReplayMeta($task->getMeta())) {
134 continue;
135 }
136 $meta = $task->getMeta();
137 if (($meta[NewsletterReplayMetadata::AUTOMATION]['run_id'] ?? null) === $run->getId()) {
138 return $scheduledTaskSubscriber;
139 }
140 }
141 return null;
142 }
143
144 public function saveErrorAndPause(ScheduledTaskSubscriberEntity $scheduledTaskSubscriber, string $error): void {
145 $task = $scheduledTaskSubscriber->getTask();
146 $subscriber = $scheduledTaskSubscriber->getSubscriber();
147 if (!$task || !$subscriber || !$subscriber->getId()) {
148 return;
149 }
150 $this->scheduledTaskSubscribersRepository->saveError($task, $subscriber->getId(), $error);
151 $task->setStatus(ScheduledTaskEntity::STATUS_PAUSED);
152 $this->entityManager->flush();
153 }
154
155 private function hasSuccessfulProcessedSend(NewsletterEntity $newsletter, SubscriberEntity $subscriber): bool {
156 $result = $this->entityManager->createQueryBuilder()
157 ->select('COUNT(st)')
158 ->from(ScheduledTaskSubscriberEntity::class, 'sts')
159 ->join('sts.task', 'st')
160 ->join(SendingQueueEntity::class, 'sq', Join::WITH, 'sq.task = st')
161 ->where('sq.newsletter = :newsletter')
162 ->andWhere('sts.subscriber = :subscriber')
163 ->andWhere('sts.processed = :processed')
164 ->andWhere('sts.failed = :notFailed')
165 ->andWhere('st.status = :completed')
166 ->setParameter('newsletter', $newsletter)
167 ->setParameter('subscriber', $subscriber)
168 ->setParameter('processed', ScheduledTaskSubscriberEntity::STATUS_PROCESSED)
169 ->setParameter('notFailed', ScheduledTaskSubscriberEntity::FAIL_STATUS_OK)
170 ->setParameter('completed', ScheduledTaskEntity::STATUS_COMPLETED)
171 ->getQuery()
172 ->getSingleScalarResult();
173
174 return (int)$result > 0;
175 }
176
177 private function hasPendingNonReplayTaskSubscriber(NewsletterEntity $newsletter, SubscriberEntity $subscriber): bool {
178 $result = $this->entityManager->createQueryBuilder()
179 ->select('COUNT(st)')
180 ->from(ScheduledTaskSubscriberEntity::class, 'sts')
181 ->join('sts.task', 'st')
182 ->join(SendingQueueEntity::class, 'sq', Join::WITH, 'sq.task = st')
183 ->where('sq.newsletter = :newsletter')
184 ->andWhere('sts.subscriber = :subscriber')
185 ->andWhere('sts.failed = :notFailed')
186 ->andWhere('(st.status = :scheduled OR st.status IS NULL)')
187 ->andWhere('st.meta IS NULL OR st.meta NOT LIKE :latestNewsletterReplayMeta')
188 ->andWhere('sq.meta IS NULL OR sq.meta NOT LIKE :latestNewsletterReplayMeta')
189 ->setParameter('newsletter', $newsletter)
190 ->setParameter('subscriber', $subscriber)
191 ->setParameter('notFailed', ScheduledTaskSubscriberEntity::FAIL_STATUS_OK)
192 ->setParameter('scheduled', ScheduledTaskEntity::STATUS_SCHEDULED)
193 ->setParameter('latestNewsletterReplayMeta', NewsletterReplayMetadata::getMetaLikePattern())
194 ->getQuery()
195 ->getSingleScalarResult();
196
197 return (int)$result > 0;
198 }
199
200 private function hasStatisticsNewsletter(NewsletterEntity $newsletter, SubscriberEntity $subscriber): bool {
201 $result = $this->entityManager->createQueryBuilder()
202 ->select('COUNT(statistics)')
203 ->from(StatisticsNewsletterEntity::class, 'statistics')
204 ->where('statistics.newsletter = :newsletter')
205 ->andWhere('statistics.subscriber = :subscriber')
206 ->setParameter('newsletter', $newsletter)
207 ->setParameter('subscriber', $subscriber)
208 ->getQuery()
209 ->getSingleScalarResult();
210
211 return (int)$result > 0;
212 }
213
214 private function findExistingReplayTaskSubscriber(NewsletterEntity $newsletter, SubscriberEntity $subscriber): ?ScheduledTaskSubscriberEntity {
215 $results = $this->entityManager->createQueryBuilder()
216 ->select('sts')
217 ->from(ScheduledTaskSubscriberEntity::class, 'sts')
218 ->join('sts.task', 'st')
219 ->join(SendingQueueEntity::class, 'sq', Join::WITH, 'sq.task = st')
220 ->where('sq.newsletter = :newsletter')
221 ->andWhere('sts.subscriber = :subscriber')
222 ->andWhere('sts.failed = :notFailed')
223 ->setParameter('newsletter', $newsletter)
224 ->setParameter('subscriber', $subscriber)
225 ->setParameter('notFailed', ScheduledTaskSubscriberEntity::FAIL_STATUS_OK)
226 ->getQuery()
227 ->getResult();
228
229 foreach ($results as $scheduledTaskSubscriber) {
230 if (!$scheduledTaskSubscriber instanceof ScheduledTaskSubscriberEntity) {
231 continue;
232 }
233 $task = $scheduledTaskSubscriber->getTask();
234 if (!$task instanceof ScheduledTaskEntity || !NewsletterReplayMetadata::isLatestNewsletterReplayMeta($task->getMeta())) {
235 continue;
236 }
237 $status = $task->getStatus();
238 if (in_array($status, [ScheduledTaskEntity::STATUS_SCHEDULED, null], true)) {
239 return $scheduledTaskSubscriber;
240 }
241 if (
242 $status === ScheduledTaskEntity::STATUS_COMPLETED
243 && $scheduledTaskSubscriber->getProcessed() === ScheduledTaskSubscriberEntity::STATUS_PROCESSED
244 ) {
245 return $scheduledTaskSubscriber;
246 }
247 }
248 return null;
249 }
250
251 /**
252 * @param array{newsletter: NewsletterEntity, queue: SendingQueueEntity, task: ScheduledTaskEntity} $source
253 */
254 private function createReplaySendingTask(array $source, SubscriberEntity $subscriber, array $automationMeta): ScheduledTaskSubscriberEntity {
255 $sourceTask = $source['task'];
256 $sourceQueue = $source['queue'];
257 $newsletter = $source['newsletter'];
258
259 $meta = [
260 NewsletterReplayMetadata::LATEST_NEWSLETTER_REPLAY => true,
261 NewsletterReplayMetadata::REPLAY_SOURCE_NEWSLETTER_ID => $newsletter->getId(),
262 NewsletterReplayMetadata::REPLAY_SOURCE_QUEUE_ID => $sourceQueue->getId(),
263 NewsletterReplayMetadata::REPLAY_SOURCE_TASK_ID => $sourceTask->getId(),
264 NewsletterReplayMetadata::REPLAY_SUBSCRIBER_ID => $subscriber->getId(),
265 NewsletterReplayMetadata::AUTOMATION => $automationMeta,
266 ];
267
268 $task = new ScheduledTaskEntity();
269 $task->setType(SendingQueue::TASK_TYPE);
270 $task->setStatus(ScheduledTaskEntity::STATUS_SCHEDULED);
271 $task->setScheduledAt(Carbon::now()->millisecond(0));
272 $task->setPriority(ScheduledTaskEntity::PRIORITY_MEDIUM);
273 $task->setMeta($meta);
274 $this->entityManager->persist($task);
275
276 $taskSubscriber = new ScheduledTaskSubscriberEntity($task, $subscriber);
277 $this->entityManager->persist($taskSubscriber);
278 $task->getSubscribers()->add($taskSubscriber);
279
280 $queue = new SendingQueueEntity();
281 $queue->setTask($task);
282 $task->setSendingQueue($queue);
283 $queue->setMeta($meta);
284 $queue->setNewsletter($newsletter);
285 $queue->setCountToProcess(1);
286 $queue->setCountTotal(1);
287 $this->entityManager->persist($queue);
288
289 return $taskSubscriber;
290 }
291
292 private function acquireLock(string $lockName): void {
293 $result = $this->entityManager->getConnection()->executeQuery(
294 'SELECT GET_LOCK(:lockName, 10)',
295 ['lockName' => $lockName]
296 )->fetchOne();
297 if (!is_numeric($result) || (int)$result !== 1) {
298 throw InvalidStateException::create()->withMessage(__('Could not create sending task.', 'mailpoet'));
299 }
300 }
301
302 private function releaseLock(string $lockName): void {
303 $this->entityManager->getConnection()->executeQuery(
304 'SELECT RELEASE_LOCK(:lockName)',
305 ['lockName' => $lockName]
306 );
307 }
308 }
309