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 / Statistics / StatisticsOpensRepository.php
mailpoet / lib / Statistics Last commit date
Track 4 days ago GATracking.php 6 months ago StatisticsBouncesRepository.php 3 years ago StatisticsClicksRepository.php 2 years ago StatisticsFormsRepository.php 3 years ago StatisticsNewslettersRepository.php 1 year ago StatisticsOpensRepository.php 3 days ago StatisticsUnsubscribesRepository.php 2 months ago StatisticsWooCommercePurchasesRepository.php 1 year ago UnsubscribeReasonTracker.php 2 months ago UserAgentsRepository.php 1 year ago index.php 3 years ago
StatisticsOpensRepository.php
167 lines
1 <?php declare(strict_types = 1);
2
3 namespace MailPoet\Statistics;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Doctrine\Repository;
9 use MailPoet\Entities\SegmentEntity;
10 use MailPoet\Entities\StatisticsNewsletterEntity;
11 use MailPoet\Entities\StatisticsOpenEntity;
12 use MailPoet\Entities\SubscriberEntity;
13 use MailPoet\Entities\UserAgentEntity;
14 use MailPoet\Settings\TrackingConfig;
15 use MailPoet\Subscribers\Statistics\SubscriberStatisticsRepository;
16 use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
17 use MailPoetVendor\Doctrine\ORM\EntityManager;
18 use MailPoetVendor\Doctrine\ORM\QueryBuilder;
19
20 /**
21 * @extends Repository<StatisticsOpenEntity>
22 */
23 class StatisticsOpensRepository extends Repository {
24 /** @var TrackingConfig */
25 private $trackingConfig;
26
27 public function __construct(
28 EntityManager $entityManager,
29 TrackingConfig $trackingConfig
30 ) {
31 parent::__construct($entityManager);
32 $this->entityManager = $entityManager;
33 $this->trackingConfig = $trackingConfig;
34 }
35
36 protected function getEntityClassName(): string {
37 return StatisticsOpenEntity::class;
38 }
39
40 public function recalculateSubscriberScore(SubscriberEntity $subscriber): void {
41 $subscriberId = $subscriber->getId();
42 if (!$subscriberId) {
43 return;
44 }
45 $this->recalculateSubscribersScore([$subscriberId]);
46 }
47
48 /**
49 * Recalculates and persists engagement scores in a single bulk UPDATE. Runs entirely
50 * in SQL so large batches avoid entity hydration, validation, and per-entity flushes.
51 * In-memory SubscriberEntity instances are not refreshed and keep stale score fields.
52 *
53 * @param int[] $subscriberIds
54 */
55 public function recalculateSubscribersScore(array $subscriberIds): void {
56 if (!$subscriberIds) {
57 return;
58 }
59 $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
60 $sentStatsTable = $this->entityManager->getClassMetadata(StatisticsNewsletterEntity::class)->getTableName();
61 $openStatsTable = $this->entityManager->getClassMetadata(StatisticsOpenEntity::class)->getTableName();
62 $humanOpensCondition = $this->trackingConfig->areOpensSeparated() ? ' AND so.user_agent_type = :userAgentType' : '';
63
64 $sql = "
65 UPDATE {$subscribersTable} s
66 LEFT JOIN (
67 SELECT subscriber_id, COUNT(DISTINCT newsletter_id) AS sent_count
68 FROM {$sentStatsTable}
69 WHERE subscriber_id IN (:ids) AND sent_at >= :yearAgo
70 GROUP BY subscriber_id
71 ) sent ON sent.subscriber_id = s.id
72 LEFT JOIN (
73 SELECT so.subscriber_id, COUNT(DISTINCT so.newsletter_id) AS open_count
74 FROM {$openStatsTable} so
75 JOIN {$sentStatsTable} sn
76 ON sn.newsletter_id = so.newsletter_id
77 AND sn.subscriber_id = so.subscriber_id
78 AND sn.sent_at >= :yearAgo
79 WHERE so.subscriber_id IN (:ids){$humanOpensCondition}
80 GROUP BY so.subscriber_id
81 ) opens ON opens.subscriber_id = s.id
82 SET s.engagement_score = CASE
83 WHEN COALESCE(sent.sent_count, 0) < :minSentCount THEN NULL
84 ELSE COALESCE(opens.open_count, 0) / sent.sent_count * 100
85 END,
86 s.engagement_score_updated_at = :now
87 WHERE s.id IN (:ids)
88 ";
89
90 $parameters = [
91 'ids' => array_map('intval', $subscriberIds),
92 'yearAgo' => (new \DateTimeImmutable('-1 year'))->format('Y-m-d H:i:s'),
93 'minSentCount' => SubscriberStatisticsRepository::MIN_SENT_EMAILS_FOR_ENGAGEMENT_SCORE,
94 'now' => (new \DateTimeImmutable())->format('Y-m-d H:i:s'),
95 ];
96 if ($humanOpensCondition) {
97 $parameters['userAgentType'] = UserAgentEntity::USER_AGENT_TYPE_HUMAN;
98 }
99 $this->entityManager->getConnection()->executeStatement(
100 $sql,
101 $parameters,
102 ['ids' => ArrayParameterType::INTEGER]
103 );
104 }
105
106 public function resetSubscribersScoreCalculation() {
107 $this->entityManager->createQueryBuilder()->update(SubscriberEntity::class, 's')
108 ->set('s.engagementScoreUpdatedAt', ':updatedAt')
109 ->setParameter('updatedAt', null)
110 ->getQuery()->execute();
111 }
112
113 public function recalculateSegmentScore(SegmentEntity $segment): void {
114 $segment->setAverageEngagementScoreUpdatedAt(new \DateTimeImmutable());
115 $avgScore = $this
116 ->entityManager
117 ->createQueryBuilder()
118 ->select('avg(subscriber.engagementScore)')
119 ->from(SubscriberEntity::class, 'subscriber')
120 ->join('subscriber.subscriberSegments', 'subscriberSegments')
121 ->where('subscriberSegments.segment = :segment')
122 ->andWhere('subscriber.status = :subscribed')
123 ->andWhere('subscriber.deletedAt IS NULL')
124 ->andWhere('subscriberSegments.status = :subscribed')
125 ->setParameter('segment', $segment)
126 ->setParameter('subscribed', SubscriberEntity::STATUS_SUBSCRIBED)
127 ->getQuery()
128 ->getSingleScalarResult();
129 $segment->setAverageEngagementScore($avgScore === null ? $avgScore : (float)$avgScore);
130 $this->entityManager->flush();
131 }
132
133 public function resetSegmentsScoreCalculation(): void {
134 $this->entityManager->createQueryBuilder()->update(SegmentEntity::class, 's')
135 ->set('s.averageEngagementScoreUpdatedAt', ':updatedAt')
136 ->setParameter('updatedAt', null)
137 ->getQuery()->execute();
138 }
139
140 public function getAllForSubscriber(SubscriberEntity $subscriber): QueryBuilder {
141 return $this->entityManager->createQueryBuilder()
142 ->select('opens.id id, queue.newsletterRenderedSubject, opens.createdAt, userAgent.userAgent')
143 ->from(StatisticsOpenEntity::class, 'opens')
144 ->join('opens.queue', 'queue')
145 ->leftJoin('opens.userAgent', 'userAgent')
146 ->where('opens.subscriber = :subscriber')
147 ->orderBy('queue.newsletterRenderedSubject')
148 ->setParameter('subscriber', $subscriber->getId());
149 }
150
151 /** @param int[] $ids */
152 public function deleteByNewsletterIds(array $ids): void {
153 $this->entityManager->createQueryBuilder()
154 ->delete(StatisticsOpenEntity::class, 's')
155 ->where('s.newsletter IN (:ids)')
156 ->setParameter('ids', $ids)
157 ->getQuery()
158 ->execute();
159
160 // delete was done via DQL, make sure the entities are also detached from the entity manager
161 $this->detachAll(function (StatisticsOpenEntity $entity) use ($ids) {
162 $newsletter = $entity->getNewsletter();
163 return $newsletter && in_array($newsletter->getId(), $ids, true);
164 });
165 }
166 }
167