PluginProbe
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO / 1.4.21
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO v1.4.21
1.4.21 1.4.20 1.4.19 1.4.18 1.4.17 1.4.16 1.4.15 1.4.14 1.4.13 1.4.12 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.54 1.3.53 1.3.52 All 184 releases
disco / app / Utility / Conditions.php

Conditions.php in Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO 1.4.21, at app/Utility/Conditions.php

702 lines 19.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php // phpcs:ignore
2
3 /**
4 * Condition Utility
5 *
6 * @package Disco
7 * @subpackage App\Utility
8 * @since 1.0.0
9 * @category Utility
10 */
11
12 namespace Disco\App\Utility;
13
14 use Disco\App\Attributes\AttributeFactory;
15
16 /**
17 * Class Filter
18 *
19 * @package Disco
20 * @subpackage App\Utility
21 * @author Ohidul Islam <[email protected]>
22 * @link https://webappick.com
23 * @license https://opensource.org/licenses/gpl-license.php GNU Public License
24 * @category Utility
25 */
26 class Conditions { // phpcs:ignore
27
28 /**
29 * @var array $product Product Info.
30 */
31 private $product;
32
33 /**
34 * Contains the conditions array.
35 *
36 * @var array $conditions Filter object.
37 */
38 private $conditions;
39
40 /**
41 * Conditions constructor.
42 *
43 * @param \Disco\App\Utility\Config $config Campaign configuration.
44 * @param array $product Product Object.
45 */
46 public function __construct( $config, $product ) {
47 $this->conditions = $config->get_conditions();
48 $this->product = $product;
49 }
50
51 /**
52 * Check if the product is passed the filter
53 *
54 * @return bool
55 */
56 public function is_passed() {
57 if ( ! empty( $this->conditions ) ) {
58 return $this->check_all_conditions( $this->conditions );
59 }
60
61 return true;
62 }
63
64 /**
65 * Check if the product is passed the filter
66 *
67 * @param array $filters Filter array.
68 * array(
69 * array(
70 * 'base_operator' => 'and',
71 * 'base_filters' => array(
72 * 0 => array(
73 * 'filter' => 'id',
74 * 'operator' => 'and',
75 * 'condition' => 'between',
76 * 'compare' => array( 1, 100 ),
77 * ),
78 * 1 => array(
79 * 'filter' => 'Hat Wizard',
80 * 'operator' => 'and',
81 * 'condition' => 'contain',
82 * 'compare' => 'Hat',
83 * ),
84 * ),
85 * ),
86 * array(
87 * 'base_operator' => 'or',
88 * 'base_filters' => array(
89 * 0 => array(
90 * 'filter' => 'total_sold',
91 * 'operator' => 'and',
92 * 'condition' => 'greater_tan',
93 * 'compare' => 2000,
94 * ),
95 * 1 => array(
96 * 'filter' => 'total_order_name',
97 * 'operator' => 'or',
98 * 'condition' => 'equal',
99 * 'compare' => '100',
100 * ),
101 * ),
102 * array(
103 * 'base_operator' => 'and',
104 * 'base_filters' => array(
105 * 0 => array(
106 * 'filter' => 'total_sold',
107 * 'operator' => 'and',
108 * 'condition' => 'greater_tan',
109 * 'compare' => 2000,
110 * ),
111 * 2 => array(
112 * 'filter' => 'title',
113 * 'operator' => 'and',
114 * 'condition' => 'not_equal',
115 * 'compare' => '',
116 * ),
117 * ),
118 * ),
119 * ),.
120 * @return bool
121 */
122 public function check_group_conditions( $filters ) {// phpcs:ignore
123
124 $result = true;
125 // Start with true, will be updated based on conditions
126 $first = true;
127
128 foreach ( $filters as $filter ) {
129 if ( is_array( $filter ) ) {
130 $filter = (object) $filter;
131 }
132
133 if ( strtolower( $filter->operator ) ) {
134 $operator = strtolower( $filter->operator );
135 } else {
136 $operator = 'and';
137 }
138
139 $condition = $this->compare_value( $filter );
140
141 // Apply the operator
142 if ( $first ) {
143 $result = $condition;
144 $first = false;
145 } elseif ( $operator === 'and' ) {
146 $result = $result && $condition;// phpcs:ignore
147 } elseif ( $operator === 'or' ) {
148 $result = $result || $condition;// phpcs:ignore
149 }
150 }//end foreach
151
152 return $result;
153 }
154
155 /**
156 * Compare value based on a type
157 *
158 * @param object $filter Filter object.
159 * @return bool
160 */
161 public function compare_value( $filter ) {
162 if ( ! is_object( $filter ) || ! isset( $filter->condition, $filter->compare, $filter->compare_with ) ) {
163 return false;
164 }
165
166 /**
167 * Special case for item_quantity and item_count
168 * If the compare_with is item_quantity or item_count,
169 * we need to get the matched products from the cart
170 * and then compare the quantity or count of those products.
171 * This is because item_quantity and item_count are not product attributes,
172 */
173 if ( in_array( $filter->compare_with, array( 'item_quantity', 'item_count' ), true ) ) {
174 $matched_products = Helper::get_matched_conditions_cart_products( $this->conditions, $this->product );
175
176 $info = array(
177 'matched_products' => $matched_products,
178 );
179
180 return $this->compare_special_case( $filter, $info );
181 }
182
183 $type = $this->get_type( $filter->compare, $filter->condition );
184
185 switch ( $type ) {
186 case 'date':
187 return $this->date_compare( $filter );
188
189 case 'array':
190 return $this->array_compare( $filter );
191
192 case 'integer':
193 case 'double':
194 return $this->number_compare( $filter );
195
196 default:
197 return $this->string_compare( $filter );
198 }
199 }
200
201 /**
202 * Get the type of the compare value
203 *
204 * @param mixed $compare Compare value.
205 * @param string $condition Condition.
206 * @return string
207 */
208 public function get_type( $compare, $condition ) { // phpcs:ignore
209 // Get $compare variable value type
210 $type = 'string';
211
212 // If $compare is a number, check if it is an integer or double
213 if ( is_numeric( $compare ) ) {
214 $type = 'integer';
215 }
216
217 // If $compare is a date, check if it is a valid date
218 if ( !is_numeric( $compare ) && is_string( $compare ) && strtotime( $compare ) ) {
219 $type = 'date';
220 }
221
222 // If $condition is an array, check if it is an array of strings
223 if ( in_array( $condition, array( 'in_list', 'not_in_list' ), true ) ) {
224 $type = 'array';
225 }
226
227 // If $compare is an array, check if it is a number range. Only for between condition.
228 if ( $condition === 'between' && is_array( $compare ) && count( $compare ) === 2 ) {
229 $type = 'integer';
230 }
231
232 // If $compare is an array, check if it is a date range. Only for date between condition.
233 if ( $condition === 'date_between'
234 && is_array( $compare )
235 && count( $compare ) === 2
236 && strtotime( $compare[0] )
237 && strtotime( $compare[1] )
238 ) {
239 $type = 'date';
240 }
241
242 if ( is_bool( $compare ) ) {
243 $type = 'boolean';
244 }
245
246 return $type;
247 }
248
249 /**
250 * Compare value for a date type.
251 *
252 * @param object $filter Filter object.
253 * @return bool
254 */
255 public function date_compare( $filter ) {
256 if ( ! is_object( $filter ) || ! isset( $filter->condition, $filter->compare, $filter->compare_with ) ) {
257 return false;
258 }
259
260 $condition = $filter->condition;
261 $compare = $filter->compare;
262 $compare_with = AttributeFactory::get_value( $filter->compare_with, $this->product );
263
264 switch ( $condition ) {
265 case 'equal':
266 return $compare === $compare_with;
267
268 case 'not_equal':
269 return $compare !== $compare_with;
270
271 case 'greater':
272 return $compare_with > $compare;
273
274 case 'greater_equal':
275 return $compare_with >= $compare;
276
277 case 'lesser':
278 return $compare_with < $compare;
279
280 case 'lesser_equal':
281 return $compare_with <= $compare;
282
283 case 'date_between':
284 return $compare_with >= $compare[0] && $compare_with <= $compare[1];
285
286 default:
287 return false;
288 }//end switch
289 }
290
291 /**
292 * Compare value for an array type.
293 *
294 * @param object $filter Filter object.
295 * @return bool
296 */
297 public function array_compare( $filter ) {// phpcs:ignore
298 if ( ! is_object( $filter ) || ! isset( $filter->condition, $filter->compare, $filter->compare_with ) ) {
299 return false;
300 }
301
302 $condition = $filter->condition;
303 $compare = $filter->compare;
304
305 // Is compare_with needs to be compared with id or name
306 $column = $this->get_array_compare_column( $filter->compare_with );
307
308 if ( isset( $compare[0] ) && is_object( $compare[0] ) ) {
309 $compare = array_column( (array) $compare, $column );
310 } elseif ( isset( $compare[0] ) && is_array( $compare[0] ) ) {
311 $compare = array_column( $compare, $column );
312 }
313
314 $compare_with = AttributeFactory::get_value( $filter->compare_with, $this->product );
315
316 if ( is_array( $compare ) ) {
317 $compare = array_map( 'trim', $compare ); // Trim all values in the array
318 }
319
320 if ( is_array( $compare_with ) ) {
321 $compare_with = array_map( 'trim', $compare_with ); // Trim all values in the array
322 }
323
324 switch ( $condition ) {
325 case 'not_in_list':
326 if ( is_array( $compare ) && is_array( $compare_with ) ) {
327 // Check if there are no common elements between the two arrays
328 return !count( array_intersect( $compare, $compare_with ) );
329 }
330
331 if ( is_array( $compare_with ) ) {
332 // Check if a scalar $compare is not in the array $compare_with
333 return !in_array( $compare, $compare_with, true );
334 }
335
336 if ( is_array( $compare ) && is_string( $compare_with ) ) {
337 // Check if a string $compare_with is not in the array $compare
338 return !in_array( $compare_with, $compare, true );
339 }
340
341 // A single stored value against a single option: without this both
342 // list conditions fell through and returned false either way.
343 if ( is_string( $compare ) && is_string( $compare_with ) ) {
344 return $compare !== $compare_with;
345 }
346
347 return false;
348
349 case 'in_list':
350 // return $compare;
351 // Ensure $compare[0] is an array before calling array_intersect
352 if ( is_array( $compare ) && is_array( $compare_with ) ) {
353 // Extract IDs from $compare if $compare_with is an array of IDs
354 return (bool) count( array_intersect( $compare, $compare_with ) );
355 }
356
357 if ( is_array( $compare_with ) && is_string( $compare ) ) {
358 // Check if a scalar $compare is in the array $compare_with
359 return in_array( $compare, $compare_with, true );
360 }
361
362 if ( is_array( $compare ) && is_string( $compare_with ) ) {
363 // Check if a string $compare_with is in the array $compare
364 return in_array( $compare_with, $compare, true );
365 }
366
367 // A single stored value against a single option.
368 if ( is_string( $compare ) && is_string( $compare_with ) ) {
369 return $compare === $compare_with;
370 }
371
372 return false;
373
374 default:
375 return false;
376 }
377 }
378
379 /**
380 * Compare value for a number type.
381 *
382 * @param object $filter Filter object.
383 * @return bool
384 */
385 public function number_compare( $filter ) {// phpcs:ignore
386 if ( ! is_object( $filter ) || ! isset( $filter->condition, $filter->compare, $filter->compare_with ) ) {
387 return false;
388 }
389
390 $condition = $filter->condition;
391 $compare = is_array( $filter->compare ) ? array_map(
392 static function( $val ) {
393 return (float) $val;
394 },
395 $filter->compare
396 ) : (float) $filter->compare;
397 $compare_with = (float) AttributeFactory::get_value( $filter->compare_with, $this->product );
398
399 switch ( $condition ) {
400 case 'equal':
401 return $compare === $compare_with;
402
403 case 'not_equal':
404 return $compare !== $compare_with;
405
406 case 'greater':
407 return $compare_with > $compare;
408
409 case 'greater_equal':
410 return $compare_with >= $compare;
411
412 case 'lesser':
413 return $compare_with < $compare;
414
415 case 'lesser_equal':
416 return $compare_with <= $compare;
417
418 case 'between':
419 // @phpstan-ignore-next-line
420 return $compare_with >= $compare[0] && $compare_with <= $compare[1];
421
422 default:
423 return false;
424 }//end switch
425 }
426
427 /**
428 * Compare value for a string type.
429 *
430 * @param object $filter Filter object.
431 * @return bool
432 */
433 public function string_compare( $filter ) {
434 if ( ! is_object( $filter ) || ! isset( $filter->condition, $filter->compare, $filter->compare_with ) ) {
435 return false;
436 }
437
438 $condition = $filter->condition;
439 $compare = $filter->compare;
440 $compare_with = AttributeFactory::get_value( $filter->compare_with, $this->product );
441
442 if ( is_array( $compare ) ) {
443 $compare = wp_json_encode( $compare );
444 }
445
446 if ( !is_string( $compare ) ) {
447 $compare = (string) $compare;
448 }
449
450 /**
451 * A field can hold more than one value.
452 *
453 * An ACF checkbox group, or any multi select, stores an array. Which
454 * comparison runs is decided by {@see self::get_type()} from the value
455 * the merchant typed and the condition, never from what the product
456 * side holds, so a plain "contains red" rule arrives here even when the
457 * stored value is an array. Handing that array to stripos(), strtolower()
458 * or substr() is fatal on PHP 8.
459 *
460 * Each stored value is compared in turn: a positive condition passes
461 * when any of them matches, a negative one only when none do.
462 */
463 $matched = false;
464
465 foreach ( $this->comparable_values( $compare_with ) as $haystack ) {
466 if ( $this->matches_string_condition( $haystack, $compare, $condition ) ) {
467 $matched = true;
468
469 break;
470 }
471 }
472
473 if ( in_array( $condition, array( 'not_equal', 'not_contain' ), true ) ) {
474 return ! $matched;
475 }
476
477 return $matched;
478 }
479
480 /**
481 * @param array $filters Filter array.
482 * @return bool
483 */
484 public function check_all_conditions( $filters ) {
485 $final_result = true;
486 $first_group = true;
487
488 if ( empty( $filters ) ) {
489 return true;
490 }
491
492 foreach ( $filters as $group ) {
493 if ( is_array( $group ) ) {
494 $group = (object) $group;
495 }
496
497 $group_result = $this->check_group_conditions( $group->base_filters );
498
499 if ( $first_group ) {
500 $final_result = $group_result;
501 $first_group = false;
502 } elseif ( $group->base_operator === 'and' ) {
503 $final_result = $final_result && $group_result; // phpcs:ignore
504 } elseif ( $group->base_operator === 'or' ) {
505 $final_result = $final_result || $group_result; // phpcs:ignore
506 }
507 }
508
509 return $final_result;
510 }
511
512 /**
513 * Get the column for array compare
514 * Sometime we need to get the column id for array compare.
515 * For example, we need to compare the product id with the array of product ids
516 *
517 * @param string $compare_with Compare with value.
518 * @return string
519 */
520 public function get_array_compare_column( $compare_with ) {
521 $attributes_by_ids = array(
522 'attributes', // Product Attributes
523 'customer_country', // Customer Country
524 'customer_state', // Customer State
525 'products_in_cart', // Products in Cart
526 'customer_history_total_order_made_by_ids', // Number of Orders Made with the Following Products
527 'customer_history_total_amount_sold_by_ids', // Total Amount Sold with the Following Products
528 'customer_history_total_quantity_sold_by_ids', // Number of Orders Made with the Following Products
529 );
530
531 if ( in_array( $compare_with, $attributes_by_ids, true ) ) {
532 return 'id';
533 }
534
535 /**
536 * ACF stores the choice value, while the option's name is its label, so
537 * matching on the name would compare "Red" against a stored "red".
538 */
539 if ( is_string( $compare_with ) && strpos( $compare_with, 'acf_fields_' ) === 0 ) {
540 return 'id';
541 }
542
543 return 'name';
544 }
545
546 /**
547 * Compare special cases for product filters, such as item quantity or item count.
548 *
549 * This function evaluates a filter condition (e.g., equal, greater, between) against either the total quantity
550 * of matched products (when comparing with 'item_quantity') or the count of matched products (when comparing with 'item_count').
551 *
552 * @param object $filter Filter object containing 'compare_with', 'condition', and 'compare' properties.
553 * @param array $info Array containing 'matched_products' (list of product data arrays).
554 * @return bool True if the condition is met, false otherwise.
555 */
556 public static function compare_special_case( $filter, $info ) { // phpcs:ignore
557 $matched_products = $info['matched_products'] ?? array();
558
559 if (
560 ! is_object( $filter ) ||
561 ! property_exists( $filter, 'compare_with' ) ||
562 ! property_exists( $filter, 'compare' ) ||
563 ! property_exists( $filter, 'condition' )
564 ) {
565 return false;
566 }
567
568 if ( $filter->compare_with === 'item_quantity' ) {
569 $total_quantity = array_sum(
570 array_map(
571 function( $item ) {
572 if ( isset( $item['quantity'] ) ) {
573 return $item['quantity'];
574 }
575
576 return 0;
577 },
578 $matched_products
579 )
580 );
581
582 $compare_value = $total_quantity;
583 } elseif ( $filter->compare_with === 'item_count' ) {
584 $compare_value = count( $matched_products );
585 } else {
586 return false;
587 }
588
589 $condition = $filter->condition;
590
591 if ( is_array( $filter->compare ) ) {
592 $target = (int) $filter->compare[0];
593 } else {
594 $target = (int) $filter->compare;
595 }
596
597 switch ( $condition ) {
598 case 'equal':
599 return $compare_value === $target;
600
601 case 'not_equal':
602 return $compare_value !== $target;
603
604 case 'greater':
605 return $compare_value > $target;
606
607 case 'greater_equal':
608 return $compare_value >= $target;
609
610 case 'lesser':
611 return $compare_value < $target;
612
613 case 'lesser_equal':
614 return $compare_value <= $target;
615
616 case 'between':
617 if ( is_array( $filter->compare ) && count( $filter->compare ) === 2 ) {
618 return $compare_value >= $filter->compare[0] && $compare_value <= $filter->compare[1];
619 }
620
621 return false;
622
623 default:
624 return false;
625 }
626 }
627
628 /**
629 * Run one string condition against a single stored value.
630 *
631 * The negative conditions share the comparison of their positive
632 * counterpart; {@see self::string_compare()} inverts the result once every
633 * stored value has been checked.
634 *
635 * @param string $haystack One stored value.
636 * @param string $compare Value the merchant typed.
637 * @param string $condition Condition name.
638 */
639 private function matches_string_condition( string $haystack, string $compare, string $condition ): bool {
640 switch ( $condition ) {
641 case 'equal':
642 case 'not_equal':
643 return strtolower( $haystack ) === strtolower( $compare );
644
645 case 'contain':
646 case 'not_contain':
647 return stripos( $haystack, $compare ) !== false;
648
649 case 'start_with':
650 return stripos( $haystack, $compare ) === 0;
651
652 case 'end_with':
653 return substr( $haystack, -strlen( $compare ) ) === $compare;
654
655 default:
656 return false;
657 }//end switch
658 }
659
660 /**
661 * Flatten an attribute value into the strings a comparison can run against.
662 *
663 * A single value yields one string, a multi value field one per entry.
664 * Anything that cannot be expressed as text, a nested array or an object,
665 * is dropped. An empty result still yields one empty string, so a negative
666 * condition has something to fail against rather than passing by default.
667 *
668 * @param mixed $value Attribute value.
669 * @return array<int, string>
670 */
671 private function comparable_values( $value ): array {
672 $values = array( $value );
673
674 if ( is_array( $value ) ) {
675 $values = $value;
676 }
677
678 $result = array();
679
680 foreach ( $values as $entry ) {
681 if ( null === $entry ) {
682 $result[] = '';
683
684 continue;
685 }
686
687 if ( ! is_scalar( $entry ) ) {
688 continue;
689 }
690
691 $result[] = (string) $entry;
692 }
693
694 if ( empty( $result ) ) {
695 return array( '' );
696 }
697
698 return $result;
699 }
700
701 }
702