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