Nessuna descrizione

InactiveSubscribersController.php 8.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. <?php declare(strict_types = 1);
  2. namespace MailPoet\Subscribers;
  3. if (!defined('ABSPATH')) exit;
  4. use MailPoet\Config\MP2Migrator;
  5. use MailPoet\Entities\ScheduledTaskEntity;
  6. use MailPoet\Entities\ScheduledTaskSubscriberEntity;
  7. use MailPoet\Entities\SendingQueueEntity;
  8. use MailPoet\Entities\SubscriberEntity;
  9. use MailPoet\Settings\SettingsRepository;
  10. use MailPoetVendor\Carbon\Carbon;
  11. use MailPoetVendor\Doctrine\DBAL\Connection;
  12. use MailPoetVendor\Doctrine\ORM\EntityManager;
  13. class InactiveSubscribersController {
  14. private $inactiveTaskIdsTableCreated = false;
  15. /** @var SettingsRepository */
  16. private $settingsRepository;
  17. /** @var EntityManager */
  18. private $entityManager;
  19. public function __construct(
  20. EntityManager $entityManager,
  21. SettingsRepository $settingsRepository
  22. ) {
  23. $this->settingsRepository = $settingsRepository;
  24. $this->entityManager = $entityManager;
  25. }
  26. public function markInactiveSubscribers(int $daysToInactive, int $batchSize, ?int $startId = null) {
  27. $thresholdDate = $this->getThresholdDate($daysToInactive);
  28. return $this->deactivateSubscribers($thresholdDate, $batchSize, $startId);
  29. }
  30. public function markActiveSubscribers(int $daysToInactive, int $batchSize): int {
  31. $thresholdDate = $this->getThresholdDate($daysToInactive);
  32. return $this->activateSubscribers($thresholdDate, $batchSize);
  33. }
  34. public function reactivateInactiveSubscribers(): void {
  35. $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
  36. $reactivateAllInactiveQuery = "
  37. UPDATE {$subscribersTable} SET status = :statusSubscribed WHERE status = :statusInactive
  38. ";
  39. $this->entityManager->getConnection()->executeQuery($reactivateAllInactiveQuery, [
  40. 'statusSubscribed' => SubscriberEntity::STATUS_SUBSCRIBED,
  41. 'statusInactive' => SubscriberEntity::STATUS_INACTIVE,
  42. ]);
  43. }
  44. private function getThresholdDate(int $daysToInactive): Carbon {
  45. $now = new Carbon();
  46. return $now->subDays($daysToInactive);
  47. }
  48. /**
  49. * @return int|bool
  50. */
  51. private function deactivateSubscribers(Carbon $thresholdDate, int $batchSize, ?int $startId = null) {
  52. $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
  53. $scheduledTasksTable = $this->entityManager->getClassMetadata(ScheduledTaskEntity::class)->getTableName();
  54. $scheduledTaskSubscribersTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
  55. $sendingQueuesTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
  56. $connection = $this->entityManager->getConnection();
  57. $thresholdDateIso = $thresholdDate->toDateTimeString();
  58. $dayAgo = new Carbon();
  59. $dayAgoIso = $dayAgo->subDay()->toDateTimeString();
  60. // If MP2 migration occurred during detection interval we can't deactivate subscribers
  61. // because they are imported with original subscription date but they were not present in a list for whole period
  62. $mp2MigrationDate = $this->getMP2MigrationDate();
  63. if ($mp2MigrationDate && $mp2MigrationDate > $thresholdDate) {
  64. return false;
  65. }
  66. // We take into account only emails which have at least one opening tracked
  67. // to ensure that tracking was enabled for the particular email
  68. $inactiveTaskIdsTable = 'inactive_task_ids';
  69. if (!$this->inactiveTaskIdsTableCreated) {
  70. $inactiveTaskIdsTableSql = "
  71. CREATE TEMPORARY TABLE IF NOT EXISTS {$inactiveTaskIdsTable}
  72. (INDEX task_id_ids (id))
  73. SELECT DISTINCT task_id as id FROM {$sendingQueuesTable} as sq
  74. JOIN {$scheduledTasksTable} as st ON sq.task_id = st.id
  75. WHERE st.processed_at > :thresholdDate
  76. AND st.processed_at < :dayAgo
  77. ";
  78. $connection->executeQuery($inactiveTaskIdsTableSql, [
  79. 'thresholdDate' => $thresholdDateIso,
  80. 'dayAgo' => $dayAgoIso,
  81. ]);
  82. $this->inactiveTaskIdsTableCreated = true;
  83. }
  84. // Select subscribers who received a recent tracked email but didn't open it
  85. $startId = (int)$startId;
  86. $endId = $startId + $batchSize;
  87. $inactiveSubscriberIdsTmpTable = 'inactive_subscriber_ids';
  88. $connection->executeQuery("
  89. CREATE TEMPORARY TABLE IF NOT EXISTS {$inactiveSubscriberIdsTmpTable}
  90. (UNIQUE subscriber_id (id))
  91. SELECT DISTINCT s.id FROM {$subscribersTable} as s
  92. JOIN {$scheduledTaskSubscribersTable} as sts USE INDEX (subscriber_id) ON s.id = sts.subscriber_id
  93. JOIN {$inactiveTaskIdsTable} task_ids ON task_ids.id = sts.task_id
  94. WHERE s.last_subscribed_at < :thresholdDate
  95. AND s.status = :status
  96. AND s.id >= :startId
  97. AND s.id < :endId
  98. ",
  99. [
  100. 'thresholdDate' => $thresholdDateIso,
  101. 'status' => SubscriberEntity::STATUS_SUBSCRIBED,
  102. 'startId' => $startId,
  103. 'endId' => $endId,
  104. ]);
  105. $result = $connection->executeQuery("
  106. SELECT isi.id FROM {$inactiveSubscriberIdsTmpTable} isi
  107. LEFT OUTER JOIN {$subscribersTable} as s ON isi.id = s.id AND GREATEST(
  108. COALESCE(s.last_engagement_at, '0'),
  109. COALESCE(s.last_subscribed_at, '0'),
  110. COALESCE(s.created_at, '0')
  111. ) > :thresholdDate
  112. WHERE s.id IS NULL
  113. ", [
  114. 'thresholdDate' => $thresholdDateIso,
  115. ]);
  116. $idsToDeactivate = $result->fetchAllAssociative();
  117. $connection->executeQuery("DROP TABLE {$inactiveSubscriberIdsTmpTable}");
  118. $idsToDeactivate = array_map(
  119. function ($id) {
  120. return (int)$id['id'];
  121. },
  122. $idsToDeactivate
  123. );
  124. if (!count($idsToDeactivate)) {
  125. return 0;
  126. }
  127. $connection->executeQuery("UPDATE {$subscribersTable} SET status = :statusInactive WHERE id IN (:idsToDeactivate)", [
  128. 'statusInactive' => SubscriberEntity::STATUS_INACTIVE,
  129. 'idsToDeactivate' => $idsToDeactivate,
  130. ], ['idsToDeactivate' => Connection::PARAM_INT_ARRAY]);
  131. return count($idsToDeactivate);
  132. }
  133. private function activateSubscribers(Carbon $thresholdDate, int $batchSize): int {
  134. $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
  135. $connection = $this->entityManager->getConnection();
  136. $mp2MigrationDate = $this->getMP2MigrationDate();
  137. if ($mp2MigrationDate && $mp2MigrationDate > $thresholdDate) {
  138. // If MP2 migration occurred during detection interval re-activate all subscribers created before migration
  139. $idsToActivate = $connection->executeQuery("
  140. SELECT id
  141. FROM {$subscribersTable}
  142. WHERE created_at < :migrationDate
  143. AND status = :statusInactive
  144. LIMIT :batchSize
  145. ", [
  146. 'migrationDate' => $mp2MigrationDate,
  147. 'statusInactive' => SubscriberEntity::STATUS_INACTIVE,
  148. 'batchSize' => $batchSize,
  149. ], ['batchSize' => \PDO::PARAM_INT])->fetchAllAssociative();
  150. } else {
  151. $idsToActivate = $connection->executeQuery("
  152. SELECT s.id
  153. FROM {$subscribersTable} s
  154. LEFT OUTER JOIN {$subscribersTable} s2 ON s.id = s2.id AND GREATEST(
  155. COALESCE(s2.last_engagement_at, '0'),
  156. COALESCE(s2.last_subscribed_at, '0'),
  157. COALESCE(s2.created_at, '0')
  158. ) > :thresholdDate
  159. WHERE s.last_subscribed_at < :thresholdDate
  160. AND s.status = :statusInactive
  161. AND s2.id IS NOT NULL
  162. GROUP BY s.id
  163. LIMIT :batchSize
  164. ", [
  165. 'thresholdDate' => $thresholdDate,
  166. 'statusInactive' => SubscriberEntity::STATUS_INACTIVE,
  167. 'batchSize' => $batchSize,
  168. ], ['batchSize' => \PDO::PARAM_INT])->fetchAllAssociative();
  169. }
  170. $idsToActivate = array_map(
  171. function($id) {
  172. return (int)$id['id'];
  173. }, $idsToActivate
  174. );
  175. if (!count($idsToActivate)) {
  176. return 0;
  177. }
  178. $connection->executeQuery("UPDATE {$subscribersTable} SET status = :statusSubscribed WHERE id IN (:idsToActivate)", [
  179. 'statusSubscribed' => SubscriberEntity::STATUS_SUBSCRIBED,
  180. 'idsToActivate' => $idsToActivate,
  181. ], ['idsToActivate' => Connection::PARAM_INT_ARRAY]);
  182. return count($idsToActivate);
  183. }
  184. private function getMP2MigrationDate() {
  185. $setting = $this->settingsRepository->findOneByName(MP2Migrator::MIGRATION_COMPLETE_SETTING_KEY);
  186. return $setting ? Carbon::instance($setting->getCreatedAt()) : null;
  187. }
  188. }