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 / DynamicSegments / FilterHandler.php
mailpoet / lib / Segments / DynamicSegments Last commit date
Exceptions 2 months ago Filters 1 month ago DynamicSegmentFilterRepository.php 4 years ago DynamicSegmentsListingRepository.php 1 month ago FilterDataMapper.php 1 month ago FilterFactory.php 2 months ago FilterHandler.php 2 months ago SegmentSaveController.php 2 years ago index.php 3 years ago
FilterHandler.php
188 lines
1 <?php declare(strict_types = 1);
2
3 namespace MailPoet\Segments\DynamicSegments;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Entities\DynamicSegmentFilterData;
9 use MailPoet\Entities\SegmentEntity;
10 use MailPoet\Entities\SubscriberEntity;
11 use MailPoet\Segments\SegmentDependencyValidator;
12 use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
13 use MailPoetVendor\Doctrine\ORM\EntityManager;
14
15 class FilterHandler {
16 /** @var EntityManager */
17 private $entityManager;
18
19 /** @var SegmentDependencyValidator */
20 private $segmentDependencyValidator;
21
22 /** @var FilterFactory */
23 private $filterFactory;
24
25 public function __construct(
26 EntityManager $entityManager,
27 SegmentDependencyValidator $segmentDependencyValidator,
28 FilterFactory $filterFactory
29 ) {
30
31 $this->entityManager = $entityManager;
32 $this->segmentDependencyValidator = $segmentDependencyValidator;
33 $this->filterFactory = $filterFactory;
34 }
35
36 public function apply(QueryBuilder $queryBuilder, SegmentEntity $segment): QueryBuilder {
37 $allFilters = $segment->getDynamicFilters();
38 $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
39 $pluginsForAllFiltersMissing = $this->segmentDependencyValidator->getMissingPluginsByAllFilters($allFilters);
40 if ($pluginsForAllFiltersMissing) {
41 return $queryBuilder->andWhere('1 = 0');
42 }
43
44 $groups = $segment->getFilterGroups();
45 $builtGroups = [];
46 foreach ($groups as $group) {
47 $groupOperator = $group['operator'];
48 $filterSelects = [];
49 foreach ($group['filters'] as $filter) {
50 $subscribersIdsQuery = $this->entityManager
51 ->getConnection()
52 ->createQueryBuilder()
53 ->select("DISTINCT $subscribersTable.id as inner_subscriber_id")
54 ->from($subscribersTable);
55 // When a required plugin is missing inside a NONE group, we cannot evaluate the
56 // negation reliably — fail closed and return empty.
57 $missingPlugins = $this->segmentDependencyValidator->getMissingPluginsByFilter($filter);
58 if ($missingPlugins && $groupOperator === DynamicSegmentFilterData::CONNECT_TYPE_NONE) {
59 return $queryBuilder->andWhere('1 = 0');
60 }
61 if ($missingPlugins) {
62 $subscribersIdsQuery->andWhere('1 = 0');
63 } else {
64 $this->filterFactory->getFilterForFilterEntity($filter)->apply($subscribersIdsQuery, $filter);
65 }
66 $filterSelects[] = $subscribersIdsQuery->getSQL();
67 $queryBuilder->setParameters(
68 array_merge(
69 $subscribersIdsQuery->getParameters(),
70 $queryBuilder->getParameters()
71 ),
72 array_merge(
73 $subscribersIdsQuery->getParameterTypes(),
74 $queryBuilder->getParameterTypes()
75 )
76 );
77 }
78 if ($filterSelects) {
79 $builtGroups[] = ['operator' => $groupOperator, 'selects' => $filterSelects];
80 }
81 }
82
83 // Fast path: a single group — emit its filter subqueries directly via joinSubqueries
84 // with the group's own operator. Covers every legacy segment (which always collapses
85 // to one group) and avoids wrapping the filter selects in an extra derived table.
86 // Preserves the SQL shape MySQL was previously optimizing against.
87 if (count($builtGroups) === 1) {
88 $only = $builtGroups[0];
89 $this->joinSubqueries($queryBuilder, $only['operator'], $only['selects']);
90 return $queryBuilder;
91 }
92
93 $groupSelects = [];
94 foreach ($builtGroups as $built) {
95 $groupSelects[] = $this->combineGroupFilters($built['operator'], $built['selects'], $subscribersTable);
96 }
97
98 $outerOperator = $segment->getFiltersConnectOperator();
99 // Multi-group segments must use AND/OR as the outer connector — FilterDataMapper
100 // rejects NONE in that case. Coerce defensively in case unusual data slips through.
101 if ($outerOperator === DynamicSegmentFilterData::CONNECT_TYPE_NONE) {
102 $outerOperator = DynamicSegmentFilterData::CONNECT_TYPE_AND;
103 }
104 $this->joinSubqueries($queryBuilder, $outerOperator, $groupSelects);
105 return $queryBuilder;
106 }
107
108 /**
109 * Combines filter subqueries within a single group into one derived-table SQL string
110 * that yields `inner_subscriber_id` rows.
111 *
112 * @param string[] $filterSelects
113 */
114 private function combineGroupFilters(string $groupOperator, array $filterSelects, string $subscribersTable): string {
115 if ($groupOperator === DynamicSegmentFilterData::CONNECT_TYPE_NONE) {
116 $unionSql = join(' UNION ', $filterSelects);
117 return sprintf(
118 "SELECT %s.id AS inner_subscriber_id FROM %s LEFT JOIN (%s) excluded_subscribers ON excluded_subscribers.inner_subscriber_id = %s.id WHERE excluded_subscribers.inner_subscriber_id IS NULL",
119 $subscribersTable,
120 $subscribersTable,
121 $unionSql,
122 $subscribersTable
123 );
124 }
125
126 if (count($filterSelects) === 1) {
127 return $filterSelects[0];
128 }
129
130 if ($groupOperator === DynamicSegmentFilterData::CONNECT_TYPE_OR) {
131 return join(' UNION ', $filterSelects);
132 }
133
134 // AND: chain INNER JOINs on inner_subscriber_id
135 $sql = sprintf('SELECT a0.inner_subscriber_id FROM (%s) a0', array_shift($filterSelects));
136 $index = 1;
137 foreach ($filterSelects as $filterSelect) {
138 $sql .= sprintf(
139 ' INNER JOIN (%s) a%d ON a%d.inner_subscriber_id = a0.inner_subscriber_id',
140 $filterSelect,
141 $index,
142 $index
143 );
144 $index++;
145 }
146 return $sql;
147 }
148
149 private function joinSubqueries(QueryBuilder $queryBuilder, string $filtersConnectOperator, array $subQueries): QueryBuilder {
150 if (!$subQueries) return $queryBuilder;
151 $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
152
153 if ($filtersConnectOperator === DynamicSegmentFilterData::CONNECT_TYPE_OR) {
154 // the final query: SELECT * FROM subscribers INNER JOIN (filter_select1 UNION filter_select2) filtered_subscribers ON filtered_subscribers.inner_subscriber_id = id
155 $queryBuilder->innerJoin(
156 $subscribersTable,
157 sprintf('(%s)', join(' UNION ', $subQueries)),
158 'filtered_subscribers',
159 "filtered_subscribers.inner_subscriber_id = $subscribersTable.id"
160 );
161 return $queryBuilder;
162 }
163
164 if ($filtersConnectOperator === DynamicSegmentFilterData::CONNECT_TYPE_NONE) {
165 $queryBuilder->leftJoin(
166 $subscribersTable,
167 sprintf('(%s)', join(' UNION ', $subQueries)),
168 'filtered_subscribers',
169 "filtered_subscribers.inner_subscriber_id = $subscribersTable.id"
170 )
171 ->andWhere('filtered_subscribers.inner_subscriber_id IS NULL');
172 return $queryBuilder;
173 }
174
175 foreach ($subQueries as $key => $subQuery) {
176 // we need a unique name for each subquery so that we can join them together in the sql query - just make sure the identifier starts with a letter, not a number
177 $subqueryName = 'a' . $key;
178 $queryBuilder->innerJoin(
179 $subscribersTable,
180 "($subQuery)",
181 $subqueryName,
182 "$subqueryName.inner_subscriber_id = $subscribersTable.id"
183 );
184 }
185 return $queryBuilder;
186 }
187 }
188