PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 4.57.0
MailPoet – Newsletters, Email Marketing, and Automation v4.57.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 / Segments / SegmentSubscribersRepository.php
mailpoet / lib / Segments Last commit date
DynamicSegments 1 year ago SegmentDependencyValidator.php 3 years ago SegmentListingRepository.php 1 year ago SegmentSaveController.php 3 years ago SegmentSubscribersRepository.php 1 year ago SegmentsFinder.php 3 years ago SegmentsRepository.php 2 years ago SegmentsSimpleListRepository.php 1 year ago SubscribersFinder.php 2 years ago WP.php 2 years ago WooCommerce.php 1 year ago index.php 3 years ago
SegmentSubscribersRepository.php
497 lines
1 <?php declare(strict_types = 1);
2
3 namespace MailPoet\Segments;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Entities\DynamicSegmentFilterData;
9 use MailPoet\Entities\DynamicSegmentFilterEntity;
10 use MailPoet\Entities\SegmentEntity;
11 use MailPoet\Entities\SubscriberEntity;
12 use MailPoet\Entities\SubscriberSegmentEntity;
13 use MailPoet\InvalidStateException;
14 use MailPoet\NotFoundException;
15 use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
16 use MailPoet\Segments\DynamicSegments\FilterHandler;
17 use MailPoetVendor\Doctrine\DBAL\Connection;
18 use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
19 use MailPoetVendor\Doctrine\DBAL\Result;
20 use MailPoetVendor\Doctrine\ORM\EntityManager;
21 use MailPoetVendor\Doctrine\ORM\Query\Expr\Join;
22 use MailPoetVendor\Doctrine\ORM\QueryBuilder as ORMQueryBuilder;
23
24 class SegmentSubscribersRepository {
25 /** @var EntityManager */
26 private $entityManager;
27
28 /** @var FilterHandler */
29 private $filterHandler;
30
31 /** @var SegmentsRepository */
32 private $segmentsRepository;
33
34 public function __construct(
35 EntityManager $entityManager,
36 FilterHandler $filterHandler,
37 SegmentsRepository $segmentsRepository
38 ) {
39 $this->entityManager = $entityManager;
40 $this->filterHandler = $filterHandler;
41 $this->segmentsRepository = $segmentsRepository;
42 }
43
44 public function findSubscribersIdsInSegment(int $segmentId, array $candidateIds = null): array {
45 return $this->loadSubscriberIdsInSegment($segmentId, $candidateIds);
46 }
47
48 public function getSubscriberIdsInSegment(int $segmentId): array {
49 return $this->loadSubscriberIdsInSegment($segmentId);
50 }
51
52 public function getSubscribersCount(int $segmentId, string $status = null): int {
53 $segment = $this->getSegment($segmentId);
54 $result = $this->getSubscribersStatisticsCount($segment);
55 return (int)$result[$status ?: 'all'];
56 }
57
58 public function getSubscribersCountBySegmentIds(array $segmentIds, string $status = null, ?int $filterSegmentId = null): int {
59 $segmentRepository = $this->entityManager->getRepository(SegmentEntity::class);
60 $segments = $segmentRepository->findBy(['id' => $segmentIds]);
61 $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
62 $queryBuilder = $this->createCountQueryBuilder();
63
64 $subQueries = [];
65 foreach ($segments as $segment) {
66 $segmentQb = $this->createCountQueryBuilder();
67 $segmentQb->select("{$subscribersTable}.id AS inner_id");
68
69 if ($segment->isStatic()) {
70 $segmentQb = $this->filterSubscribersInStaticSegment($segmentQb, $segment, $status);
71 } else {
72 $segmentQb = $this->filterSubscribersInDynamicSegment($segmentQb, $segment, $status);
73 }
74
75 // inner parameters and types have to be merged to outer queryBuilder
76 $queryBuilder->setParameters(array_merge(
77 $segmentQb->getParameters(),
78 $queryBuilder->getParameters()
79 ), array_merge(
80 $segmentQb->getParameterTypes(),
81 $queryBuilder->getParameterTypes()
82 ));
83 $subQueries[] = $segmentQb->getSQL();
84 }
85
86 $queryBuilder->innerJoin(
87 $subscribersTable,
88 sprintf('(%s)', join(' UNION ', $subQueries)),
89 'inner_subscribers',
90 "inner_subscribers.inner_id = {$subscribersTable}.id"
91 );
92
93 try {
94 if (is_int($filterSegmentId)) {
95 $filterSegment = $this->segmentsRepository->verifyDynamicSegmentExists($filterSegmentId);
96 $filterSegmentQb = $this->createCountQueryBuilder();
97 $filterSegmentQb->select("{$subscribersTable}.id AS filter_segment_subscriber_id");
98 $filterSegmentQb = $this->filterSubscribersInDynamicSegment($filterSegmentQb, $filterSegment, $status);
99 $queryBuilder->setParameters(array_merge($filterSegmentQb->getParameters(), $queryBuilder->getParameters()), array_merge($filterSegmentQb->getParameterTypes(), $queryBuilder->getParameterTypes()));
100 $queryBuilder->innerJoin(
101 $subscribersTable,
102 sprintf('(%s)', $filterSegmentQb->getSQL()),
103 'filter_segment',
104 "filter_segment.filter_segment_subscriber_id = {$subscribersTable}.id"
105 );
106 }
107 } catch (InvalidStateException $exception) {
108 return 0;
109 }
110
111 $statement = $this->executeQuery($queryBuilder);
112 /** @var string $result */
113 $result = $statement->fetchOne();
114 return (int)$result;
115 }
116
117 /**
118 * @param DynamicSegmentFilterData[] $filters
119 * @return int
120 * @throws InvalidStateException
121 */
122 public function getDynamicSubscribersCount(array $filters): int {
123 $segment = new SegmentEntity('temporary segment', SegmentEntity::TYPE_DYNAMIC, '');
124 foreach ($filters as $filter) {
125 $segment->addDynamicFilter(new DynamicSegmentFilterEntity($segment, $filter));
126 }
127 $queryBuilder = $this->createDynamicStatisticsQueryBuilder();
128 $queryBuilder = $this->filterSubscribersInDynamicSegment($queryBuilder, $segment, null);
129 $statement = $this->executeQuery($queryBuilder);
130 /** @var array{all:string} $result */
131 $result = $statement->fetch();
132 return (int)$result['all'];
133 }
134
135 private function createCountQueryBuilder(): QueryBuilder {
136 $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
137 return $this->entityManager
138 ->getConnection()
139 ->createQueryBuilder()
140 ->select("count(DISTINCT $subscribersTable.id)")
141 ->from($subscribersTable);
142 }
143
144 private function createDynamicStatisticsQueryBuilder(): QueryBuilder {
145 $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
146 return $this->entityManager
147 ->getConnection()
148 ->createQueryBuilder()
149 ->from($subscribersTable)
150 ->addSelect("IFNULL(SUM(
151 CASE WHEN $subscribersTable.deleted_at IS NULL
152 THEN 1 ELSE 0 END
153 ), 0) as `all`")
154 ->addSelect("IFNULL(SUM(
155 CASE WHEN $subscribersTable.deleted_at IS NOT NULL
156 THEN 1 ELSE 0 END
157 ), 0) as trash")
158 ->addSelect("IFNULL(SUM(
159 CASE WHEN $subscribersTable.status = :status_subscribed AND $subscribersTable.deleted_at IS NULL
160 THEN 1 ELSE 0 END
161 ), 0) as :status_subscribed")
162 ->addSelect("IFNULL(SUM(
163 CASE WHEN $subscribersTable.status = :status_unsubscribed AND $subscribersTable.deleted_at IS NULL
164 THEN 1 ELSE 0 END
165 ), 0) as :status_unsubscribed")
166 ->addSelect("IFNULL(SUM(
167 CASE WHEN $subscribersTable.status = :status_inactive AND $subscribersTable.deleted_at IS NULL
168 THEN 1 ELSE 0 END
169 ), 0) as :status_inactive")
170 ->addSelect("IFNULL(SUM(
171 CASE WHEN $subscribersTable.status = :status_unconfirmed AND $subscribersTable.deleted_at IS NULL
172 THEN 1 ELSE 0 END
173 ), 0) as :status_unconfirmed")
174 ->addSelect("IFNULL(SUM(
175 CASE WHEN $subscribersTable.status = :status_bounced AND $subscribersTable.deleted_at IS NULL
176 THEN 1 ELSE 0 END
177 ), 0) as :status_bounced")
178 ->setParameter('status_subscribed', SubscriberEntity::STATUS_SUBSCRIBED)
179 ->setParameter('status_unsubscribed', SubscriberEntity::STATUS_UNSUBSCRIBED)
180 ->setParameter('status_inactive', SubscriberEntity::STATUS_INACTIVE)
181 ->setParameter('status_unconfirmed', SubscriberEntity::STATUS_UNCONFIRMED)
182 ->setParameter('status_bounced', SubscriberEntity::STATUS_BOUNCED);
183 }
184
185 private function createStaticStatisticsQueryBuilder(SegmentEntity $segment): QueryBuilder {
186 $subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
187 $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
188 return $this->entityManager
189 ->getConnection()
190 ->createQueryBuilder()
191 ->from($subscriberSegmentTable, 'subscriber_segment')
192 ->where('subscriber_segment.segment_id = :segment_id')
193 ->setParameter('segment_id', $segment->getId())
194 ->join('subscriber_segment', $subscribersTable, 'subscribers', 'subscribers.id = subscriber_segment.subscriber_id')
195 ->addSelect('IFNULL(SUM(
196 CASE WHEN subscribers.deleted_at IS NULL
197 THEN 1 ELSE 0 END
198 ), 0) as `all`')
199 ->addSelect('IFNULL(SUM(
200 CASE WHEN subscribers.deleted_at IS NOT NULL
201 THEN 1 ELSE 0 END
202 ), 0) as trash')
203 ->addSelect('IFNULL(SUM(
204 CASE WHEN subscribers.status = :status_subscribed AND subscriber_segment.status = :status_subscribed AND subscribers.deleted_at IS NULL
205 THEN 1 ELSE 0 END
206 ), 0) as :status_subscribed')
207 ->addSelect('IFNULL(SUM(
208 CASE WHEN (subscribers.status = :status_unsubscribed OR subscriber_segment.status = :status_unsubscribed) AND subscribers.deleted_at IS NULL
209 THEN 1 ELSE 0 END
210 ), 0) as :status_unsubscribed')
211 ->addSelect('IFNULL(SUM(
212 CASE WHEN subscribers.status = :status_inactive AND subscriber_segment.status != :status_unsubscribed AND subscribers.deleted_at IS NULL
213 THEN 1 ELSE 0 END
214 ), 0) as :status_inactive')
215 ->addSelect('IFNULL(SUM(
216 CASE WHEN subscribers.status = :status_unconfirmed AND subscriber_segment.status != :status_unsubscribed AND subscribers.deleted_at IS NULL
217 THEN 1 ELSE 0 END
218 ), 0) as :status_unconfirmed')
219 ->addSelect('IFNULL(SUM(
220 CASE WHEN subscribers.status = :status_bounced AND subscriber_segment.status != :status_unsubscribed AND subscribers.deleted_at IS NULL
221 THEN 1 ELSE 0 END
222 ), 0) as :status_bounced')
223 ->setParameter('status_subscribed', SubscriberEntity::STATUS_SUBSCRIBED)
224 ->setParameter('status_unsubscribed', SubscriberEntity::STATUS_UNSUBSCRIBED)
225 ->setParameter('status_inactive', SubscriberEntity::STATUS_INACTIVE)
226 ->setParameter('status_unconfirmed', SubscriberEntity::STATUS_UNCONFIRMED)
227 ->setParameter('status_bounced', SubscriberEntity::STATUS_BOUNCED);
228 }
229
230 private function createStaticGlobalStatusStatisticsQueryBuilder(SegmentEntity $segment): QueryBuilder {
231 $subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
232 $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
233 return $this->entityManager
234 ->getConnection()
235 ->createQueryBuilder()
236 ->from($subscriberSegmentTable, 'subscriber_segment')
237 ->where('subscriber_segment.segment_id = :segment_id')
238 ->setParameter('segment_id', $segment->getId())
239 ->join('subscriber_segment', $subscribersTable, 'subscribers', 'subscribers.id = subscriber_segment.subscriber_id')
240 ->addSelect('IFNULL(SUM(
241 CASE WHEN subscribers.deleted_at IS NULL
242 THEN 1 ELSE 0 END
243 ), 0) as `all`')
244 ->addSelect('IFNULL(SUM(
245 CASE WHEN subscribers.deleted_at IS NOT NULL
246 THEN 1 ELSE 0 END
247 ), 0) as trash')
248 ->addSelect('IFNULL(SUM(
249 CASE WHEN subscribers.status = :status_subscribed AND subscribers.deleted_at IS NULL
250 THEN 1 ELSE 0 END
251 ), 0) as :status_subscribed')
252 ->addSelect('IFNULL(SUM(
253 CASE WHEN subscribers.status = :status_unsubscribed AND subscribers.deleted_at IS NULL
254 THEN 1 ELSE 0 END
255 ), 0) as :status_unsubscribed')
256 ->addSelect('IFNULL(SUM(
257 CASE WHEN subscribers.status = :status_inactive AND subscribers.deleted_at IS NULL
258 THEN 1 ELSE 0 END
259 ), 0) as :status_inactive')
260 ->addSelect('IFNULL(SUM(
261 CASE WHEN subscribers.status = :status_unconfirmed AND subscribers.deleted_at IS NULL
262 THEN 1 ELSE 0 END
263 ), 0) as :status_unconfirmed')
264 ->addSelect('IFNULL(SUM(
265 CASE WHEN subscribers.status = :status_bounced AND subscribers.deleted_at IS NULL
266 THEN 1 ELSE 0 END
267 ), 0) as :status_bounced')
268 ->setParameter('status_subscribed', SubscriberEntity::STATUS_SUBSCRIBED)
269 ->setParameter('status_unsubscribed', SubscriberEntity::STATUS_UNSUBSCRIBED)
270 ->setParameter('status_inactive', SubscriberEntity::STATUS_INACTIVE)
271 ->setParameter('status_unconfirmed', SubscriberEntity::STATUS_UNCONFIRMED)
272 ->setParameter('status_bounced', SubscriberEntity::STATUS_BOUNCED);
273 }
274
275 public function getSubscribersWithoutSegmentCount(): int {
276 $queryBuilder = $this->entityManager->createQueryBuilder();
277 $queryBuilder
278 ->select('COUNT(DISTINCT s) AS subscribersCount')
279 ->from(SubscriberEntity::class, 's');
280 $this->addConstraintsForSubscribersWithoutSegment($queryBuilder);
281 return (int)$queryBuilder->getQuery()->getSingleScalarResult();
282 }
283
284 public function getSubscribersWithoutSegmentStatisticsCount(): array {
285 $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
286 $queryBuilder = $this->entityManager
287 ->getConnection()
288 ->createQueryBuilder();
289 $queryBuilder
290 ->addSelect('IFNULL(SUM(
291 CASE WHEN s.deleted_at IS NULL
292 THEN 1 ELSE 0 END
293 ), 0) as `all`')
294 ->addSelect('IFNULL(SUM(
295 CASE WHEN s.deleted_at IS NOT NULL
296 THEN 1 ELSE 0 END
297 ), 0) as trash')
298 ->addSelect('IFNULL(SUM(
299 CASE WHEN s.status = :status_subscribed AND s.deleted_at IS NULL
300 THEN 1 ELSE 0 END
301 ), 0) as :status_subscribed')
302 ->addSelect('IFNULL(SUM(
303 CASE WHEN s.status = :status_unsubscribed AND s.deleted_at IS NULL
304 THEN 1 ELSE 0 END
305 ), 0) as :status_unsubscribed')
306 ->addSelect('IFNULL(SUM(
307 CASE WHEN s.status = :status_inactive AND s.deleted_at IS NULL
308 THEN 1 ELSE 0 END
309 ), 0) as :status_inactive')
310 ->addSelect('IFNULL(SUM(
311 CASE WHEN s.status = :status_unconfirmed AND s.deleted_at IS NULL
312 THEN 1 ELSE 0 END
313 ), 0) as :status_unconfirmed')
314 ->addSelect('IFNULL(SUM(
315 CASE WHEN s.status = :status_bounced AND s.deleted_at IS NULL
316 THEN 1 ELSE 0 END
317 ), 0) as :status_bounced')
318 ->from($subscribersTable, 's')
319 ->setParameter('status_subscribed', SubscriberEntity::STATUS_SUBSCRIBED)
320 ->setParameter('status_unsubscribed', SubscriberEntity::STATUS_UNSUBSCRIBED)
321 ->setParameter('status_inactive', SubscriberEntity::STATUS_INACTIVE)
322 ->setParameter('status_unconfirmed', SubscriberEntity::STATUS_UNCONFIRMED)
323 ->setParameter('status_bounced', SubscriberEntity::STATUS_BOUNCED);
324
325 $this->addConstraintsForSubscribersWithoutSegmentToDBAL($queryBuilder);
326 $statement = $this->executeQuery($queryBuilder);
327 $result = $statement->fetch();
328
329 return $result;
330 }
331
332 public function addConstraintsForSubscribersWithoutSegment(ORMQueryBuilder $queryBuilder): void {
333 $deletedSegmentsQueryBuilder = $this->entityManager->createQueryBuilder();
334 $deletedSegmentsQueryBuilder->select('sg.id')
335 ->from(SegmentEntity::class, 'sg')
336 ->where($deletedSegmentsQueryBuilder->expr()->isNotNull('sg.deletedAt'));
337
338 $queryBuilder
339 ->leftJoin(
340 's.subscriberSegments',
341 'ssg',
342 Join::WITH,
343 (string)$queryBuilder->expr()->andX(
344 $queryBuilder->expr()->eq('ssg.subscriber', 's.id'),
345 $queryBuilder->expr()->eq('ssg.status', ':statusSubscribed'),
346 $queryBuilder->expr()->notIn('ssg.segment', $deletedSegmentsQueryBuilder->getDQL())
347 )
348 )
349 ->andWhere('ssg.id IS NULL')
350 ->setParameter('statusSubscribed', SubscriberEntity::STATUS_SUBSCRIBED);
351 }
352
353 public function addConstraintsForSubscribersWithoutSegmentToDBAL(QueryBuilder $queryBuilder): void {
354 $deletedSegmentsQueryBuilder = $this->entityManager->createQueryBuilder();
355 $subscribersSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
356 $deletedSegmentsQueryBuilder->select('sg.id')
357 ->from(SegmentEntity::class, 'sg')
358 ->where($deletedSegmentsQueryBuilder->expr()->isNotNull('sg.deletedAt'));
359
360 $queryBuilder
361 ->leftJoin(
362 's',
363 $subscribersSegmentTable,
364 'ssg',
365 (string)$queryBuilder->expr()->and(
366 $queryBuilder->expr()->eq('ssg.subscriber_id', 's.id'),
367 $queryBuilder->expr()->eq('ssg.status', ':statusSubscribed'),
368 $queryBuilder->expr()->notIn('ssg.segment_id', $deletedSegmentsQueryBuilder->getQuery()->getSQL())
369 )
370 )
371 ->andWhere('ssg.id IS NULL')
372 ->setParameter('statusSubscribed', SubscriberEntity::STATUS_SUBSCRIBED);
373 }
374
375 private function loadSubscriberIdsInSegment(int $segmentId, array $candidateIds = null): array {
376 $segment = $this->getSegment($segmentId);
377 $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
378 $queryBuilder = $this->entityManager
379 ->getConnection()
380 ->createQueryBuilder()
381 ->select("DISTINCT $subscribersTable.id")
382 ->from($subscribersTable);
383
384 if ($segment->isStatic()) {
385 $queryBuilder = $this->filterSubscribersInStaticSegment($queryBuilder, $segment, SubscriberEntity::STATUS_SUBSCRIBED);
386 } else {
387 $queryBuilder = $this->filterSubscribersInDynamicSegment($queryBuilder, $segment, SubscriberEntity::STATUS_SUBSCRIBED);
388 }
389
390 if ($candidateIds) {
391 $queryBuilder->andWhere("$subscribersTable.id IN (:candidateIds)")
392 ->setParameter('candidateIds', $candidateIds, Connection::PARAM_STR_ARRAY);
393 }
394
395 $statement = $this->executeQuery($queryBuilder);
396 $result = $statement->fetchAll();
397 return array_column($result, 'id');
398 }
399
400 private function filterSubscribersInStaticSegment(
401 QueryBuilder $queryBuilder,
402 SegmentEntity $segment,
403 string $status = null
404 ): QueryBuilder {
405 $subscribersSegmentsTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
406 $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
407 $parameterName = "segment_{$segment->getId()}"; // When we use this method more times the parameter name has to be unique
408 $queryBuilder = $queryBuilder->join(
409 $subscribersTable,
410 $subscribersSegmentsTable,
411 'subsegment',
412 "subsegment.subscriber_id = $subscribersTable.id AND subsegment.segment_id = :$parameterName"
413 )->andWhere("$subscribersTable.deleted_at IS NULL")
414 ->setParameter($parameterName, $segment->getId());
415 if ($status) {
416 $queryBuilder = $queryBuilder->andWhere("$subscribersTable.status = :status")
417 ->andWhere("subsegment.status = :status")
418 ->setParameter('status', $status);
419 }
420 return $queryBuilder;
421 }
422
423 private function filterSubscribersInDynamicSegment(
424 QueryBuilder $queryBuilder,
425 SegmentEntity $segment,
426 string $status = null
427 ): QueryBuilder {
428 $filters = [];
429 $dynamicFilters = $segment->getDynamicFilters();
430 foreach ($dynamicFilters as $dynamicFilter) {
431 $filters[] = $dynamicFilter->getFilterData();
432 }
433
434 // We don't allow dynamic segment without filers since it would return all subscribers
435 // For BC compatibility fetching an empty result
436 if (count($filters) === 0) {
437 return $queryBuilder->andWhere('0 = 1');
438 } elseif ($segment instanceof SegmentEntity) {
439 try {
440 $queryBuilder = $this->filterHandler->apply($queryBuilder, $segment);
441 } catch (InvalidFilterException $e) {
442 // If a segment has an invalid filter, we should simply consider it empty instead of throwing
443 // an unhandled error. Unhandled errors here can break many admin pages.
444 $queryBuilder->andWhere('0 = 1');
445 }
446 }
447 $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
448 $queryBuilder = $queryBuilder->andWhere("$subscribersTable.deleted_at IS NULL");
449 if ($status) {
450 $queryBuilder = $queryBuilder->andWhere("$subscribersTable.status = :status")
451 ->setParameter('status', $status);
452 }
453 return $queryBuilder;
454 }
455
456 private function getSegment(int $id): SegmentEntity {
457 $segment = $this->entityManager->find(SegmentEntity::class, $id);
458 if (!$segment instanceof SegmentEntity) {
459 throw new NotFoundException('Segment not found');
460 }
461 return $segment;
462 }
463
464 private function executeQuery(QueryBuilder $queryBuilder): Result {
465 $result = $queryBuilder->execute();
466 // Execute for select always returns statement but PHP Stan doesn't know that :(
467 if (!$result instanceof Result) {
468 throw new InvalidStateException('Invalid query.');
469 }
470 return $result;
471 }
472
473 public function getSubscribersGlobalStatusStatisticsCount(SegmentEntity $segment): array {
474 if ($segment->isStatic()) {
475 $queryBuilder = $this->createStaticGlobalStatusStatisticsQueryBuilder($segment);
476 } else {
477 $queryBuilder = $this->createDynamicStatisticsQueryBuilder();
478 $this->filterSubscribersInDynamicSegment($queryBuilder, $segment);
479 }
480
481 $statement = $this->executeQuery($queryBuilder);
482 return $statement->fetch();
483 }
484
485 public function getSubscribersStatisticsCount(SegmentEntity $segment): array {
486 if ($segment->isStatic()) {
487 $queryBuilder = $this->createStaticStatisticsQueryBuilder($segment);
488 } else {
489 $queryBuilder = $this->createDynamicStatisticsQueryBuilder();
490 $this->filterSubscribersInDynamicSegment($queryBuilder, $segment);
491 }
492
493 $statement = $this->executeQuery($queryBuilder);
494 return $statement->fetch();
495 }
496 }
497