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 / FilterDataMapper.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 2 months ago FilterFactory.php 2 months ago FilterHandler.php 2 months ago SegmentSaveController.php 2 years ago index.php 3 years ago
FilterDataMapper.php
762 lines
1 <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
2
3 namespace MailPoet\Segments\DynamicSegments;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Entities\DynamicSegmentFilterData;
9 use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
10 use MailPoet\Segments\DynamicSegments\Filters\AutomationsEvents;
11 use MailPoet\Segments\DynamicSegments\Filters\DateFilterHelper;
12 use MailPoet\Segments\DynamicSegments\Filters\EmailAction;
13 use MailPoet\Segments\DynamicSegments\Filters\EmailActionClickAny;
14 use MailPoet\Segments\DynamicSegments\Filters\EmailOpensAbsoluteCountAction;
15 use MailPoet\Segments\DynamicSegments\Filters\EmailsReceived;
16 use MailPoet\Segments\DynamicSegments\Filters\FilterHelper;
17 use MailPoet\Segments\DynamicSegments\Filters\MailPoetCustomFields;
18 use MailPoet\Segments\DynamicSegments\Filters\NumberOfClicks;
19 use MailPoet\Segments\DynamicSegments\Filters\SubscriberDateField;
20 use MailPoet\Segments\DynamicSegments\Filters\SubscriberScore;
21 use MailPoet\Segments\DynamicSegments\Filters\SubscriberSegment;
22 use MailPoet\Segments\DynamicSegments\Filters\SubscriberSubscribedViaForm;
23 use MailPoet\Segments\DynamicSegments\Filters\SubscriberTag;
24 use MailPoet\Segments\DynamicSegments\Filters\SubscriberTextField;
25 use MailPoet\Segments\DynamicSegments\Filters\WooCommerceAverageSpent;
26 use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCategory;
27 use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCountry;
28 use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCustomerTextField;
29 use MailPoet\Segments\DynamicSegments\Filters\WooCommerceFirstOrder;
30 use MailPoet\Segments\DynamicSegments\Filters\WooCommerceMembership;
31 use MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfOrders;
32 use MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfReviews;
33 use MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct;
34 use MailPoet\Segments\DynamicSegments\Filters\WooCommerceProductVariation;
35 use MailPoet\Segments\DynamicSegments\Filters\WooCommercePurchaseDate;
36 use MailPoet\Segments\DynamicSegments\Filters\WooCommercePurchasedWithAttribute;
37 use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSingleOrderValue;
38 use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSubscription;
39 use MailPoet\Segments\DynamicSegments\Filters\WooCommerceTag;
40 use MailPoet\Segments\DynamicSegments\Filters\WooCommerceTotalSpent;
41 use MailPoet\Segments\DynamicSegments\Filters\WooCommerceUsedCouponCode;
42 use MailPoet\Segments\DynamicSegments\Filters\WooCommerceUsedPaymentMethod;
43 use MailPoet\Segments\DynamicSegments\Filters\WooCommerceUsedShippingMethod;
44 use MailPoet\WP\Functions as WPFunctions;
45
46 class FilterDataMapper {
47 public const MAX_GROUPS = 5;
48 public const MAX_FILTERS_PER_GROUP = 10;
49
50 private WPFunctions $wp;
51
52 private DateFilterHelper $dateFilterHelper;
53
54 private WooCommerceNumberOfReviews $wooCommerceNumberOfReviews;
55
56 private FilterHelper $filterHelper;
57
58 private WooCommerceUsedCouponCode $wooCommerceUsedCouponCode;
59
60 private WooCommerceTag $wooCommerceTag;
61
62 private WooCommercePurchasedWithAttribute $wooCommercePurchasedWithAttribute;
63
64 public function __construct(
65 WPFunctions $wp,
66 DateFilterHelper $dateFilterHelper,
67 FilterHelper $filterHelper,
68 WooCommerceNumberOfReviews $wooCommerceNumberOfReviews,
69 WooCommerceUsedCouponCode $wooCommerceUsedCouponCode,
70 WooCommercePurchasedWithAttribute $wooCommercePurchasedWithAttribute,
71 WooCommerceTag $wooCommerceTag
72 ) {
73 $this->wp = $wp;
74 $this->dateFilterHelper = $dateFilterHelper;
75 $this->filterHelper = $filterHelper;
76 $this->wooCommerceNumberOfReviews = $wooCommerceNumberOfReviews;
77 $this->wooCommerceUsedCouponCode = $wooCommerceUsedCouponCode;
78 $this->wooCommercePurchasedWithAttribute = $wooCommercePurchasedWithAttribute;
79 $this->wooCommerceTag = $wooCommerceTag;
80 }
81
82 /**
83 * @param array $data
84 * @return DynamicSegmentFilterData[]
85 */
86 public function map(array $data = []): array {
87 if (!isset($data['filters']) || count($data['filters'] ?? []) < 1) {
88 throw new InvalidFilterException('Filters are missing', InvalidFilterException::MISSING_FILTER);
89 }
90 $this->validateGroups($data['filters'], $data['filters_connect'] ?? null);
91 $processFilter = function ($filter, $data) {
92 $filter['connect'] = $data['filters_connect'] ?? DynamicSegmentFilterData::CONNECT_TYPE_AND;
93 return $this->withGroupingParams($this->createFilter($filter), $filter);
94 };
95 $wpFilterName = 'mailpoet_dynamic_segments_filters_map';
96 if ($this->wp->hasFilter($wpFilterName)) {
97 $filtered = $this->wp->applyFilters($wpFilterName, $data, $processFilter);
98 if (is_array($filtered)) {
99 return array_values(array_filter($filtered, function ($f) {
100 return $f instanceof DynamicSegmentFilterData;
101 }));
102 }
103 }
104 $filter = reset($data['filters']);
105 return [$processFilter($filter, $data)];
106 }
107
108 /**
109 * @throws InvalidFilterException
110 */
111 private function validateGroups(array $filters, ?string $filtersConnect): void {
112 $groupCounts = [];
113 foreach ($filters as $filter) {
114 if (!isset($filter['group_id'])) {
115 continue;
116 }
117 $groupId = (int)$filter['group_id'];
118 $groupCounts[$groupId] = ($groupCounts[$groupId] ?? 0) + 1;
119 }
120 if (count($groupCounts) > self::MAX_GROUPS) {
121 throw new InvalidFilterException(
122 'Too many filter groups (max ' . self::MAX_GROUPS . ')',
123 InvalidFilterException::TOO_MANY_GROUPS
124 );
125 }
126 foreach ($groupCounts as $count) {
127 if ($count > self::MAX_FILTERS_PER_GROUP) {
128 throw new InvalidFilterException(
129 'Too many filters in a group (max ' . self::MAX_FILTERS_PER_GROUP . ')',
130 InvalidFilterException::TOO_MANY_FILTERS_PER_GROUP
131 );
132 }
133 }
134 if (count($groupCounts) > 1 && $filtersConnect === DynamicSegmentFilterData::CONNECT_TYPE_NONE) {
135 throw new InvalidFilterException(
136 'Outer connector must be and/or for grouped segments',
137 InvalidFilterException::INVALID_OUTER_CONNECTOR
138 );
139 }
140 }
141
142 /**
143 * Stamps group_id/group_operator from the raw filter row onto the constructed
144 * filter data so the entity can group filters at query time.
145 */
146 private function withGroupingParams(DynamicSegmentFilterData $filterData, array $rawFilter): DynamicSegmentFilterData {
147 if (!isset($rawFilter['group_id'])) {
148 return $filterData;
149 }
150 $existingData = $filterData->getData() ?? [];
151 $existingData['group_id'] = (int)$rawFilter['group_id'];
152 $existingData['group_operator'] = $this->normalizeGroupOperator($rawFilter['group_operator'] ?? null);
153 return new DynamicSegmentFilterData(
154 (string)$filterData->getFilterType(),
155 (string)$filterData->getAction(),
156 $existingData
157 );
158 }
159
160 private function normalizeGroupOperator($value): string {
161 if (
162 $value === DynamicSegmentFilterData::CONNECT_TYPE_AND
163 || $value === DynamicSegmentFilterData::CONNECT_TYPE_OR
164 || $value === DynamicSegmentFilterData::CONNECT_TYPE_NONE
165 ) {
166 return $value;
167 }
168 return DynamicSegmentFilterData::CONNECT_TYPE_AND;
169 }
170
171 private function createFilter(array $filterData): DynamicSegmentFilterData {
172 if (isset($filterData['days']) && !isset($filterData['timeframe'])) {
173 // Backwards compatibility for filters created before time period component had "over all time" option
174 $filterData['timeframe'] = DynamicSegmentFilterData::TIMEFRAME_IN_THE_LAST;
175 }
176 switch ($this->getSegmentType($filterData)) {
177 case DynamicSegmentFilterData::TYPE_AUTOMATIONS:
178 return $this->createAutomations($filterData);
179 case DynamicSegmentFilterData::TYPE_USER_ROLE:
180 return $this->createSubscriber($filterData);
181 case DynamicSegmentFilterData::TYPE_EMAIL:
182 return $this->createEmail($filterData);
183 case DynamicSegmentFilterData::TYPE_WOOCOMMERCE:
184 return $this->createWooCommerce($filterData);
185 case DynamicSegmentFilterData::TYPE_WOOCOMMERCE_MEMBERSHIP:
186 return $this->createWooCommerceMembership($filterData);
187 case DynamicSegmentFilterData::TYPE_WOOCOMMERCE_SUBSCRIPTION:
188 return $this->createWooCommerceSubscription($filterData);
189 default:
190 throw new InvalidFilterException('Invalid type', InvalidFilterException::INVALID_TYPE);
191 }
192 }
193
194 /**
195 * @throws InvalidFilterException
196 */
197 private function getSegmentType(array $data): string {
198 if (!isset($data['segmentType'])) {
199 throw new InvalidFilterException('Segment type is not set', InvalidFilterException::MISSING_TYPE);
200 }
201 return $data['segmentType'];
202 }
203
204 private function createAutomations(array $data): DynamicSegmentFilterData {
205 if (empty($data['action'])) {
206 throw new InvalidFilterException('Missing automations filter action', InvalidFilterException::MISSING_ACTION);
207 }
208
209 if (in_array($data['action'], AutomationsEvents::SUPPORTED_ACTIONS)) {
210 if (
211 !isset($data['operator']) || !in_array($data['operator'], [
212 DynamicSegmentFilterData::OPERATOR_ANY,
213 DynamicSegmentFilterData::OPERATOR_ALL,
214 DynamicSegmentFilterData::OPERATOR_NONE,
215 ])
216 ) {
217 throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
218 }
219 if (
220 !isset($data['automation_ids'])
221 || !is_array($data['automation_ids'])
222 || count($data['automation_ids']) < 1
223 ) {
224 throw new InvalidFilterException('Missing automation IDs', InvalidFilterException::MISSING_VALUE);
225 }
226 return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_AUTOMATIONS, $data['action'], [
227 'action' => $data['action'],
228 'automation_ids' => $data['automation_ids'],
229 'operator' => $data['operator'],
230 'connect' => $data['connect'],
231 ]);
232 }
233
234 throw new InvalidFilterException('Unknown automations action', InvalidFilterException::MISSING_ACTION);
235 }
236
237 private function createSubscriber(array $data): DynamicSegmentFilterData {
238 if (empty($data['action'])) {
239 $data['action'] = DynamicSegmentFilterData::TYPE_USER_ROLE;
240 }
241 if ($data['action'] === SubscriberScore::TYPE) {
242 if (!isset($data['value'])) {
243 throw new InvalidFilterException('Missing engagement score value', InvalidFilterException::MISSING_VALUE);
244 }
245 return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
246 'value' => $data['value'],
247 'operator' => $data['operator'] ?? SubscriberScore::HIGHER_THAN,
248 'connect' => $data['connect'],
249 ]);
250 }
251 if ($data['action'] === SubscriberSegment::TYPE) {
252 if (empty($data['segments'])) {
253 throw new InvalidFilterException('Missing segments', InvalidFilterException::MISSING_VALUE);
254 }
255 return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
256 'segments' => array_map(function ($segmentId) {
257 return intval($segmentId);
258 }, $data['segments']),
259 'operator' => $data['operator'] ?? DynamicSegmentFilterData::OPERATOR_ANY,
260 'connect' => $data['connect'],
261 ]);
262 }
263 if ($data['action'] === MailPoetCustomFields::TYPE) {
264 if (empty($data['custom_field_id'])) {
265 throw new InvalidFilterException('Missing custom field id', InvalidFilterException::MISSING_VALUE);
266 }
267 if (empty($data['custom_field_type'])) {
268 throw new InvalidFilterException('Missing custom field type', InvalidFilterException::MISSING_VALUE);
269 }
270 if (!isset($data['value'])) {
271 throw new InvalidFilterException('Missing value', InvalidFilterException::MISSING_VALUE);
272 }
273 $filterData = [
274 'value' => $data['value'],
275 'custom_field_id' => $data['custom_field_id'],
276 'custom_field_type' => $data['custom_field_type'],
277 'connect' => $data['connect'],
278 ];
279 if (!empty($data['date_type'])) {
280 $filterData['date_type'] = $data['date_type'];
281 }
282 if (!empty($data['operator'])) {
283 $filterData['operator'] = $data['operator'];
284 }
285 return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], $filterData);
286 }
287 if ($data['action'] === SubscriberTag::TYPE) {
288 if (empty($data['tags'])) {
289 throw new InvalidFilterException('Missing tags', InvalidFilterException::MISSING_VALUE);
290 }
291 return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
292 'tags' => array_map(function ($tagId) {
293 return intval($tagId);
294 }, $data['tags']),
295 'operator' => $data['operator'] ?? DynamicSegmentFilterData::OPERATOR_ANY,
296 'connect' => $data['connect'],
297 ]);
298 }
299 if ($data['action'] === SubscriberSubscribedViaForm::TYPE) {
300 if (!isset($data['form_ids']) || empty($data['form_ids'])) {
301 throw new InvalidFilterException('Missing at least one form ID', InvalidFilterException::MISSING_VALUE);
302 }
303 if (!isset($data['operator']) || !in_array($data['operator'], [DynamicSegmentFilterData::OPERATOR_ANY, DynamicSegmentFilterData::OPERATOR_NONE])) {
304 throw new InvalidFilterException('Missing valid operator', InvalidFilterException::MISSING_VALUE);
305 }
306 return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
307 'form_ids' => array_map(function($formId) {
308 return intval($formId);
309 }, $data['form_ids']),
310 'operator' => $data['operator'],
311 'connect' => $data['connect'],
312 ]);
313 }
314 if (in_array($data['action'], SubscriberTextField::TYPES)) {
315 if (empty($data['value'])) {
316 throw new InvalidFilterException('Missing value', InvalidFilterException::MISSING_VALUE);
317 }
318 if (empty($data['operator'])) {
319 throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
320 }
321 if (!in_array($data['operator'], DynamicSegmentFilterData::TEXT_FIELD_OPERATORS)) {
322 throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR);
323 }
324 return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
325 'value' => $data['value'],
326 'operator' => $data['operator'],
327 'action' => $data['action'],
328 'connect' => $data['connect'],
329 ]);
330 }
331 if (in_array($data['action'], SubscriberDateField::TYPES)) {
332 if (empty($data['value'])) {
333 throw new InvalidFilterException('Missing date value', InvalidFilterException::MISSING_VALUE);
334 }
335 if (empty($data['operator'])) {
336 throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
337 }
338 if (!in_array($data['operator'], $this->dateFilterHelper->getValidOperators())) {
339 throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR);
340 }
341 $filterData = [
342 'value' => $data['value'],
343 'operator' => $data['operator'],
344 'connect' => $data['connect'],
345 ];
346 if ($data['operator'] === DateFilterHelper::BETWEEN) {
347 $betweenData = [
348 'timeframe' => DynamicSegmentFilterData::TIMEFRAME_BETWEEN,
349 'value' => $filterData['value'],
350 'value2' => $data['value2'] ?? null,
351 ];
352 if (is_string($betweenData['value']) && is_string($betweenData['value2'])) {
353 [$betweenData['value'], $betweenData['value2']] = $this->orderDateRange($betweenData['value'], $betweenData['value2']);
354 }
355 try {
356 $this->filterHelper->validateDaysPeriodData($betweenData);
357 } catch (InvalidFilterException $e) {
358 throw $e;
359 } catch (\Throwable $e) {
360 throw new InvalidFilterException('Invalid date value', InvalidFilterException::INVALID_DATE_VALUE);
361 }
362 $filterData['value'] = $betweenData['value'];
363 $filterData['value2'] = $betweenData['value2'];
364 } elseif (isset($data['value2'])) {
365 $filterData['value2'] = $data['value2'];
366 }
367 return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], $filterData);
368 }
369 if (empty($data['wordpressRole'])) {
370 throw new InvalidFilterException('Missing role', InvalidFilterException::MISSING_ROLE);
371 }
372 return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
373 'wordpressRole' => $data['wordpressRole'],
374 'operator' => $data['operator'] ?? DynamicSegmentFilterData::OPERATOR_ANY,
375 'connect' => $data['connect'],
376 ]);
377 }
378
379 /**
380 * @throws InvalidFilterException
381 */
382 private function createEmail(array $data): DynamicSegmentFilterData {
383 if (empty($data['action'])) {
384 throw new InvalidFilterException('Missing action', InvalidFilterException::MISSING_ACTION);
385 }
386 if (!in_array($data['action'], EmailAction::ALLOWED_ACTIONS)) {
387 throw new InvalidFilterException('Invalid email action', InvalidFilterException::INVALID_EMAIL_ACTION);
388 }
389 if (
390 ($data['action'] === EmailOpensAbsoluteCountAction::TYPE)
391 || ($data['action'] === EmailOpensAbsoluteCountAction::MACHINE_TYPE)
392 ) {
393 return $this->createEmailOpensAbsoluteCount($data);
394 }
395 if ($data['action'] === EmailActionClickAny::TYPE) {
396 return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_EMAIL, $data['action'], [
397 'connect' => $data['connect'],
398 ]);
399 }
400
401 $filterData = [
402 'connect' => $data['connect'],
403 'operator' => $data['operator'] ?? DynamicSegmentFilterData::OPERATOR_ANY,
404 ];
405
406 if ($data['action'] === EmailsReceived::ACTION) {
407 if (!isset($data['emails'])) {
408 throw new InvalidFilterException('Missing email count value', InvalidFilterException::MISSING_VALUE);
409 }
410 $filterData['emails'] = $data['emails'];
411 $filterData['operator'] = $data['operator'];
412 $filterData['connect'] = $data['connect'];
413 $this->copyDatePeriodData($data, $filterData);
414 return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_EMAIL, $data['action'], $filterData);
415 }
416
417 if ($data['action'] === NumberOfClicks::ACTION) {
418 if (!isset($data['clicks'])) {
419 throw new InvalidFilterException('Missing click count value', InvalidFilterException::MISSING_VALUE);
420 }
421 $filterData['clicks'] = $data['clicks'];
422 $filterData['operator'] = $data['operator'];
423 $filterData['connect'] = $data['connect'];
424 $this->copyDatePeriodData($data, $filterData);
425 return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_EMAIL, $data['action'], $filterData);
426 }
427
428 if (($data['action'] === EmailAction::ACTION_CLICKED)) {
429 if (empty($data['newsletter_id'])) {
430 throw new InvalidFilterException('Missing newsletter id', InvalidFilterException::MISSING_NEWSLETTER_ID);
431 }
432 $filterData['newsletter_id'] = $data['newsletter_id'];
433 } else {
434 if (empty($data['newsletters']) || !is_array($data['newsletters'])) {
435 throw new InvalidFilterException('Missing newsletter', InvalidFilterException::MISSING_NEWSLETTER_ID);
436 }
437 $filterData['newsletters'] = array_values(
438 array_map('intval', array_filter($data['newsletters'], 'is_scalar'))
439 );
440 }
441
442 $filterType = DynamicSegmentFilterData::TYPE_EMAIL;
443 $action = $data['action'];
444 if (isset($data['link_ids']) && is_array($data['link_ids'])) {
445 $filterData['link_ids'] = $this->normalizeEmailLinkIds($data['link_ids']);
446 if (!isset($data['operator'])) {
447 throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
448 }
449 $filterData['operator'] = $data['operator'];
450 }
451 return new DynamicSegmentFilterData($filterType, $action, $filterData);
452 }
453
454 private function normalizeEmailLinkIds(array $linkIds): array {
455 $normalizedLinkIds = [];
456 foreach ($linkIds as $linkId) {
457 if (is_int($linkId)) {
458 $normalizedLinkIds[] = $linkId;
459 continue;
460 }
461 if (is_float($linkId) && floor($linkId) === $linkId) {
462 $normalizedLinkIds[] = (int)$linkId;
463 continue;
464 }
465 if (!is_string($linkId)) {
466 continue;
467 }
468 $linkId = trim($linkId);
469 if ($linkId === '') {
470 continue;
471 }
472 $normalizedLinkIds[] = ctype_digit($linkId) ? (int)$linkId : $linkId;
473 }
474 return $normalizedLinkIds;
475 }
476
477 /**
478 * @throws InvalidFilterException
479 */
480 private function createEmailOpensAbsoluteCount(array $data): DynamicSegmentFilterData {
481 if (!isset($data['opens'])) {
482 throw new InvalidFilterException('Missing number of opens', InvalidFilterException::MISSING_VALUE);
483 }
484 $filterData = [
485 'opens' => $data['opens'],
486 'operator' => $data['operator'] ?? 'more',
487 'connect' => $data['connect'],
488 ];
489 $this->copyDatePeriodData($data, $filterData);
490 $filterType = DynamicSegmentFilterData::TYPE_EMAIL;
491 $action = $data['action'];
492 return new DynamicSegmentFilterData($filterType, $action, $filterData);
493 }
494
495 /**
496 * @throws InvalidFilterException
497 */
498 private function createWooCommerce(array $data): DynamicSegmentFilterData {
499 if (empty($data['action'])) {
500 throw new InvalidFilterException('Missing action', InvalidFilterException::MISSING_ACTION);
501 }
502 $filterData = [
503 'connect' => $data['connect'],
504 ];
505 $filterType = DynamicSegmentFilterData::TYPE_WOOCOMMERCE;
506 $action = $data['action'];
507 if ($data['action'] === WooCommerceCategory::ACTION_CATEGORY) {
508 if (!isset($data['category_ids'])) {
509 throw new InvalidFilterException('Missing category', InvalidFilterException::MISSING_CATEGORY_ID);
510 }
511 if (!isset($data['operator'])) {
512 throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
513 }
514 $filterData['operator'] = $data['operator'];
515 $filterData['category_ids'] = $data['category_ids'];
516 $this->copyDatePeriodData($data, $filterData, DynamicSegmentFilterData::TIMEFRAME_ALL_TIME);
517 } elseif ($data['action'] === WooCommerceProduct::ACTION_PRODUCT) {
518 if (!isset($data['product_ids'])) {
519 throw new InvalidFilterException('Missing product', InvalidFilterException::MISSING_PRODUCT_ID);
520 }
521 if (!isset($data['operator'])) {
522 throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
523 }
524 $filterData['operator'] = $data['operator'];
525 $filterData['product_ids'] = $data['product_ids'];
526 $this->copyDatePeriodData($data, $filterData, DynamicSegmentFilterData::TIMEFRAME_ALL_TIME);
527 } elseif ($data['action'] === WooCommerceProductVariation::ACTION_PRODUCT_VARIATION) {
528 if (!isset($data['variation_ids']) || !is_array($data['variation_ids']) || count($data['variation_ids']) === 0) {
529 throw new InvalidFilterException('Missing variation', InvalidFilterException::MISSING_VARIATION_ID);
530 }
531 if (
532 !isset($data['operator']) || !in_array($data['operator'], [
533 DynamicSegmentFilterData::OPERATOR_ANY,
534 DynamicSegmentFilterData::OPERATOR_ALL,
535 DynamicSegmentFilterData::OPERATOR_NONE,
536 ], true)
537 ) {
538 throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
539 }
540 $filterData['operator'] = $data['operator'];
541 $filterData['variation_ids'] = $data['variation_ids'];
542 $this->copyDatePeriodData($data, $filterData, DynamicSegmentFilterData::TIMEFRAME_ALL_TIME);
543 } elseif ($data['action'] === WooCommerceCountry::ACTION_CUSTOMER_COUNTRY) {
544 if (!isset($data['country_code'])) {
545 throw new InvalidFilterException('Missing country', InvalidFilterException::MISSING_COUNTRY);
546 }
547 $filterData['country_code'] = $data['country_code'];
548 $filterData['operator'] = $data['operator'] ?? DynamicSegmentFilterData::OPERATOR_ANY;
549 } elseif (in_array($data['action'], WooCommerceNumberOfOrders::ACTIONS)) {
550 if (
551 !isset($data['number_of_orders_type'])
552 || !isset($data['number_of_orders_count']) || $data['number_of_orders_count'] < 0
553 ) {
554 throw new InvalidFilterException('Missing required fields', InvalidFilterException::MISSING_NUMBER_OF_ORDERS_FIELDS);
555 }
556 $filterData['number_of_orders_type'] = $data['number_of_orders_type'];
557 $filterData['number_of_orders_count'] = $data['number_of_orders_count'];
558 $this->copyDatePeriodData($data, $filterData);
559 } elseif ($data['action'] === WooCommerceNumberOfReviews::ACTION) {
560 $this->wooCommerceNumberOfReviews->validateFilterData($data);
561 $filterData['count_type'] = $data['count_type'];
562 $filterData['count'] = $data['count'];
563 $filterData['rating'] = $data['rating'];
564 $this->copyDatePeriodData($data, $filterData);
565 } elseif ($data['action'] === WooCommerceTotalSpent::ACTION_TOTAL_SPENT) {
566 if (
567 !isset($data['total_spent_type'])
568 || !isset($data['total_spent_amount']) || $data['total_spent_amount'] < 0
569 ) {
570 throw new InvalidFilterException('Missing required fields', InvalidFilterException::MISSING_TOTAL_SPENT_FIELDS);
571 }
572 $filterData['total_spent_type'] = $data['total_spent_type'];
573 $filterData['total_spent_amount'] = $data['total_spent_amount'];
574 $this->copyDatePeriodData($data, $filterData);
575 } elseif ($data['action'] === WooCommerceSingleOrderValue::ACTION_SINGLE_ORDER_VALUE) {
576 if (
577 !isset($data['single_order_value_type'])
578 || !isset($data['single_order_value_amount']) || $data['single_order_value_amount'] < 0
579 ) {
580 throw new InvalidFilterException('Missing required fields', InvalidFilterException::MISSING_SINGLE_ORDER_VALUE_FIELDS);
581 }
582 $filterData['single_order_value_type'] = $data['single_order_value_type'];
583 $filterData['single_order_value_amount'] = $data['single_order_value_amount'];
584 $this->copyDatePeriodData($data, $filterData);
585 } elseif (in_array($data['action'], [WooCommercePurchaseDate::ACTION, WooCommerceFirstOrder::ACTION])) {
586 $filterData['operator'] = $data['operator'];
587 $filterData['value'] = $data['value'];
588 if ($data['operator'] === DateFilterHelper::BETWEEN) {
589 $betweenData = [
590 'timeframe' => DynamicSegmentFilterData::TIMEFRAME_BETWEEN,
591 'value' => $filterData['value'],
592 'value2' => $data['value2'] ?? null,
593 ];
594 if (is_string($betweenData['value']) && is_string($betweenData['value2'])) {
595 [$betweenData['value'], $betweenData['value2']] = $this->orderDateRange($betweenData['value'], $betweenData['value2']);
596 }
597 try {
598 $this->filterHelper->validateDaysPeriodData($betweenData);
599 } catch (InvalidFilterException $e) {
600 throw $e;
601 } catch (\Throwable $e) {
602 throw new InvalidFilterException('Invalid date value', InvalidFilterException::INVALID_DATE_VALUE);
603 }
604 $filterData['value'] = $betweenData['value'];
605 $filterData['value2'] = $betweenData['value2'];
606 }
607 } elseif ($data['action'] === WooCommerceAverageSpent::ACTION) {
608 if (
609 !isset($data['average_spent_type'])
610 || !isset($data['average_spent_amount']) || $data['average_spent_amount'] < 0
611 ) {
612 throw new InvalidFilterException('Missing required fields', InvalidFilterException::MISSING_AVERAGE_SPENT_FIELDS);
613 }
614 $this->copyDatePeriodData($data, $filterData);
615 $filterData['average_spent_amount'] = $data['average_spent_amount'];
616 $filterData['average_spent_type'] = $data['average_spent_type'];
617 } elseif ($data['action'] === WooCommerceUsedPaymentMethod::ACTION) {
618 if (!isset($data['operator']) || !in_array($data['operator'], WooCommerceUsedPaymentMethod::VALID_OPERATORS, true)) {
619 throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
620 }
621 if (!isset($data['payment_methods']) || !is_array($data['payment_methods']) || empty($data['payment_methods'])) {
622 throw new InvalidFilterException('Missing payment gateways', InvalidFilterException::MISSING_VALUE);
623 }
624 $filterData['operator'] = $data['operator'];
625 $filterData['payment_methods'] = $data['payment_methods'];
626 $this->copyDatePeriodData($data, $filterData);
627 } elseif ($data['action'] === WooCommerceUsedShippingMethod::ACTION) {
628 if (!isset($data['operator']) || !in_array($data['operator'], WooCommerceUsedShippingMethod::VALID_OPERATORS, true)) {
629 throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
630 }
631 if (!isset($data['shipping_methods']) || !is_array($data['shipping_methods']) || empty($data['shipping_methods'])) {
632 throw new InvalidFilterException('Missing shipping methods', InvalidFilterException::MISSING_VALUE);
633 }
634 $filterData['operator'] = $data['operator'];
635 $filterData['shipping_methods'] = $data['shipping_methods'];
636 $this->copyDatePeriodData($data, $filterData);
637 } elseif (in_array($data['action'], WooCommerceCustomerTextField::ACTIONS)) {
638 if (empty($data['value'])) {
639 throw new InvalidFilterException('Missing value', InvalidFilterException::MISSING_VALUE);
640 }
641 if (empty($data['operator'])) {
642 throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
643 }
644 if (!in_array($data['operator'], DynamicSegmentFilterData::TEXT_FIELD_OPERATORS)) {
645 throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR);
646 }
647 $filterData['value'] = $data['value'];
648 $filterData['operator'] = $data['operator'];
649 $filterData['action'] = $data['action'];
650 } elseif ($data['action'] === WooCommerceUsedCouponCode::ACTION) {
651 $this->wooCommerceUsedCouponCode->validateFilterData($data);
652 $filterData['operator'] = $data['operator'];
653 $filterData['coupon_code_ids'] = $data['coupon_code_ids'];
654 $this->copyDatePeriodData($data, $filterData);
655 } elseif ($data['action'] === WooCommercePurchasedWithAttribute::ACTION) {
656 $this->wooCommercePurchasedWithAttribute->validateFilterData($data);
657 $filterData['operator'] = $data['operator'];
658 $filterData['attribute_taxonomy_slug'] = $data['attribute_taxonomy_slug'] ?? null;
659 $filterData['attribute_term_ids'] = $data['attribute_term_ids'] ?? null;
660 $filterData['attribute_type'] = $data['attribute_type'];
661 $filterData['attribute_local_name'] = $data['attribute_local_name'] ?? null;
662 $filterData['attribute_local_values'] = $data['attribute_local_values'] ?? null;
663 $this->copyDatePeriodData($data, $filterData, DynamicSegmentFilterData::TIMEFRAME_ALL_TIME);
664 } elseif ($data['action'] === WooCommerceTag::ACTION) {
665 $this->wooCommerceTag->validateFilterData($data);
666 $filterData['operator'] = $data['operator'];
667 $filterData['tag_ids'] = $data['tag_ids'];
668 $this->copyDatePeriodData($data, $filterData, DynamicSegmentFilterData::TIMEFRAME_ALL_TIME);
669 } else {
670 throw new InvalidFilterException("Unknown action " . $data['action'], InvalidFilterException::MISSING_ACTION);
671 }
672 return new DynamicSegmentFilterData($filterType, $action, $filterData);
673 }
674
675 private function copyDatePeriodData(array $data, array &$filterData, string $defaultTimeframe = DynamicSegmentFilterData::TIMEFRAME_IN_THE_LAST): void {
676 $timeframe = $data['timeframe'] ?? $defaultTimeframe;
677 $datePeriodData = [
678 'timeframe' => $timeframe,
679 'days' => $data['days'] ?? 0,
680 ];
681 if (in_array($timeframe, [DynamicSegmentFilterData::TIMEFRAME_BEFORE, DynamicSegmentFilterData::TIMEFRAME_AFTER, DynamicSegmentFilterData::TIMEFRAME_ON, DynamicSegmentFilterData::TIMEFRAME_BETWEEN], true)) {
682 $datePeriodData['value'] = $data['value'] ?? null;
683 }
684 if ($timeframe === DynamicSegmentFilterData::TIMEFRAME_BETWEEN) {
685 $datePeriodData['value2'] = $data['value2'] ?? null;
686 if (is_string($datePeriodData['value']) && is_string($datePeriodData['value2'])) {
687 [$datePeriodData['value'], $datePeriodData['value2']] = $this->orderDateRange($datePeriodData['value'], $datePeriodData['value2']);
688 }
689 }
690 $this->filterHelper->validateDaysPeriodData($datePeriodData);
691 $filterData['timeframe'] = $datePeriodData['timeframe'];
692 $filterData['days'] = $timeframe === DynamicSegmentFilterData::TIMEFRAME_IN_THE_LAST ? $datePeriodData['days'] : 0;
693 if (isset($datePeriodData['value'])) {
694 $filterData['value'] = $datePeriodData['value'];
695 }
696 if (isset($datePeriodData['value2'])) {
697 $filterData['value2'] = $datePeriodData['value2'];
698 }
699 }
700
701 /**
702 * @return array{0: string, 1: string}
703 */
704 private function orderDateRange(string $value, string $value2): array {
705 return strcmp($value, $value2) <= 0 ? [$value, $value2] : [$value2, $value];
706 }
707
708 /**
709 * @throws InvalidFilterException
710 */
711 private function createWooCommerceMembership(array $data): DynamicSegmentFilterData {
712 if (empty($data['action'])) {
713 throw new InvalidFilterException('Missing action', InvalidFilterException::MISSING_ACTION);
714 }
715 $filterData = [
716 'connect' => $data['connect'],
717 ];
718 $filterType = DynamicSegmentFilterData::TYPE_WOOCOMMERCE_MEMBERSHIP;
719 $action = $data['action'];
720 if ($data['action'] === WooCommerceMembership::ACTION_MEMBER_OF) {
721 if (!isset($data['plan_ids']) || !is_array($data['plan_ids'])) {
722 throw new InvalidFilterException('Missing plan', InvalidFilterException::MISSING_PLAN_ID);
723 }
724 if (!isset($data['operator'])) {
725 throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
726 }
727 $filterData['operator'] = $data['operator'];
728 $filterData['plan_ids'] = $data['plan_ids'];
729 } else {
730 throw new InvalidFilterException("Unknown action " . $data['action'], InvalidFilterException::MISSING_ACTION);
731 }
732 return new DynamicSegmentFilterData($filterType, $action, $filterData);
733 }
734
735 /**
736 * @throws InvalidFilterException
737 */
738 private function createWooCommerceSubscription(array $data): DynamicSegmentFilterData {
739 if (empty($data['action'])) {
740 throw new InvalidFilterException('Missing action', InvalidFilterException::MISSING_ACTION);
741 }
742 $filterData = [
743 'connect' => $data['connect'],
744 ];
745 $filterType = DynamicSegmentFilterData::TYPE_WOOCOMMERCE_SUBSCRIPTION;
746 $action = $data['action'];
747 if ($data['action'] === WooCommerceSubscription::ACTION_HAS_ACTIVE) {
748 if (!isset($data['product_ids']) || !is_array($data['product_ids'])) {
749 throw new InvalidFilterException('Missing product', InvalidFilterException::MISSING_PRODUCT_ID);
750 }
751 if (!isset($data['operator'])) {
752 throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
753 }
754 $filterData['operator'] = $data['operator'];
755 $filterData['product_ids'] = $data['product_ids'];
756 } else {
757 throw new InvalidFilterException("Unknown action " . $data['action'], InvalidFilterException::MISSING_ACTION);
758 }
759 return new DynamicSegmentFilterData($filterType, $action, $filterData);
760 }
761 }
762