PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.33.1
MailPoet – Newsletters, Email Marketing, and Automation v5.33.1
5.37.0 5.36.1 5.36.0 5.35.1 5.35.0 5.34.3 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 / Subscribers / InactiveSubscribersController.php
mailpoet / lib / Subscribers Last commit date
ConfirmationEmailTemplate 2 months ago ImportExport 1 month ago RestApi 1 month ago Statistics 2 months ago BulkActionController.php 3 months ago BulkActionException.php 3 months ago BulkConfirmationEmailResender.php 3 months ago ConfirmationEmailCustomizer.php 3 months ago ConfirmationEmailMailer.php 3 months ago ConfirmationEmailResolver.php 3 months ago EngagementDataBackfiller.php 3 months ago InactiveSubscribersController.php 1 month ago LinkTokens.php 3 months ago NewSubscriberNotificationMailer.php 3 months ago RequiredCustomFieldValidator.php 3 months ago SegmentsCountRecalculator.php 1 month ago Source.php 3 months ago SubscriberActions.php 3 months ago SubscriberCustomFieldRepository.php 3 years ago SubscriberIPsRepository.php 2 years ago SubscriberLimitNotificationEvaluator.php 3 months ago SubscriberLimitNotificationMailer.php 3 months ago SubscriberLimitNotificationScheduler.php 3 months ago SubscriberListingRepository.php 1 month ago SubscriberPersonalDataEraser.php 3 months ago SubscriberSaveController.php 3 months ago SubscriberSegmentRepository.php 1 month ago SubscriberSubscribeController.php 1 month ago SubscriberTagRepository.php 4 years ago SubscribersCountsController.php 1 month ago SubscribersEmailCountsController.php 1 month ago SubscribersRepository.php 1 month ago index.php 3 years ago
InactiveSubscribersController.php
192 lines
1 <?php declare(strict_types = 1);
2
3 namespace MailPoet\Subscribers;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Entities\ScheduledTaskEntity;
9 use MailPoet\Entities\ScheduledTaskSubscriberEntity;
10 use MailPoet\Entities\SendingQueueEntity;
11 use MailPoet\Entities\SubscriberEntity;
12 use MailPoetVendor\Carbon\Carbon;
13 use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
14 use MailPoetVendor\Doctrine\DBAL\ParameterType;
15 use MailPoetVendor\Doctrine\ORM\EntityManager;
16
17 class InactiveSubscribersController {
18
19 const UNOPENED_EMAILS_THRESHOLD = 3;
20 const LIFETIME_EMAILS_THRESHOLD = 10;
21
22 private $processedTaskIdsTableCreated = false;
23
24 /** @var EntityManager */
25 private $entityManager;
26
27 public function __construct(
28 EntityManager $entityManager
29 ) {
30 $this->entityManager = $entityManager;
31 }
32
33 public function markInactiveSubscribers(int $daysToInactive, int $startId, int $endId, ?int $unopenedEmails = self::UNOPENED_EMAILS_THRESHOLD) {
34 $thresholdDate = $this->getThresholdDate($daysToInactive);
35 return $this->deactivateSubscribers($thresholdDate, $startId, $endId, $unopenedEmails);
36 }
37
38 public function markActiveSubscribers(int $daysToInactive, int $batchSize): int {
39 $thresholdDate = $this->getThresholdDate($daysToInactive);
40 return $this->activateSubscribers($thresholdDate, $batchSize);
41 }
42
43 public function reactivateInactiveSubscribers(): void {
44 $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
45 $reactivateAllInactiveQuery = "
46 UPDATE {$subscribersTable} SET status = :statusSubscribed WHERE status = :statusInactive
47 ";
48 $this->entityManager->getConnection()->executeQuery($reactivateAllInactiveQuery, [
49 'statusSubscribed' => SubscriberEntity::STATUS_SUBSCRIBED,
50 'statusInactive' => SubscriberEntity::STATUS_INACTIVE,
51 ]);
52 }
53
54 private function getThresholdDate(int $daysToInactive): Carbon {
55 $now = new Carbon();
56 return $now->subDays($daysToInactive);
57 }
58
59 /**
60 * @return int
61 */
62 private function deactivateSubscribers(Carbon $thresholdDate, int $startId, int $endId, ?int $unopenedEmails = self::UNOPENED_EMAILS_THRESHOLD) {
63 $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
64 $scheduledTasksTable = $this->entityManager->getClassMetadata(ScheduledTaskEntity::class)->getTableName();
65 $scheduledTaskSubscribersTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
66 $sendingQueuesTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
67 $connection = $this->entityManager->getConnection();
68
69 $thresholdDateIso = $thresholdDate->toDateTimeString();
70 $dayAgo = new Carbon();
71 $dayAgoIso = $dayAgo->subDay()->toDateTimeString();
72
73 // Temporary table with processed tasks from threshold date up to yesterday
74 $processedTaskIdsTable = 'inactive_task_ids';
75 if (!$this->processedTaskIdsTableCreated) {
76 $processedTaskIdsTableSql = "
77 CREATE TEMPORARY TABLE IF NOT EXISTS {$processedTaskIdsTable}
78 (INDEX task_id_ids (id), PRIMARY KEY (`id`))
79 SELECT DISTINCT task_id as id FROM {$sendingQueuesTable} as sq
80 JOIN {$scheduledTasksTable} as st ON sq.task_id = st.id
81 WHERE st.processed_at > :thresholdDate
82 AND st.processed_at < :dayAgo
83 ";
84 $connection->executeQuery($processedTaskIdsTableSql, [
85 'thresholdDate' => $thresholdDateIso,
86 'dayAgo' => $dayAgoIso,
87 ]);
88 $this->processedTaskIdsTableCreated = true;
89 }
90
91 // Select subscribers who received at least a number of emails after threshold date and subscribed before that
92 $lifetimeEmailsThreshold = self::LIFETIME_EMAILS_THRESHOLD;
93 $inactiveSubscriberIdsTmpTable = 'inactive_subscriber_ids';
94 $connection->executeQuery(
95 "
96 CREATE TEMPORARY TABLE IF NOT EXISTS {$inactiveSubscriberIdsTmpTable}
97 (UNIQUE subscriber_id (id), PRIMARY KEY (`id`))
98 SELECT s.id FROM {$subscribersTable} as s
99 JOIN {$scheduledTaskSubscribersTable} as sts USE INDEX (subscriber_id) ON s.id = sts.subscriber_id
100 JOIN {$processedTaskIdsTable} task_ids ON task_ids.id = sts.task_id
101 WHERE s.last_subscribed_at < :thresholdDate
102 AND s.status = :status
103 AND s.id >= :startId
104 AND s.id <= :endId
105 AND s.email_count >= {$lifetimeEmailsThreshold}
106 GROUP BY s.id
107 HAVING count(s.id) >= :unopenedEmailsThreshold
108 ",
109 [
110 'thresholdDate' => $thresholdDateIso,
111 'status' => SubscriberEntity::STATUS_SUBSCRIBED,
112 'startId' => $startId,
113 'endId' => $endId,
114 'unopenedEmailsThreshold' => $unopenedEmails,
115 ]
116 );
117
118 $result = $connection->executeQuery("
119 SELECT isi.id FROM {$inactiveSubscriberIdsTmpTable} isi
120 LEFT OUTER JOIN {$subscribersTable} as s ON isi.id = s.id AND GREATEST(
121 COALESCE(s.last_engagement_at, '0'),
122 COALESCE(s.last_subscribed_at, '0'),
123 COALESCE(s.created_at, '0')
124 ) > :thresholdDate
125 WHERE s.id IS NULL
126 ", [
127 'thresholdDate' => $thresholdDateIso,
128 ]);
129 $idsToDeactivate = $result->fetchAllAssociative();
130
131 $connection->executeQuery("DROP TABLE {$inactiveSubscriberIdsTmpTable}");
132
133 $idsToDeactivate = $this->extractIds($idsToDeactivate);
134 if (!count($idsToDeactivate)) {
135 return 0;
136 }
137 $connection->executeQuery("UPDATE {$subscribersTable} SET status = :statusInactive WHERE id IN (:idsToDeactivate)", [
138 'statusInactive' => SubscriberEntity::STATUS_INACTIVE,
139 'idsToDeactivate' => $idsToDeactivate,
140 ], ['idsToDeactivate' => ArrayParameterType::INTEGER]);
141 return count($idsToDeactivate);
142 }
143
144 private function activateSubscribers(Carbon $thresholdDate, int $batchSize): int {
145 $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
146 $connection = $this->entityManager->getConnection();
147
148 $idsToActivate = $connection->executeQuery("
149 SELECT s.id
150 FROM {$subscribersTable} s
151 LEFT OUTER JOIN {$subscribersTable} s2 ON s.id = s2.id AND GREATEST(
152 COALESCE(s2.last_engagement_at, '0'),
153 COALESCE(s2.last_subscribed_at, '0'),
154 COALESCE(s2.created_at, '0')
155 ) > :thresholdDate
156 WHERE s.last_subscribed_at < :thresholdDate
157 AND s.status = :statusInactive
158 AND s2.id IS NOT NULL
159 GROUP BY s.id
160 LIMIT :batchSize
161 ", [
162 'thresholdDate' => $thresholdDate,
163 'statusInactive' => SubscriberEntity::STATUS_INACTIVE,
164 'batchSize' => $batchSize,
165 ], ['batchSize' => ParameterType::INTEGER])->fetchAllAssociative();
166
167 $idsToActivate = $this->extractIds($idsToActivate);
168 if (!count($idsToActivate)) {
169 return 0;
170 }
171 $connection->executeQuery("UPDATE {$subscribersTable} SET status = :statusSubscribed WHERE id IN (:idsToActivate)", [
172 'statusSubscribed' => SubscriberEntity::STATUS_SUBSCRIBED,
173 'idsToActivate' => $idsToActivate,
174 ], ['idsToActivate' => ArrayParameterType::INTEGER]);
175 return count($idsToActivate);
176 }
177
178 /**
179 * @param array<array<string, mixed>> $rows
180 * @return int[]
181 */
182 private function extractIds(array $rows): array {
183 $ids = [];
184 foreach ($rows as $row) {
185 if (isset($row['id']) && is_numeric($row['id'])) {
186 $ids[] = (int)$row['id'];
187 }
188 }
189 return $ids;
190 }
191 }
192