暂无描述

NewslettersRepository.php 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. <?php
  2. namespace MailPoet\Newsletter;
  3. if (!defined('ABSPATH')) exit;
  4. use DateTimeInterface;
  5. use MailPoet\AutomaticEmails\WooCommerce\Events\AbandonedCart;
  6. use MailPoet\AutomaticEmails\WooCommerce\Events\FirstPurchase;
  7. use MailPoet\AutomaticEmails\WooCommerce\Events\PurchasedInCategory;
  8. use MailPoet\AutomaticEmails\WooCommerce\Events\PurchasedProduct;
  9. use MailPoet\Doctrine\Repository;
  10. use MailPoet\Entities\NewsletterEntity;
  11. use MailPoet\Entities\NewsletterLinkEntity;
  12. use MailPoet\Entities\NewsletterOptionEntity;
  13. use MailPoet\Entities\NewsletterOptionFieldEntity;
  14. use MailPoet\Entities\NewsletterPostEntity;
  15. use MailPoet\Entities\NewsletterSegmentEntity;
  16. use MailPoet\Entities\ScheduledTaskEntity;
  17. use MailPoet\Entities\ScheduledTaskSubscriberEntity;
  18. use MailPoet\Entities\SendingQueueEntity;
  19. use MailPoet\Entities\StatisticsClickEntity;
  20. use MailPoet\Entities\StatisticsNewsletterEntity;
  21. use MailPoet\Entities\StatisticsOpenEntity;
  22. use MailPoet\Entities\StatisticsWooCommercePurchaseEntity;
  23. use MailPoet\Entities\StatsNotificationEntity;
  24. use MailPoet\Logging\LoggerFactory;
  25. use MailPoetVendor\Carbon\Carbon;
  26. use MailPoetVendor\Doctrine\DBAL\Connection;
  27. use MailPoetVendor\Doctrine\ORM\EntityManager;
  28. use MailPoetVendor\Doctrine\ORM\Query\Expr\Join;
  29. /**
  30. * @extends Repository<NewsletterEntity>
  31. */
  32. class NewslettersRepository extends Repository {
  33. /** @var LoggerFactory */
  34. private $loggerFactory;
  35. public function __construct(
  36. EntityManager $entityManager
  37. ) {
  38. parent::__construct($entityManager);
  39. $this->loggerFactory = LoggerFactory::getInstance();
  40. }
  41. protected function getEntityClassName() {
  42. return NewsletterEntity::class;
  43. }
  44. /**
  45. * @param string[] $types
  46. * @return NewsletterEntity[]
  47. */
  48. public function findActiveByTypes($types) {
  49. return $this->entityManager
  50. ->createQueryBuilder()
  51. ->select('n')
  52. ->from(NewsletterEntity::class, 'n')
  53. ->where('n.status = :status')
  54. ->setParameter(':status', NewsletterEntity::STATUS_ACTIVE)
  55. ->andWhere('n.deletedAt is null')
  56. ->andWhere('n.type IN (:types)')
  57. ->setParameter('types', $types)
  58. ->orderBy('n.subject')
  59. ->getQuery()
  60. ->getResult();
  61. }
  62. public function getStandardNewsletterSentCount(DateTimeInterface $since): int {
  63. return (int)$this->doctrineRepository->createQueryBuilder('n')
  64. ->select('COUNT(n)')
  65. ->join('n.queues', 'q')
  66. ->join('q.task', 't')
  67. ->andWhere('n.type = :type')
  68. ->andWhere('n.status = :status')
  69. ->andWhere('t.status = :taskStatus')
  70. ->andWhere('t.processedAt >= :since')
  71. ->setParameter('type', NewsletterEntity::TYPE_STANDARD)
  72. ->setParameter('status', NewsletterEntity::STATUS_SENT)
  73. ->setParameter('taskStatus', ScheduledTaskEntity::STATUS_COMPLETED)
  74. ->setParameter('since', $since)
  75. ->getQuery()
  76. ->getSingleScalarResult() ?: 0;
  77. }
  78. public function getAnalytics(): array {
  79. // for automatic emails join 'event' newsletter option to further group the counts
  80. $eventOptionId = (int)$this->entityManager->createQueryBuilder()
  81. ->select('nof.id')
  82. ->from(NewsletterOptionFieldEntity::class, 'nof')
  83. ->andWhere('nof.newsletterType = :eventOptionFieldType')
  84. ->andWhere('nof.name = :eventOptionFieldName')
  85. ->setParameter('eventOptionFieldType', NewsletterEntity::TYPE_AUTOMATIC)
  86. ->setParameter('eventOptionFieldName', 'event')
  87. ->getQuery()
  88. ->getSingleScalarResult();
  89. $results = $this->doctrineRepository->createQueryBuilder('n')
  90. ->select('n.type, eventOption.value AS event, COUNT(n) AS cnt')
  91. ->leftJoin('n.options', 'eventOption', Join::WITH, "eventOption.optionField = :eventOptionId")
  92. ->andWhere('n.deletedAt IS NULL')
  93. ->andWhere('n.status IN (:statuses)')
  94. ->setParameter('eventOptionId', $eventOptionId)
  95. ->setParameter('statuses', [NewsletterEntity::STATUS_ACTIVE, NewsletterEntity::STATUS_SENT])
  96. ->groupBy('n.type, eventOption.value')
  97. ->getQuery()
  98. ->getResult();
  99. $analyticsMap = [];
  100. foreach ($results as $result) {
  101. $type = $result['type'];
  102. if ($type === NewsletterEntity::TYPE_AUTOMATIC) {
  103. $analyticsMap[$type][$result['event'] ?? ''] = (int)$result['cnt'];
  104. } else {
  105. $analyticsMap[$type] = (int)$result['cnt'];
  106. }
  107. }
  108. return [
  109. 'welcome_newsletters_count' => $analyticsMap[NewsletterEntity::TYPE_WELCOME] ?? 0,
  110. 'notifications_count' => $analyticsMap[NewsletterEntity::TYPE_NOTIFICATION] ?? 0,
  111. 'automatic_emails_count' => array_sum($analyticsMap[NewsletterEntity::TYPE_AUTOMATIC] ?? []),
  112. 'sent_newsletters_count' => $analyticsMap[NewsletterEntity::TYPE_STANDARD] ?? 0,
  113. 'sent_newsletters_3_months' => $this->getStandardNewsletterSentCount(Carbon::now()->subMonths(3)),
  114. 'sent_newsletters_30_days' => $this->getStandardNewsletterSentCount(Carbon::now()->subDays(30)),
  115. 'first_purchase_emails_count' => $analyticsMap[NewsletterEntity::TYPE_AUTOMATIC][FirstPurchase::SLUG] ?? 0,
  116. 'product_purchased_emails_count' => $analyticsMap[NewsletterEntity::TYPE_AUTOMATIC][PurchasedProduct::SLUG] ?? 0,
  117. 'product_purchased_in_category_emails_count' => $analyticsMap[NewsletterEntity::TYPE_AUTOMATIC][PurchasedInCategory::SLUG] ?? 0,
  118. 'abandoned_cart_emails_count' => $analyticsMap[NewsletterEntity::TYPE_AUTOMATIC][AbandonedCart::SLUG] ?? 0,
  119. ];
  120. }
  121. /**
  122. * @param array $segmentIds
  123. * @return NewsletterEntity[]
  124. */
  125. public function getArchives(array $segmentIds = []) {
  126. $types = [
  127. NewsletterEntity::TYPE_STANDARD,
  128. NewsletterEntity::TYPE_NOTIFICATION_HISTORY,
  129. ];
  130. $queryBuilder = $this->entityManager
  131. ->createQueryBuilder()
  132. ->select('n')
  133. ->distinct()
  134. ->from(NewsletterEntity::class, 'n')
  135. ->innerJoin(SendingQueueEntity::class, 'sq', Join::WITH, 'sq.newsletter = n.id')
  136. ->innerJoin(ScheduledTaskEntity::class, 'st', Join::WITH, 'st.id = sq.task')
  137. ->where('n.type IN (:types)')
  138. ->andWhere('st.status = :statusCompleted')
  139. ->andWhere('n.deletedAt IS NULL')
  140. ->orderBy('st.processedAt', 'DESC')
  141. ->addOrderBy('st.id', 'ASC')
  142. ->setParameter('types', $types)
  143. ->setParameter('statusCompleted', SendingQueueEntity::STATUS_COMPLETED);
  144. if (!empty($segmentIds)) {
  145. $queryBuilder->innerJoin(NewsletterSegmentEntity::class, 'ns', Join::WITH, 'ns.newsletter = n.id')
  146. ->andWhere('ns.segment IN (:segmentIds)')
  147. ->setParameter('segmentIds', $segmentIds);
  148. }
  149. return $queryBuilder->getQuery()->getResult();
  150. }
  151. /**
  152. * @return int - number of processed ids
  153. */
  154. public function bulkTrash(array $ids): int {
  155. if (empty($ids)) {
  156. return 0;
  157. }
  158. $this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS, $attachProcessors = true)->addInfo(
  159. 'trashing newsletters', ['id' => $ids]
  160. );
  161. // Fetch children id for trashing
  162. $childrenIds = $this->fetchChildrenIds($ids);
  163. $ids = array_merge($ids, $childrenIds);
  164. $this->entityManager->createQueryBuilder()
  165. ->update(NewsletterEntity::class, 'n')
  166. ->set('n.deletedAt', 'CURRENT_TIMESTAMP()')
  167. ->where('n.id IN (:ids)')
  168. ->setParameter('ids', $ids)
  169. ->getQuery()->execute();
  170. // Trash scheduled tasks
  171. $scheduledTasksTable = $this->entityManager->getClassMetadata(ScheduledTaskEntity::class)->getTableName();
  172. $sendingQueueTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
  173. $this->entityManager->getConnection()->executeUpdate("
  174. UPDATE $scheduledTasksTable t
  175. JOIN $sendingQueueTable q ON t.`id` = q.`task_id`
  176. SET t.`deleted_at` = NOW()
  177. WHERE q.`newsletter_id` IN (:ids)
  178. ", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
  179. // Trash sending queues
  180. $this->entityManager->getConnection()->executeUpdate("
  181. UPDATE $sendingQueueTable q
  182. SET q.`deleted_at` = NOW()
  183. WHERE q.`newsletter_id` IN (:ids)
  184. ", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
  185. return count($ids);
  186. }
  187. public function bulkRestore(array $ids) {
  188. if (empty($ids)) {
  189. return 0;
  190. }
  191. // Fetch children ids to restore
  192. $childrenIds = $this->fetchChildrenIds($ids);
  193. $ids = array_merge($ids, $childrenIds);
  194. $this->entityManager->createQueryBuilder()->update(NewsletterEntity::class, 'n')
  195. ->set('n.deletedAt', ':deletedAt')
  196. ->where('n.id IN (:ids)')
  197. ->setParameter('deletedAt', null)
  198. ->setParameter('ids', $ids)
  199. ->getQuery()->execute();
  200. // Restore scheduled tasks and pause running ones
  201. $scheduledTasksTable = $this->entityManager->getClassMetadata(ScheduledTaskEntity::class)->getTableName();
  202. $sendingQueueTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
  203. $this->entityManager->getConnection()->executeUpdate("
  204. UPDATE $scheduledTasksTable t
  205. JOIN $sendingQueueTable q ON t.`id` = q.`task_id`
  206. SET t.`deleted_at` = null, t.`status` = IFNULL(t.status, :pausedStatus)
  207. WHERE q.`newsletter_id` IN (:ids)
  208. ", [
  209. 'ids' => $ids,
  210. 'pausedStatus' => ScheduledTaskEntity::STATUS_PAUSED,
  211. ], [
  212. 'ids' => Connection::PARAM_INT_ARRAY,
  213. ]);
  214. // Restore sending queues
  215. $this->entityManager->getConnection()->executeUpdate("
  216. UPDATE $sendingQueueTable q
  217. SET q.`deleted_at` = null
  218. WHERE q.`newsletter_id` IN (:ids)
  219. ", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
  220. return count($ids);
  221. }
  222. public function bulkDelete(array $ids) {
  223. if (empty($ids)) {
  224. return 0;
  225. }
  226. // Fetch children ids for deleting
  227. $childrenIds = $this->fetchChildrenIds($ids);
  228. $ids = array_merge($ids, $childrenIds);
  229. $this->entityManager->transactional(function (EntityManager $entityManager) use ($ids) {
  230. // Delete statistics data
  231. $newsletterStatisticsTable = $entityManager->getClassMetadata(StatisticsNewsletterEntity::class)->getTableName();
  232. $entityManager->getConnection()->executeUpdate("
  233. DELETE s FROM $newsletterStatisticsTable s
  234. WHERE s.`newsletter_id` IN (:ids)
  235. ", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
  236. $statisticsOpensTable = $entityManager->getClassMetadata(StatisticsOpenEntity::class)->getTableName();
  237. $entityManager->getConnection()->executeUpdate("
  238. DELETE s FROM $statisticsOpensTable s
  239. WHERE s.`newsletter_id` IN (:ids)
  240. ", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
  241. $statisticsClicksTable = $entityManager->getClassMetadata(StatisticsClickEntity::class)->getTableName();
  242. $entityManager->getConnection()->executeUpdate("
  243. DELETE s FROM $statisticsClicksTable s
  244. WHERE s.`newsletter_id` IN (:ids)
  245. ", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
  246. $statisticsPurchasesTable = $entityManager->getClassMetadata(StatisticsWooCommercePurchaseEntity::class)->getTableName();
  247. $entityManager->getConnection()->executeUpdate("
  248. DELETE s FROM $statisticsPurchasesTable s
  249. WHERE s.`newsletter_id` IN (:ids)
  250. ", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
  251. // Delete newsletter posts
  252. $postsTable = $entityManager->getClassMetadata(NewsletterPostEntity::class)->getTableName();
  253. $entityManager->getConnection()->executeUpdate("
  254. DELETE np FROM $postsTable np
  255. WHERE np.`newsletter_id` IN (:ids)
  256. ", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
  257. // Delete newsletter options
  258. $optionsTable = $entityManager->getClassMetadata(NewsletterOptionEntity::class)->getTableName();
  259. $entityManager->getConnection()->executeUpdate("
  260. DELETE no FROM $optionsTable no
  261. WHERE no.`newsletter_id` IN (:ids)
  262. ", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
  263. // Delete newsletter links
  264. $linksTable = $entityManager->getClassMetadata(NewsletterLinkEntity::class)->getTableName();
  265. $entityManager->getConnection()->executeUpdate("
  266. DELETE nl FROM $linksTable nl
  267. WHERE nl.`newsletter_id` IN (:ids)
  268. ", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
  269. // Delete stats notifications tasks
  270. $scheduledTasksTable = $entityManager->getClassMetadata(ScheduledTaskEntity::class)->getTableName();
  271. $statsNotificationsTable = $entityManager->getClassMetadata(StatsNotificationEntity::class)->getTableName();
  272. $taskIds = $entityManager->getConnection()->executeQuery("
  273. SELECT task_id FROM $statsNotificationsTable sn
  274. WHERE sn.`newsletter_id` IN (:ids)
  275. ", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY])->fetchAll();
  276. $taskIds = array_column($taskIds, 'task_id');
  277. $entityManager->getConnection()->executeUpdate("
  278. DELETE st FROM $scheduledTasksTable st
  279. WHERE st.`id` IN (:ids)
  280. ", ['ids' => $taskIds], ['ids' => Connection::PARAM_INT_ARRAY]);
  281. // Delete stats notifications
  282. $entityManager->getConnection()->executeUpdate("
  283. DELETE sn FROM $statsNotificationsTable sn
  284. WHERE sn.`newsletter_id` IN (:ids)
  285. ", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
  286. // Delete scheduled tasks and scheduled task subscribers
  287. $sendingQueueTable = $entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
  288. $scheduledTaskSubscribersTable = $entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
  289. // Delete scheduled tasks subscribers
  290. $entityManager->getConnection()->executeUpdate("
  291. DELETE ts FROM $scheduledTaskSubscribersTable ts
  292. JOIN $scheduledTasksTable t ON t.`id` = ts.`task_id`
  293. JOIN $sendingQueueTable q ON q.`task_id` = t.`id`
  294. WHERE q.`newsletter_id` IN (:ids)
  295. ", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
  296. $entityManager->getConnection()->executeUpdate("
  297. DELETE t FROM $scheduledTasksTable t
  298. JOIN $sendingQueueTable q ON t.`id` = q.`task_id`
  299. WHERE q.`newsletter_id` IN (:ids)
  300. ", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
  301. // Delete sending queues
  302. $entityManager->getConnection()->executeUpdate("
  303. DELETE q FROM $sendingQueueTable q
  304. WHERE q.`newsletter_id` IN (:ids)
  305. ", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
  306. // Delete newsletter segments
  307. $newsletterSegmentsTable = $entityManager->getClassMetadata(NewsletterSegmentEntity::class)->getTableName();
  308. $entityManager->getConnection()->executeUpdate("
  309. DELETE ns FROM $newsletterSegmentsTable ns
  310. WHERE ns.`newsletter_id` IN (:ids)
  311. ", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
  312. $queryBuilder = $entityManager->createQueryBuilder();
  313. $queryBuilder->delete(NewsletterEntity::class, 'n')
  314. ->where('n.id IN (:ids)')
  315. ->setParameter('ids', $ids)
  316. ->getQuery()->execute();
  317. });
  318. return count($ids);
  319. }
  320. /**
  321. * @return NewsletterEntity[]
  322. */
  323. public function findSendigNotificationHistoryWithPausedTask(NewsletterEntity $newsletter): array {
  324. $result = $this->entityManager->createQueryBuilder()
  325. ->select('n')
  326. ->from(NewsletterEntity::class, 'n')
  327. ->join('n.queues', 'q')
  328. ->join('q.task', 't')
  329. ->where('n.parent = :parent')
  330. ->andWhere('n.type = :type')
  331. ->andWhere('n.status = :status')
  332. ->andWhere('t.status != :taskStatus')
  333. ->setParameter('parent', $newsletter)
  334. ->setParameter('type', NewsletterEntity::TYPE_NOTIFICATION_HISTORY)
  335. ->setParameter('status', NewsletterEntity::STATUS_SENDING)
  336. ->setParameter('taskStatus', ScheduledTaskEntity::STATUS_PAUSED)
  337. ->getQuery()->execute();
  338. return $result;
  339. }
  340. public function prefetchOptions(array $newsletters) {
  341. $this->entityManager->createQueryBuilder()
  342. ->select('PARTIAL n.{id}, o, opf')
  343. ->from(NewsletterEntity::class, 'n')
  344. ->join('n.options', 'o')
  345. ->join('o.optionField', 'opf')
  346. ->where('n.id IN (:newsletters)')
  347. ->setParameter('newsletters', $newsletters)
  348. ->getQuery()
  349. ->getResult();
  350. }
  351. public function prefetchSegments(array $newsletters) {
  352. $this->entityManager->createQueryBuilder()
  353. ->select('PARTIAL n.{id}, ns, s')
  354. ->from(NewsletterEntity::class, 'n')
  355. ->join('n.newsletterSegments', 'ns')
  356. ->join('ns.segment', 's')
  357. ->where('n.id IN (:newsletters)')
  358. ->setParameter('newsletters', $newsletters)
  359. ->getQuery()
  360. ->getResult();
  361. }
  362. private function fetchChildrenIds(array $parentIds) {
  363. $ids = $this->entityManager->createQueryBuilder()->select('n.id')
  364. ->from(NewsletterEntity::class, 'n')
  365. ->where('n.parent IN (:ids)')
  366. ->setParameter('ids', $parentIds)
  367. ->getQuery()->getScalarResult();
  368. return array_column($ids, 'id');
  369. }
  370. }