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 / Segments / SegmentsRepository.php
mailpoet / lib / Segments Last commit date
DynamicSegments 1 month ago RestApi 1 month ago SegmentDependencyValidator.php 3 years ago SegmentListingRepository.php 1 month ago SegmentSaveController.php 2 weeks ago SegmentSubscribersRepository.php 2 weeks ago SegmentsFinder.php 3 years ago SegmentsRepository.php 2 weeks ago SegmentsSimpleListRepository.php 2 months ago SubscribersFinder.php 1 month ago WP.php 2 weeks ago WooCommerce.php 2 weeks ago index.php 3 years ago
SegmentsRepository.php
461 lines
1 <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
2
3 namespace MailPoet\Segments;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use DateTime;
9 use MailPoet\ConflictException;
10 use MailPoet\Doctrine\Repository;
11 use MailPoet\Entities\DynamicSegmentFilterData;
12 use MailPoet\Entities\DynamicSegmentFilterEntity;
13 use MailPoet\Entities\NewsletterSegmentEntity;
14 use MailPoet\Entities\SegmentEntity;
15 use MailPoet\Entities\SubscriberEntity;
16 use MailPoet\Entities\SubscriberSegmentEntity;
17 use MailPoet\Form\FormsRepository;
18 use MailPoet\InvalidStateException;
19 use MailPoet\Logging\LoggerFactory;
20 use MailPoet\Newsletter\Segment\NewsletterSegmentRepository;
21 use MailPoet\NotFoundException;
22 use MailPoet\Subscribers\SegmentsCountRecalculator;
23 use MailPoet\WP\Functions as WPFunctions;
24 use MailPoetVendor\Carbon\Carbon;
25 use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
26 use MailPoetVendor\Doctrine\DBAL\ParameterType;
27 use MailPoetVendor\Doctrine\ORM\EntityManager;
28 use MailPoetVendor\Doctrine\ORM\ORMException;
29
30 /**
31 * @extends Repository<SegmentEntity>
32 */
33 class SegmentsRepository extends Repository {
34
35 /** @var NewsletterSegmentRepository */
36 private $newsletterSegmentRepository;
37
38 /** @var FormsRepository */
39 private $formsRepository;
40
41 /** @var WPFunctions */
42 private $wp;
43
44 /** @var LoggerFactory */
45 private $loggerFactory;
46
47 /** @var SegmentsCountRecalculator */
48 private $segmentsCountRecalculator;
49
50 public function __construct(
51 EntityManager $entityManager,
52 NewsletterSegmentRepository $newsletterSegmentRepository,
53 FormsRepository $formsRepository,
54 WPFunctions $wp,
55 LoggerFactory $loggerFactory,
56 SegmentsCountRecalculator $segmentsCountRecalculator
57 ) {
58 parent::__construct($entityManager);
59 $this->newsletterSegmentRepository = $newsletterSegmentRepository;
60 $this->formsRepository = $formsRepository;
61 $this->wp = $wp;
62 $this->loggerFactory = $loggerFactory;
63 $this->segmentsCountRecalculator = $segmentsCountRecalculator;
64 }
65
66 protected function getEntityClassName() {
67 return SegmentEntity::class;
68 }
69
70 /**
71 * @param string[] $types
72 * @return SegmentEntity[]
73 */
74 public function findByTypeNotIn(array $types): array {
75 return $this->doctrineRepository->createQueryBuilder('s')
76 ->select('s')
77 ->where('s.type NOT IN (:types)')
78 ->setParameter('types', $types)
79 ->getQuery()
80 ->getResult();
81 }
82
83 public function getWPUsersSegment(): SegmentEntity {
84 $cached = current(
85 array_filter(
86 $this->getAllFromIdentityMap(),
87 fn(SegmentEntity $segment) => $segment->getType() === SegmentEntity::TYPE_WP_USERS
88 )
89 );
90
91 if ($cached) {
92 return $cached;
93 }
94
95 $segment = $this->findOneBy(['type' => SegmentEntity::TYPE_WP_USERS]);
96 if (!$segment) {
97 // create the wp users segment
98 $segment = new SegmentEntity(
99 __('WordPress Users', 'mailpoet'),
100 SegmentEntity::TYPE_WP_USERS,
101 __('This list contains all of your WordPress users.', 'mailpoet')
102 );
103 $this->entityManager->persist($segment);
104 $this->entityManager->flush();
105 }
106 return $segment;
107 }
108
109 public function getWooCommerceSegment(): SegmentEntity {
110 $cached = current(
111 array_filter(
112 $this->getAllFromIdentityMap(),
113 fn(SegmentEntity $segment) => $segment->getType() === SegmentEntity::TYPE_WC_USERS
114 )
115 );
116
117 if ($cached) {
118 return $cached;
119 }
120
121 $segment = $this->findOneBy(['type' => SegmentEntity::TYPE_WC_USERS]);
122 if (!$segment) {
123 // create the WooCommerce customers segment
124 $segment = new SegmentEntity(
125 __('WooCommerce Customers', 'mailpoet'),
126 SegmentEntity::TYPE_WC_USERS,
127 __('This list contains all of your WooCommerce customers.', 'mailpoet')
128 );
129 $this->entityManager->persist($segment);
130 $this->entityManager->flush();
131 }
132 return $segment;
133 }
134
135 public function getCountsPerType(): array {
136 $results = $this->doctrineRepository->createQueryBuilder('s')
137 ->select('s.type, COUNT(s) as cnt')
138 ->where('s.deletedAt IS NULL')
139 ->groupBy('s.type')
140 ->getQuery()
141 ->getResult();
142
143 $countMap = [];
144 foreach ($results as $result) {
145 $countMap[$result['type']] = (int)$result['cnt'];
146 }
147 return $countMap;
148 }
149
150 public function isNameUnique(string $name, ?int $id): bool {
151 $qb = $this->doctrineRepository->createQueryBuilder('s')
152 ->select('s')
153 ->where('s.name = :name')
154 ->setParameter('name', $name);
155
156 if ($id !== null) {
157 $qb->andWhere('s.id != :id')
158 ->setParameter('id', $id);
159 }
160
161 $results = $qb->getQuery()
162 ->getResult();
163
164 return count($results) === 0;
165 }
166
167 /**
168 * @throws ConflictException
169 */
170 public function verifyNameIsUnique(string $name, ?int $id): void {
171 if (!$this->isNameUnique($name, $id)) {
172 throw new ConflictException("Could not create new segment with name [{$name}] because a segment with that name already exists.");
173 }
174 }
175
176 /**
177 * @param int $id
178 *
179 * @return SegmentEntity
180 * @throws InvalidStateException
181 */
182 public function verifyDynamicSegmentExists(int $id): SegmentEntity {
183 try {
184 $dynamicSegment = $this->findOneById($id);
185 if (!$dynamicSegment instanceof SegmentEntity) {
186 throw InvalidStateException::create()->withMessage(sprintf("Could not find segment with ID '%s'.", $id));
187 }
188 if ($dynamicSegment->getType() !== SegmentEntity::TYPE_DYNAMIC) {
189 throw InvalidStateException::create()->withMessage(sprintf("Segment with ID '%s' is not a dynamic segment. Its type is %s.", $id, $dynamicSegment->getType()));
190 }
191 } catch (InvalidStateException $exception) {
192 $this->loggerFactory->getLogger(LoggerFactory::TOPIC_SEGMENTS)->error(sprintf("Could not verify existence of dynamic segment: %s", $exception->getMessage()));
193 throw $exception;
194 }
195 return $dynamicSegment;
196 }
197
198 /**
199 * @param DynamicSegmentFilterData[] $filtersData
200 * @throws ConflictException
201 * @throws NotFoundException
202 * @throws ORMException
203 */
204 public function createOrUpdate(
205 string $name,
206 string $description = '',
207 string $type = SegmentEntity::TYPE_DEFAULT,
208 array $filtersData = [],
209 ?int $id = null,
210 bool $displayInManageSubscriptionPage = true,
211 ?int $confirmationEmailId = null,
212 ?int $confirmationPageId = null,
213 ?string $publicDescription = null
214 ): SegmentEntity {
215 if ($confirmationEmailId !== null && $confirmationEmailId <= 0) {
216 $confirmationEmailId = null;
217 }
218 if ($confirmationPageId !== null && $confirmationPageId <= 0) {
219 $confirmationPageId = null;
220 }
221 $displayInManageSubPage = $type === SegmentEntity::TYPE_DEFAULT ? $displayInManageSubscriptionPage : false;
222
223 if ($id) {
224 $segment = $this->findOneById($id);
225 if (!$segment instanceof SegmentEntity) {
226 throw new NotFoundException("Segment with ID [{$id}] was not found.");
227 }
228 if ($name !== $segment->getName()) {
229 $this->verifyNameIsUnique($name, $id);
230 $segment->setName($name);
231 }
232 $segment->setDescription($description);
233 $segment->setDisplayInManageSubscriptionPage($displayInManageSubPage);
234 $segment->setConfirmationEmailId($confirmationEmailId);
235 $segment->setConfirmationPageId($confirmationPageId);
236 if ($publicDescription !== null) {
237 $segment->setPublicDescription($publicDescription);
238 }
239 } else {
240 $this->verifyNameIsUnique($name, $id);
241 $segment = new SegmentEntity($name, $type, $description);
242 $segment->setDisplayInManageSubscriptionPage($displayInManageSubPage);
243 $segment->setConfirmationEmailId($confirmationEmailId);
244 $segment->setConfirmationPageId($confirmationPageId);
245 $segment->setPublicDescription($publicDescription ?? '');
246 $this->persist($segment);
247 }
248
249 // We want to remove redundant filters before update
250 while ($segment->getDynamicFilters()->count() > count($filtersData)) {
251 $filterEntity = $segment->getDynamicFilters()->last();
252 if ($filterEntity) {
253 $segment->getDynamicFilters()->removeElement($filterEntity);
254 $this->entityManager->remove($filterEntity);
255 }
256 }
257
258 $createOrUpdateFilter = function ($filterData, $key) use ($segment) {
259 if ($filterData instanceof DynamicSegmentFilterData) {
260 $filterEntity = $segment->getDynamicFilters()->get($key);
261 if (!$filterEntity instanceof DynamicSegmentFilterEntity) {
262 $filterEntity = new DynamicSegmentFilterEntity($segment, $filterData);
263 $segment->getDynamicFilters()->add($filterEntity);
264 $this->entityManager->persist($filterEntity);
265 } else {
266 $filterEntity->setFilterData($filterData);
267 }
268 }
269 };
270
271 $wpActionName = 'mailpoet_dynamic_segments_filters_save';
272 if ($this->wp->hasAction($wpActionName)) {
273 $this->wp->doAction($wpActionName, $createOrUpdateFilter, $filtersData);
274 } else {
275 $filterData = reset($filtersData);
276 $key = key($filtersData);
277 $createOrUpdateFilter($filterData, $key);
278 }
279
280 $this->flush();
281 return $segment;
282 }
283
284 public function bulkDelete(array $ids, string $type = SegmentEntity::TYPE_DEFAULT): int {
285 if (empty($ids)) {
286 return 0;
287 }
288
289 // Dynamic segments never materialize memberships in subscriber_segment, so
290 // deleting them cannot change any subscriber's segments_count. Skip the
291 // capture (and the recalc below) for them.
292 $isDynamic = $type === SegmentEntity::TYPE_DYNAMIC;
293
294 // Capture the affected subscribers before the cascade removes their
295 // memberships, so segments_count can be refreshed for them afterwards. When
296 // there are too many to recompute inline, skip the (potentially huge)
297 // capture and let the background sweep reconcile their counts instead.
298 $deferRecalculation = false;
299 $affectedSubscriberIds = [];
300 if (!$isDynamic) {
301 $deferRecalculation = $this->segmentsCountRecalculator->countSegmentMembers($ids, true, $type) >= $this->segmentsCountRecalculator->getDeferThreshold();
302 if (!$deferRecalculation) {
303 $affectedSubscriberIds = $this->getSubscriberIdsForSegments($ids, $type);
304 }
305 }
306
307 $count = 0;
308 $this->entityManager->transactional(function (EntityManager $entityManager) use ($ids, $type, &$count) {
309 $subscriberSegmentTable = $entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
310 $segmentTable = $entityManager->getClassMetadata(SegmentEntity::class)->getTableName();
311 $segmentFiltersTable = $entityManager->getClassMetadata(DynamicSegmentFilterEntity::class)->getTableName();
312
313 $entityManager->getConnection()->executeStatement("
314 DELETE ss FROM $subscriberSegmentTable ss
315 JOIN $segmentTable s ON ss.`segment_id` = s.`id`
316 WHERE ss.`segment_id` IN (:ids)
317 AND s.`type` = :type
318 ", [
319 'ids' => $ids,
320 'type' => $type,
321 ], ['ids' => ArrayParameterType::INTEGER]);
322
323 $entityManager->getConnection()->executeStatement("
324 DELETE df FROM $segmentFiltersTable df
325 WHERE df.`segment_id` IN (:ids)
326 ", [
327 'ids' => $ids,
328 ], ['ids' => ArrayParameterType::INTEGER]);
329
330 $queryBuilder = $entityManager->createQueryBuilder();
331 $count = $queryBuilder->delete(SegmentEntity::class, 's')
332 ->where('s.id IN (:ids)')
333 ->andWhere('s.type = :type')
334 ->setParameter('ids', $ids, ArrayParameterType::INTEGER)
335 ->setParameter('type', $type, ParameterType::STRING)
336 ->getQuery()->execute();
337
338 $queryBuilder = $entityManager->createQueryBuilder();
339 $queryBuilder->delete(NewsletterSegmentEntity::class, 'ns')
340 ->where('ns.segment IN (:ids)')
341 ->setParameter('ids', $ids, ArrayParameterType::INTEGER)
342 ->getQuery()->execute();
343 });
344
345 if (!$isDynamic) {
346 if ($deferRecalculation) {
347 $this->segmentsCountRecalculator->scheduleBackgroundRecalculation();
348 } else {
349 $this->segmentsCountRecalculator->recalculateForSubscribers($affectedSubscriberIds);
350 }
351 }
352
353 return $count;
354 }
355
356 /**
357 * @param int[] $segmentIds
358 * @return int[]
359 */
360 private function getSubscriberIdsForSegments(array $segmentIds, string $type): array {
361 if (empty($segmentIds)) {
362 return [];
363 }
364
365 $subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
366 $segmentTable = $this->entityManager->getClassMetadata(SegmentEntity::class)->getTableName();
367
368 $subscribedStatus = SubscriberEntity::STATUS_SUBSCRIBED;
369 $ids = $this->entityManager->getConnection()->executeQuery("
370 SELECT DISTINCT ss.`subscriber_id` FROM $subscriberSegmentTable ss
371 JOIN $segmentTable s ON ss.`segment_id` = s.`id`
372 WHERE ss.`segment_id` IN (:ids)
373 AND s.`type` = :type
374 AND ss.`status` = :subscribedStatus
375 ", [
376 'ids' => $segmentIds,
377 'type' => $type,
378 'subscribedStatus' => $subscribedStatus,
379 ], ['ids' => ArrayParameterType::INTEGER])->fetchFirstColumn();
380
381 return array_map(function ($id): int {
382 return is_numeric($id) ? (int)$id : 0;
383 }, $ids);
384 }
385
386 public function bulkTrash(array $ids, string $type = SegmentEntity::TYPE_DEFAULT): int {
387 $activelyUsedInNewsletters = $this->newsletterSegmentRepository->getSubjectsOfActivelyUsedEmailsForSegments($ids);
388 $activelyUsedInForms = $this->formsRepository->getNamesOfFormsForSegments();
389 $activelyUsed = array_unique(array_merge(array_keys($activelyUsedInNewsletters), array_keys($activelyUsedInForms)));
390 $ids = array_diff($ids, $activelyUsed);
391 return $this->updateDeletedAt($ids, new Carbon(), $type);
392 }
393
394 public function doTrash(array $ids, string $type = SegmentEntity::TYPE_DEFAULT): int {
395 return $this->updateDeletedAt($ids, new Carbon(), $type);
396 }
397
398 public function bulkRestore(array $ids, string $type = SegmentEntity::TYPE_DEFAULT): int {
399 return $this->updateDeletedAt($ids, null, $type);
400 }
401
402 private function updateDeletedAt(array $ids, ?DateTime $deletedAt, string $type): int {
403 if (empty($ids)) {
404 return 0;
405 }
406
407 $rows = $this->entityManager->createQueryBuilder()->update(SegmentEntity::class, 's')
408 ->set('s.deletedAt', ':deletedAt')
409 ->where('s.id IN (:ids)')
410 ->andWhere('s.type IN (:type)')
411 ->setParameter('deletedAt', $deletedAt)
412 ->setParameter('ids', $ids)
413 ->setParameter('type', $type)
414 ->getQuery()->execute();
415
416 // Trashing or restoring a segment changes whether its memberships count
417 // towards segments_count, so refresh every affected subscriber. The
418 // memberships still exist here (only deleted_at changed), so walk them in
419 // keyset-paginated batches instead of materializing every member id at once.
420 // Dynamic segments have no materialized memberships, so there is nothing to
421 // recalculate for them.
422 if ($type !== SegmentEntity::TYPE_DYNAMIC) {
423 $this->segmentsCountRecalculator->recalculateForSegments($ids);
424 }
425
426 return $rows;
427 }
428
429 public function findByUpdatedScoreNotInLastDay(int $limit): array {
430 $dateTime = (new Carbon())->subDay();
431 return $this->entityManager->createQueryBuilder()
432 ->select('s')
433 ->from(SegmentEntity::class, 's')
434 ->where('s.averageEngagementScoreUpdatedAt IS NULL')
435 ->orWhere('s.averageEngagementScoreUpdatedAt < :dateTime')
436 ->setParameter('dateTime', $dateTime)
437 ->getQuery()
438 ->setMaxResults($limit)
439 ->getResult();
440 }
441
442 /**
443 * Returns count of segments that have more than one dynamic filter
444 */
445 public function getSegmentCountWithMultipleFilters(): int {
446 $segmentFiltersTable = $this->entityManager->getClassMetadata(DynamicSegmentFilterEntity::class)->getTableName();
447 $qbInner = $this->entityManager->getConnection()->createQueryBuilder()
448 ->select('COUNT(DISTINCT sf.id) AS segmentCount')
449 ->from($segmentFiltersTable, 'sf')
450 ->groupBy('sf.segment_id')
451 ->having('COUNT(sf.id) > 1');
452 /** @var null|int $result */
453 $result = $this->entityManager->getConnection()->createQueryBuilder()
454 ->select('count(*)')
455 ->from(sprintf('(%s) as subCounts', $qbInner->getSQL()))
456 ->execute()
457 ->fetchOne();
458 return (int)$result;
459 }
460 }
461