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 / Newsletter / Statistics / NewsletterStatisticsRepository.php
mailpoet / lib / Newsletter / Statistics Last commit date
Export 1 week ago NewsletterStatistics.php 3 years ago NewsletterStatisticsRepository.php 2 weeks ago WooCommerceRevenue.php 3 years ago index.php 3 years ago
NewsletterStatisticsRepository.php
342 lines
1 <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
2
3 namespace MailPoet\Newsletter\Statistics;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Doctrine\Repository;
9 use MailPoet\Entities\NewsletterEntity;
10 use MailPoet\Entities\ScheduledTaskEntity;
11 use MailPoet\Entities\SendingQueueEntity;
12 use MailPoet\Entities\StatisticsBounceEntity;
13 use MailPoet\Entities\StatisticsClickEntity;
14 use MailPoet\Entities\StatisticsNewsletterEntity;
15 use MailPoet\Entities\StatisticsOpenEntity;
16 use MailPoet\Entities\StatisticsUnsubscribeEntity;
17 use MailPoet\Entities\StatisticsWooCommercePurchaseEntity;
18 use MailPoet\Entities\SubscriberEntity;
19 use MailPoet\Entities\UserAgentEntity;
20 use MailPoet\Settings\TrackingConfig;
21 use MailPoet\WooCommerce\Helper as WCHelper;
22 use MailPoet\WooCommerce\OrderAttributionRevenueReader;
23 use MailPoetVendor\Doctrine\ORM\EntityManager;
24 use MailPoetVendor\Doctrine\ORM\Query\Expr\Join;
25 use MailPoetVendor\Doctrine\ORM\QueryBuilder;
26 use MailPoetVendor\Doctrine\ORM\UnexpectedResultException;
27
28 /**
29 * @extends Repository<NewsletterEntity>
30 */
31 class NewsletterStatisticsRepository extends Repository {
32
33 /** @var WCHelper */
34 private $wcHelper;
35
36 /** @var TrackingConfig */
37 private $trackingConfig;
38
39 /** @var OrderAttributionRevenueReader */
40 private $orderAttributionRevenueReader;
41
42 public function __construct(
43 EntityManager $entityManager,
44 WCHelper $wcHelper,
45 TrackingConfig $trackingConfig,
46 OrderAttributionRevenueReader $orderAttributionRevenueReader
47 ) {
48 parent::__construct($entityManager);
49 $this->wcHelper = $wcHelper;
50 $this->trackingConfig = $trackingConfig;
51 $this->orderAttributionRevenueReader = $orderAttributionRevenueReader;
52 }
53
54 protected function getEntityClassName() {
55 return NewsletterEntity::class;
56 }
57
58 public function getStatistics(NewsletterEntity $newsletter): NewsletterStatistics {
59 $stats = new NewsletterStatistics(
60 $this->getStatisticsClickCount($newsletter),
61 $this->getStatisticsOpenCount($newsletter),
62 $this->getStatisticsUnsubscribeCount($newsletter),
63 $this->getStatisticsBounceCount($newsletter),
64 $this->getTotalSentCount($newsletter),
65 $this->getWooCommerceRevenue($newsletter)
66 );
67 $stats->setMachineOpenCount($this->getStatisticsMachineOpenCount($newsletter));
68 return $stats;
69 }
70
71 /**
72 * @param NewsletterEntity[] $newsletters
73 * @return NewsletterStatistics[]
74 */
75 public function getBatchStatistics(
76 array $newsletters,
77 ?\DateTimeImmutable $from = null,
78 ?\DateTimeImmutable $to = null,
79 array $include = [
80 'totals',
81 StatisticsClickEntity::class,
82 StatisticsOpenEntity::class,
83 StatisticsUnsubscribeEntity::class,
84 StatisticsBounceEntity::class,
85 WooCommerceRevenue::class,
86 ]
87 ): array {
88
89 $totalSentCounts = in_array('totals', $include, true) ? $this->getTotalSentCounts($newsletters, $from, $to) : [];
90 $clickCounts = in_array(StatisticsClickEntity::class, $include, true) ? $this->getStatisticCounts(StatisticsClickEntity::class, $newsletters, $from, $to) : [];
91 $openCounts = in_array(StatisticsOpenEntity::class, $include, true) ? $this->getStatisticCounts(StatisticsOpenEntity::class, $newsletters, $from, $to) : [];
92 $unsubscribeCounts = in_array(StatisticsUnsubscribeEntity::class, $include, true) ? $this->getStatisticCounts(StatisticsUnsubscribeEntity::class, $newsletters, $from, $to) : [];
93 $bounceCounts = in_array(StatisticsBounceEntity::class, $include, true) ? $this->getStatisticCounts(StatisticsBounceEntity::class, $newsletters, $from, $to) : [];
94 $wooCommerceRevenues = in_array(WooCommerceRevenue::class, $include, true) ? $this->getWooCommerceRevenues($newsletters, $from, $to) : [];
95
96 $statistics = [];
97 foreach ($newsletters as $newsletter) {
98 $id = $newsletter->getId();
99 $statistics[$id] = new NewsletterStatistics(
100 $clickCounts[$id] ?? 0,
101 $openCounts[$id] ?? 0,
102 $unsubscribeCounts[$id] ?? 0,
103 $bounceCounts[$id] ?? 0,
104 $totalSentCounts[$id] ?? 0,
105 $wooCommerceRevenues[$id] ?? null
106 );
107 }
108 return $statistics;
109 }
110
111 public function getTotalSentCount(NewsletterEntity $newsletter): int {
112 $counts = $this->getTotalSentCounts([$newsletter]);
113 return $counts[$newsletter->getId()] ?? 0;
114 }
115
116 public function getStatisticsClickCount(NewsletterEntity $newsletter): int {
117 $counts = $this->getStatisticCounts(StatisticsClickEntity::class, [$newsletter]);
118 return $counts[$newsletter->getId()] ?? 0;
119 }
120
121 public function getStatisticsOpenCount(NewsletterEntity $newsletter): int {
122 $counts = $this->getStatisticCounts(StatisticsOpenEntity::class, [$newsletter]);
123 return $counts[$newsletter->getId()] ?? 0;
124 }
125
126 public function getStatisticsMachineOpenCount(NewsletterEntity $newsletter): int {
127 $qb = $this->getStatisticsQuery(StatisticsOpenEntity::class, [$newsletter]);
128 $result = $qb->andWhere('(stats.userAgentType = :userAgentType)')
129 ->setParameter('userAgentType', UserAgentEntity::USER_AGENT_TYPE_MACHINE)
130 ->getQuery()
131 ->getOneOrNullResult();
132
133 if (empty($result)) return 0;
134 return $result['cnt'] ?? 0;
135 }
136
137 /**
138 * @param SubscriberEntity $subscriber
139 * @param int|null $limit
140 * @param int|null $offset
141 * @return list<array{newsletter_id: mixed, newsletter_rendered_subject: string|null, opened_at: \DateTimeInterface|null, sent_at: \DateTimeInterface}>
142 */
143 public function getAllForSubscriber(
144 SubscriberEntity $subscriber,
145 ?int $limit = null,
146 ?int $offset = null
147 ): array {
148 return $this->entityManager->createQueryBuilder()
149 ->select('IDENTITY(statistics.newsletter) AS newsletter_id')
150 ->addSelect('opens.createdAt AS opened_at')
151 ->addSelect('queue.newsletterRenderedSubject AS newsletter_rendered_subject')
152 ->addSelect('statistics.sentAt AS sent_at')
153 ->from(StatisticsNewsletterEntity::class, 'statistics')
154 ->join(SendingQueueEntity::class, 'queue', Join::WITH, 'statistics.queue = queue')
155 ->leftJoin(
156 StatisticsOpenEntity::class,
157 'opens',
158 Join::WITH,
159 'statistics.newsletter = opens.newsletter AND statistics.subscriber = opens.subscriber'
160 )
161 ->where('statistics.subscriber = :subscriber')
162 ->setParameter('subscriber', $subscriber)
163 ->addOrderBy('newsletter_id')
164 ->setMaxResults($limit)
165 ->setFirstResult($offset)
166 ->getQuery()
167 ->getResult();
168 }
169
170 public function getStatisticsUnsubscribeCount(NewsletterEntity $newsletter): int {
171 $counts = $this->getStatisticCounts(StatisticsUnsubscribeEntity::class, [$newsletter]);
172 return $counts[$newsletter->getId()] ?? 0;
173 }
174
175 public function getStatisticsBounceCount(NewsletterEntity $newsletter): int {
176 $counts = $this->getStatisticCounts(StatisticsBounceEntity::class, [$newsletter]);
177 return $counts[$newsletter->getId()] ?? 0;
178 }
179
180 public function getWooCommerceRevenue(NewsletterEntity $newsletter) {
181 $revenues = $this->getWooCommerceRevenues([$newsletter]);
182 return $revenues[$newsletter->getId()] ?? null;
183 }
184
185 /**
186 * @param NewsletterEntity $newsletter
187 * @return int
188 */
189 public function getChildrenCount(NewsletterEntity $newsletter) {
190 try {
191 return (int)$this->entityManager
192 ->createQueryBuilder()
193 ->select('COUNT(n.id) as cnt')
194 ->from(NewsletterEntity::class, 'n')
195 ->where('n.parent = :newsletter')
196 ->setParameter('newsletter', $newsletter)
197 ->getQuery()
198 ->getSingleScalarResult();
199 } catch (UnexpectedResultException $e) {
200 return 0;
201 }
202 }
203
204 private function getTotalSentCounts(array $newsletters, ?\DateTimeImmutable $from = null, ?\DateTimeImmutable $to = null): array {
205 $query = $this->doctrineRepository
206 ->createQueryBuilder('n')
207 ->select('n.id, SUM(q.countProcessed) AS cnt')
208 ->join('n.queues', 'q')
209 ->join('q.task', 't')
210 ->where('t.status = :status')
211 ->setParameter('status', ScheduledTaskEntity::STATUS_COMPLETED)
212 ->andWhere('q.newsletter IN (:newsletters)')
213 ->setParameter('newsletters', $newsletters)
214 ->groupBy('n.id');
215
216 if ($from && $to) {
217 $query->andWhere('q.createdAt BETWEEN :from AND :to')
218 ->setParameter('from', $from)
219 ->setParameter('to', $to);
220 } elseif ($from && $to === null) {
221 $query->andWhere('q.createdAt >= :from')
222 ->setParameter('from', $from);
223 } elseif ($from === null && $to) {
224 $query->andWhere('q.createdAt <= :to')
225 ->setParameter('to', $to);
226 }
227
228 $results = $query->getQuery()
229 ->getResult();
230
231 $counts = [];
232 foreach ($results ?: [] as $result) {
233 $counts[(int)$result['id']] = (int)$result['cnt'];
234 }
235 return $counts;
236 }
237
238 private function getStatisticCounts(string $statisticsEntityName, array $newsletters, ?\DateTimeImmutable $from = null, ?\DateTimeImmutable $to = null): array {
239 $qb = $this->getStatisticsQuery($statisticsEntityName, $newsletters);
240 if (
241 $statisticsEntityName === StatisticsClickEntity::class
242 || ($statisticsEntityName === StatisticsOpenEntity::class && $this->trackingConfig->areOpensSeparated())
243 ) {
244 $qb->andWhere('(stats.userAgentType = :userAgentType) OR (stats.userAgentType IS NULL)')
245 ->setParameter('userAgentType', UserAgentEntity::USER_AGENT_TYPE_HUMAN);
246 }
247 if ($from && $to) {
248 $qb->andWhere('stats.createdAt BETWEEN :from AND :to')
249 ->setParameter('from', $from)
250 ->setParameter('to', $to);
251 } elseif ($from && $to === null) {
252 $qb->andWhere('stats.createdAt >= :from')
253 ->setParameter('from', $from);
254 } elseif ($from === null && $to) {
255 $qb->andWhere('stats.createdAt <= :to')
256 ->setParameter('to', $to);
257 }
258
259 $results = $qb
260 ->getQuery()
261 ->getResult();
262
263 $counts = [];
264 foreach ($results ?: [] as $result) {
265 $counts[(int)$result['id']] = (int)$result['cnt'];
266 }
267 return $counts;
268 }
269
270 private function getStatisticsQuery(string $statisticsEntityName, array $newsletters): QueryBuilder {
271 return $this->entityManager->createQueryBuilder()
272 ->select('IDENTITY(stats.newsletter) AS id, COUNT(DISTINCT stats.subscriber) as cnt')
273 ->from($statisticsEntityName, 'stats')
274 ->where('stats.newsletter IN (:newsletters)')
275 ->groupBy('stats.newsletter')
276 ->setParameter('newsletters', $newsletters);
277 }
278
279 private function getWooCommerceRevenues(array $newsletters, ?\DateTimeImmutable $from = null, ?\DateTimeImmutable $to = null) {
280 if (!$this->wcHelper->isWooCommerceActive()) {
281 return null;
282 }
283
284 $newsletterIds = array_map(function(NewsletterEntity $newsletter): int {
285 return (int)$newsletter->getId();
286 }, $newsletters);
287 $revenueStatus = $this->wcHelper->getPurchaseStates();
288 $currency = $this->wcHelper->getWoocommerceCurrency();
289 $wooBackedRevenues = $this->orderAttributionRevenueReader->getNewsletterRevenues($newsletterIds, $from, $to);
290 if (is_array($wooBackedRevenues)) {
291 $revenues = [];
292 foreach ($wooBackedRevenues as $newsletterId => $result) {
293 $revenues[(int)$newsletterId] = new WooCommerceRevenue(
294 $currency,
295 (float)$result['total'],
296 (int)$result['count'],
297 $this->wcHelper
298 );
299 }
300 return $revenues;
301 }
302
303 $query = $this->entityManager
304 ->createQueryBuilder()
305 ->select('IDENTITY(stats.newsletter) AS id, SUM(stats.orderPriceTotal) AS total, COUNT(stats.id) AS cnt')
306 ->from(StatisticsWooCommercePurchaseEntity::class, 'stats')
307 ->where('stats.newsletter IN (:newsletters)')
308 ->andWhere('stats.orderCurrency = :currency')
309 ->andWhere('stats.status IN (:revenue_status)')
310 ->setParameter('newsletters', $newsletters)
311 ->setParameter('currency', $currency)
312 ->setParameter('revenue_status', $revenueStatus)
313 ->groupBy('stats.newsletter');
314
315 if ($from && $to) {
316 $query->andWhere('stats.createdAt BETWEEN :from AND :to')
317 ->setParameter('from', $from)
318 ->setParameter('to', $to);
319 } elseif ($from && $to === null) {
320 $query->andWhere('stats.createdAt >= :from')
321 ->setParameter('from', $from);
322 } elseif ($from === null && $to) {
323 $query->andWhere('stats.createdAt <= :to')
324 ->setParameter('to', $to);
325 }
326
327 $results = $query->getQuery()
328 ->getResult();
329
330 $revenues = [];
331 foreach ($results ?: [] as $result) {
332 $revenues[(int)$result['id']] = new WooCommerceRevenue(
333 $currency,
334 (float)$result['total'],
335 (int)$result['cnt'],
336 $this->wcHelper
337 );
338 }
339 return $revenues;
340 }
341 }
342