PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.28.1
MailPoet – Newsletters, Email Marketing, and Automation v5.28.1
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 2 days ago RestApi 1 week ago SegmentDependencyValidator.php 3 years ago SegmentListingRepository.php 2 weeks ago SegmentSaveController.php 2 weeks ago SegmentSubscribersRepository.php 4 weeks ago SegmentsFinder.php 3 years ago SegmentsRepository.php 2 weeks ago SegmentsSimpleListRepository.php 4 weeks ago SubscribersFinder.php 4 weeks ago WP.php 3 weeks ago WooCommerce.php 4 weeks ago index.php 3 years ago
SegmentsRepository.php
387 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\SubscriberSegmentEntity;
16 use MailPoet\Form\FormsRepository;
17 use MailPoet\InvalidStateException;
18 use MailPoet\Logging\LoggerFactory;
19 use MailPoet\Newsletter\Segment\NewsletterSegmentRepository;
20 use MailPoet\NotFoundException;
21 use MailPoet\WP\Functions as WPFunctions;
22 use MailPoetVendor\Carbon\Carbon;
23 use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
24 use MailPoetVendor\Doctrine\DBAL\ParameterType;
25 use MailPoetVendor\Doctrine\ORM\EntityManager;
26 use MailPoetVendor\Doctrine\ORM\ORMException;
27
28 /**
29 * @extends Repository<SegmentEntity>
30 */
31 class SegmentsRepository extends Repository {
32
33 /** @var NewsletterSegmentRepository */
34 private $newsletterSegmentRepository;
35
36 /** @var FormsRepository */
37 private $formsRepository;
38
39 /** @var WPFunctions */
40 private $wp;
41
42 /** @var LoggerFactory */
43 private $loggerFactory;
44
45 public function __construct(
46 EntityManager $entityManager,
47 NewsletterSegmentRepository $newsletterSegmentRepository,
48 FormsRepository $formsRepository,
49 WPFunctions $wp,
50 LoggerFactory $loggerFactory
51 ) {
52 parent::__construct($entityManager);
53 $this->newsletterSegmentRepository = $newsletterSegmentRepository;
54 $this->formsRepository = $formsRepository;
55 $this->wp = $wp;
56 $this->loggerFactory = $loggerFactory;
57 }
58
59 protected function getEntityClassName() {
60 return SegmentEntity::class;
61 }
62
63 /**
64 * @param string[] $types
65 * @return SegmentEntity[]
66 */
67 public function findByTypeNotIn(array $types): array {
68 return $this->doctrineRepository->createQueryBuilder('s')
69 ->select('s')
70 ->where('s.type NOT IN (:types)')
71 ->setParameter('types', $types)
72 ->getQuery()
73 ->getResult();
74 }
75
76 public function getWPUsersSegment(): SegmentEntity {
77 $cached = current(
78 array_filter(
79 $this->getAllFromIdentityMap(),
80 fn(SegmentEntity $segment) => $segment->getType() === SegmentEntity::TYPE_WP_USERS
81 )
82 );
83
84 if ($cached) {
85 return $cached;
86 }
87
88 $segment = $this->findOneBy(['type' => SegmentEntity::TYPE_WP_USERS]);
89 if (!$segment) {
90 // create the wp users segment
91 $segment = new SegmentEntity(
92 __('WordPress Users', 'mailpoet'),
93 SegmentEntity::TYPE_WP_USERS,
94 __('This list contains all of your WordPress users.', 'mailpoet')
95 );
96 $this->entityManager->persist($segment);
97 $this->entityManager->flush();
98 }
99 return $segment;
100 }
101
102 public function getWooCommerceSegment(): SegmentEntity {
103 $cached = current(
104 array_filter(
105 $this->getAllFromIdentityMap(),
106 fn(SegmentEntity $segment) => $segment->getType() === SegmentEntity::TYPE_WC_USERS
107 )
108 );
109
110 if ($cached) {
111 return $cached;
112 }
113
114 $segment = $this->findOneBy(['type' => SegmentEntity::TYPE_WC_USERS]);
115 if (!$segment) {
116 // create the WooCommerce customers segment
117 $segment = new SegmentEntity(
118 __('WooCommerce Customers', 'mailpoet'),
119 SegmentEntity::TYPE_WC_USERS,
120 __('This list contains all of your WooCommerce customers.', 'mailpoet')
121 );
122 $this->entityManager->persist($segment);
123 $this->entityManager->flush();
124 }
125 return $segment;
126 }
127
128 public function getCountsPerType(): array {
129 $results = $this->doctrineRepository->createQueryBuilder('s')
130 ->select('s.type, COUNT(s) as cnt')
131 ->where('s.deletedAt IS NULL')
132 ->groupBy('s.type')
133 ->getQuery()
134 ->getResult();
135
136 $countMap = [];
137 foreach ($results as $result) {
138 $countMap[$result['type']] = (int)$result['cnt'];
139 }
140 return $countMap;
141 }
142
143 public function isNameUnique(string $name, ?int $id): bool {
144 $qb = $this->doctrineRepository->createQueryBuilder('s')
145 ->select('s')
146 ->where('s.name = :name')
147 ->setParameter('name', $name);
148
149 if ($id !== null) {
150 $qb->andWhere('s.id != :id')
151 ->setParameter('id', $id);
152 }
153
154 $results = $qb->getQuery()
155 ->getResult();
156
157 return count($results) === 0;
158 }
159
160 /**
161 * @throws ConflictException
162 */
163 public function verifyNameIsUnique(string $name, ?int $id): void {
164 if (!$this->isNameUnique($name, $id)) {
165 throw new ConflictException("Could not create new segment with name [{$name}] because a segment with that name already exists.");
166 }
167 }
168
169 /**
170 * @param int $id
171 *
172 * @return SegmentEntity
173 * @throws InvalidStateException
174 */
175 public function verifyDynamicSegmentExists(int $id): SegmentEntity {
176 try {
177 $dynamicSegment = $this->findOneById($id);
178 if (!$dynamicSegment instanceof SegmentEntity) {
179 throw InvalidStateException::create()->withMessage(sprintf("Could not find segment with ID '%s'.", $id));
180 }
181 if ($dynamicSegment->getType() !== SegmentEntity::TYPE_DYNAMIC) {
182 throw InvalidStateException::create()->withMessage(sprintf("Segment with ID '%s' is not a dynamic segment. Its type is %s.", $id, $dynamicSegment->getType()));
183 }
184 } catch (InvalidStateException $exception) {
185 $this->loggerFactory->getLogger(LoggerFactory::TOPIC_SEGMENTS)->error(sprintf("Could not verify existence of dynamic segment: %s", $exception->getMessage()));
186 throw $exception;
187 }
188 return $dynamicSegment;
189 }
190
191 /**
192 * @param DynamicSegmentFilterData[] $filtersData
193 * @throws ConflictException
194 * @throws NotFoundException
195 * @throws ORMException
196 */
197 public function createOrUpdate(
198 string $name,
199 string $description = '',
200 string $type = SegmentEntity::TYPE_DEFAULT,
201 array $filtersData = [],
202 ?int $id = null,
203 bool $displayInManageSubscriptionPage = true,
204 ?int $confirmationEmailId = null,
205 ?int $confirmationPageId = null,
206 ?string $publicDescription = null
207 ): SegmentEntity {
208 if ($confirmationEmailId !== null && $confirmationEmailId <= 0) {
209 $confirmationEmailId = null;
210 }
211 if ($confirmationPageId !== null && $confirmationPageId <= 0) {
212 $confirmationPageId = null;
213 }
214 $displayInManageSubPage = $type === SegmentEntity::TYPE_DEFAULT ? $displayInManageSubscriptionPage : false;
215
216 if ($id) {
217 $segment = $this->findOneById($id);
218 if (!$segment instanceof SegmentEntity) {
219 throw new NotFoundException("Segment with ID [{$id}] was not found.");
220 }
221 if ($name !== $segment->getName()) {
222 $this->verifyNameIsUnique($name, $id);
223 $segment->setName($name);
224 }
225 $segment->setDescription($description);
226 $segment->setDisplayInManageSubscriptionPage($displayInManageSubPage);
227 $segment->setConfirmationEmailId($confirmationEmailId);
228 $segment->setConfirmationPageId($confirmationPageId);
229 if ($publicDescription !== null) {
230 $segment->setPublicDescription($publicDescription);
231 }
232 } else {
233 $this->verifyNameIsUnique($name, $id);
234 $segment = new SegmentEntity($name, $type, $description);
235 $segment->setDisplayInManageSubscriptionPage($displayInManageSubPage);
236 $segment->setConfirmationEmailId($confirmationEmailId);
237 $segment->setConfirmationPageId($confirmationPageId);
238 $segment->setPublicDescription($publicDescription ?? '');
239 $this->persist($segment);
240 }
241
242 // We want to remove redundant filters before update
243 while ($segment->getDynamicFilters()->count() > count($filtersData)) {
244 $filterEntity = $segment->getDynamicFilters()->last();
245 if ($filterEntity) {
246 $segment->getDynamicFilters()->removeElement($filterEntity);
247 $this->entityManager->remove($filterEntity);
248 }
249 }
250
251 $createOrUpdateFilter = function ($filterData, $key) use ($segment) {
252 if ($filterData instanceof DynamicSegmentFilterData) {
253 $filterEntity = $segment->getDynamicFilters()->get($key);
254 if (!$filterEntity instanceof DynamicSegmentFilterEntity) {
255 $filterEntity = new DynamicSegmentFilterEntity($segment, $filterData);
256 $segment->getDynamicFilters()->add($filterEntity);
257 $this->entityManager->persist($filterEntity);
258 } else {
259 $filterEntity->setFilterData($filterData);
260 }
261 }
262 };
263
264 $wpActionName = 'mailpoet_dynamic_segments_filters_save';
265 if ($this->wp->hasAction($wpActionName)) {
266 $this->wp->doAction($wpActionName, $createOrUpdateFilter, $filtersData);
267 } else {
268 $filterData = reset($filtersData);
269 $key = key($filtersData);
270 $createOrUpdateFilter($filterData, $key);
271 }
272
273 $this->flush();
274 return $segment;
275 }
276
277 public function bulkDelete(array $ids, string $type = SegmentEntity::TYPE_DEFAULT): int {
278 if (empty($ids)) {
279 return 0;
280 }
281
282 $count = 0;
283 $this->entityManager->transactional(function (EntityManager $entityManager) use ($ids, $type, &$count) {
284 $subscriberSegmentTable = $entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
285 $segmentTable = $entityManager->getClassMetadata(SegmentEntity::class)->getTableName();
286 $segmentFiltersTable = $entityManager->getClassMetadata(DynamicSegmentFilterEntity::class)->getTableName();
287
288 $entityManager->getConnection()->executeStatement("
289 DELETE ss FROM $subscriberSegmentTable ss
290 JOIN $segmentTable s ON ss.`segment_id` = s.`id`
291 WHERE ss.`segment_id` IN (:ids)
292 AND s.`type` = :type
293 ", [
294 'ids' => $ids,
295 'type' => $type,
296 ], ['ids' => ArrayParameterType::INTEGER]);
297
298 $entityManager->getConnection()->executeStatement("
299 DELETE df FROM $segmentFiltersTable df
300 WHERE df.`segment_id` IN (:ids)
301 ", [
302 'ids' => $ids,
303 ], ['ids' => ArrayParameterType::INTEGER]);
304
305 $queryBuilder = $entityManager->createQueryBuilder();
306 $count = $queryBuilder->delete(SegmentEntity::class, 's')
307 ->where('s.id IN (:ids)')
308 ->andWhere('s.type = :type')
309 ->setParameter('ids', $ids, ArrayParameterType::INTEGER)
310 ->setParameter('type', $type, ParameterType::STRING)
311 ->getQuery()->execute();
312
313 $queryBuilder = $entityManager->createQueryBuilder();
314 $queryBuilder->delete(NewsletterSegmentEntity::class, 'ns')
315 ->where('ns.segment IN (:ids)')
316 ->setParameter('ids', $ids, ArrayParameterType::INTEGER)
317 ->getQuery()->execute();
318 });
319 return $count;
320 }
321
322 public function bulkTrash(array $ids, string $type = SegmentEntity::TYPE_DEFAULT): int {
323 $activelyUsedInNewsletters = $this->newsletterSegmentRepository->getSubjectsOfActivelyUsedEmailsForSegments($ids);
324 $activelyUsedInForms = $this->formsRepository->getNamesOfFormsForSegments();
325 $activelyUsed = array_unique(array_merge(array_keys($activelyUsedInNewsletters), array_keys($activelyUsedInForms)));
326 $ids = array_diff($ids, $activelyUsed);
327 return $this->updateDeletedAt($ids, new Carbon(), $type);
328 }
329
330 public function doTrash(array $ids, string $type = SegmentEntity::TYPE_DEFAULT): int {
331 return $this->updateDeletedAt($ids, new Carbon(), $type);
332 }
333
334 public function bulkRestore(array $ids, string $type = SegmentEntity::TYPE_DEFAULT): int {
335 return $this->updateDeletedAt($ids, null, $type);
336 }
337
338 private function updateDeletedAt(array $ids, ?DateTime $deletedAt, string $type): int {
339 if (empty($ids)) {
340 return 0;
341 }
342
343 $rows = $this->entityManager->createQueryBuilder()->update(SegmentEntity::class, 's')
344 ->set('s.deletedAt', ':deletedAt')
345 ->where('s.id IN (:ids)')
346 ->andWhere('s.type IN (:type)')
347 ->setParameter('deletedAt', $deletedAt)
348 ->setParameter('ids', $ids)
349 ->setParameter('type', $type)
350 ->getQuery()->execute();
351
352 return $rows;
353 }
354
355 public function findByUpdatedScoreNotInLastDay(int $limit): array {
356 $dateTime = (new Carbon())->subDay();
357 return $this->entityManager->createQueryBuilder()
358 ->select('s')
359 ->from(SegmentEntity::class, 's')
360 ->where('s.averageEngagementScoreUpdatedAt IS NULL')
361 ->orWhere('s.averageEngagementScoreUpdatedAt < :dateTime')
362 ->setParameter('dateTime', $dateTime)
363 ->getQuery()
364 ->setMaxResults($limit)
365 ->getResult();
366 }
367
368 /**
369 * Returns count of segments that have more than one dynamic filter
370 */
371 public function getSegmentCountWithMultipleFilters(): int {
372 $segmentFiltersTable = $this->entityManager->getClassMetadata(DynamicSegmentFilterEntity::class)->getTableName();
373 $qbInner = $this->entityManager->getConnection()->createQueryBuilder()
374 ->select('COUNT(DISTINCT sf.id) AS segmentCount')
375 ->from($segmentFiltersTable, 'sf')
376 ->groupBy('sf.segment_id')
377 ->having('COUNT(sf.id) > 1');
378 /** @var null|int $result */
379 $result = $this->entityManager->getConnection()->createQueryBuilder()
380 ->select('count(*)')
381 ->from(sprintf('(%s) as subCounts', $qbInner->getSQL()))
382 ->execute()
383 ->fetchOne();
384 return (int)$result;
385 }
386 }
387