PluginProbe
Ultimate Store Kit – Store Builder Addons for Elementor, WooCommerce Store Builder, EDD Store Builder / 3.1.4
Ultimate Store Kit – Store Builder Addons for Elementor, WooCommerce Store Builder, EDD Store Builder v3.1.4
3.1.4 3.0.8 3.0.9 3.1.0 3.1.2 3.1.3 3.0.7 3.0.5 3.0.4 3.0.3 3.0.2 trunk 1.5.0 1.5.1 1.5.2 1.6.1 1.6.2 1.6.3 1.6.4 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 All 93 releases
ultimate-store-kit / includes / controls / group-query / group-control-query.php

group-control-query.php in Ultimate Store Kit – Store Builder Addons for Elementor, WooCommerce Store Builder, EDD Store Builder 3.1.4, at includes/controls/group-query/group-control-query.php

1,019 lines 28.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace UltimateStoreKit\Includes\Controls\GroupQuery;
4
5 use Elementor\Controls_Manager;
6 use UltimateStoreKit\Includes\Controls\SelectInput\Dynamic_Select;
7
8 if (! defined('ABSPATH')) {
9 exit; // Exit if accessed directly.
10 }
11
12 // phpcs:disable WordPressVIPMinimum.Performance.WPQueryParams -- WordPressVIPMinimum targets the VIP platform, not the plugin directory. These exclusionary parameters come from a widget's own "exclude" control: the list is whatever the site owner picked in Elementor, applied to a bounded result set, not an unbounded catalogue scan.
13 // phpcs:disable WordPress.DB.SlowDBQuery -- meta_query / tax_query / meta_key are the documented way to express these widget filters. Removing them means fetching every post and filtering in PHP, which is strictly slower.
14
15 trait Group_Control_Query
16 {
17
18 public function register_query_builder_controls()
19 {
20
21 $this->add_control(
22 'product_source',
23 [
24 'label' => __('Source', 'ultimate-store-kit'),
25 'type' => Controls_Manager::SELECT,
26 'options' => $this->getGroupControlQueryPostTypes(),
27 'default' => 'product',
28 ]
29 );
30 $this->add_control(
31 'product_limit',
32 [
33 'label' => esc_html__('Product Limit', 'ultimate-store-kit'),
34 'type' => Controls_Manager::NUMBER,
35 'default' => 9,
36 ]
37 );
38
39 $this->add_control(
40 'product_selected_ids',
41 [
42 'label' => __('Search & Select', 'ultimate-store-kit'),
43 'type' => Dynamic_Select::TYPE,
44 'multiple' => true,
45 'label_block' => true,
46 'query_args' => [
47 'query' => 'posts',
48 'post_type' => 'product',
49 ],
50 'condition' => [
51 'product_source' => 'manual_selection',
52 ]
53 ]
54 );
55
56 $this->start_controls_tabs(
57 'tabs_product_include_exclude',
58 [
59 'condition' => [
60 'product_source!' => ['manual_selection', 'current_query'],
61 ]
62 ]
63 );
64
65 $this->start_controls_tab(
66 'tab_product_include',
67 [
68 'label' => __('Include', 'ultimate-store-kit'),
69 'condition' => [
70 'product_source!' => ['manual_selection'],
71 ]
72 ]
73 );
74
75 $this->add_control(
76 'product_include_by',
77 [
78 'label' => __('Include By', 'ultimate-store-kit'),
79 'type' => Controls_Manager::SELECT2,
80 'multiple' => true,
81 'label_block' => true,
82 'options' => [
83 'categories' => __('Categories', 'ultimate-store-kit'),
84 'tags' => __('Tags', 'ultimate-store-kit'),
85 'brands' => __('Brands', 'ultimate-store-kit'),
86 'attributes' => __('Attributes', 'ultimate-store-kit'),
87 'date' => __('Date', 'ultimate-store-kit'),
88 'image' => __('Product Image', 'ultimate-store-kit'),
89 'price' => __('Price', 'ultimate-store-kit'),
90 'stock' => __('Stock', 'ultimate-store-kit'),
91 ],
92 'condition' => [
93 'product_source!' => ['manual_selection'],
94 ]
95 ]
96 );
97
98 $this->add_control(
99 'product_include_category_ids',
100 [
101 'label' => __('Categories', 'ultimate-store-kit'),
102 'type' => Dynamic_Select::TYPE,
103 'multiple' => true,
104 'label_block' => true,
105 'query_args' => [
106 'query' => 'product_cat',
107 ],
108 'condition' => [
109 'product_include_by' => 'categories',
110 'product_source!' => ['manual_selection'],
111 ]
112 ]
113 );
114
115 $this->add_control(
116 'product_include_tag_ids',
117 [
118 'label' => __('Tags', 'ultimate-store-kit'),
119 'type' => Dynamic_Select::TYPE,
120 'multiple' => true,
121 'label_block' => true,
122 'query_args' => [
123 'query' => 'product_tag',
124 ],
125 'condition' => [
126 'product_include_by' => 'tags',
127 'product_source!' => ['manual_selection'],
128 ]
129 ]
130 );
131
132 $this->add_control(
133 'product_include_brand_ids',
134 [
135 'label' => __('Brands', 'ultimate-store-kit'),
136 'type' => Dynamic_Select::TYPE,
137 'multiple' => true,
138 'label_block' => true,
139 'query_args' => [
140 'query' => 'product_brand',
141 ],
142 'condition' => [
143 'product_include_by' => 'brands',
144 'product_source!' => ['manual_selection'],
145 ]
146 ]
147 );
148
149 $this->add_control(
150 'product_include_attribute_ids',
151 [
152 'label' => __('Attributes', 'ultimate-store-kit'),
153 'type' => Dynamic_Select::TYPE,
154 'multiple' => true,
155 'label_block' => true,
156 'query_args' => [
157 'query' => 'product_attributes',
158 ],
159 'condition' => [
160 'product_include_by' => 'attributes',
161 'product_source!' => ['manual_selection'],
162 ]
163 ]
164 );
165
166 $this->add_control(
167 'product_include_date',
168 [
169 'label' => __('Date', 'ultimate-store-kit'),
170 'type' => Controls_Manager::SELECT,
171 'default' => 'anytime',
172 'options' => [
173 'anytime' => __('All', 'ultimate-store-kit'),
174 'today' => __('Past Day', 'ultimate-store-kit'),
175 'week' => __('Past Week', 'ultimate-store-kit'),
176 'month' => __('Past Month', 'ultimate-store-kit'),
177 'quarter' => __('Past Quarter', 'ultimate-store-kit'),
178 'year' => __('Past Year', 'ultimate-store-kit'),
179 'exact' => __('Custom', 'ultimate-store-kit'),
180 ],
181 'condition' => [
182 'product_source!' => 'manual_selection',
183 'product_include_by' => 'date',
184 ]
185 ]
186 );
187
188 $this->add_control(
189 'product_date_before',
190 [
191 'label' => __('Before', 'ultimate-store-kit'),
192 'type' => Controls_Manager::DATE_TIME,
193 'description' => __('Setting a "Before" date will show all the posts published until the chosen date (inclusive).', 'ultimate-store-kit'),
194 'condition' => [
195 'product_include_date' => 'exact',
196 'product_source!' => 'manual_selection',
197 ]
198 ]
199 );
200
201 $this->add_control(
202 'product_date_after',
203 [
204 'label' => __('After', 'ultimate-store-kit'),
205 'type' => Controls_Manager::DATE_TIME,
206 'description' => __('Setting an "After" date will show all the posts published since the chosen date (inclusive).', 'ultimate-store-kit'),
207 'condition' => [
208 'product_include_date' => 'exact',
209 'product_source!' => 'manual_selection',
210 ]
211 ]
212 );
213
214 $this->add_control(
215 'product_image_include',
216 [
217 'label' => esc_html__('Product Image', 'ultimate-store-kit'),
218 'type' => \Elementor\Controls_Manager::SELECT,
219 'default' => '',
220 'options' => [
221 '' => esc_html__('Default', 'ultimate-store-kit'),
222 'set' => esc_html__('Set', 'ultimate-store-kit'),
223 'not_set' => esc_html__('Not Set', 'ultimate-store-kit'),
224 ],
225 'condition' => [
226 'product_include_by' => 'image',
227 'product_source!' => ['manual_selection'],
228 ]
229 ]
230 );
231
232 $this->add_control(
233 'product_price_include',
234 [
235 'label' => esc_html__('Price', 'ultimate-store-kit'),
236 'type' => \Elementor\Controls_Manager::SELECT,
237 'default' => '',
238 'options' => [
239 '' => esc_html__('Default', 'ultimate-store-kit'),
240 'set' => esc_html__('Set', 'ultimate-store-kit'),
241 'not_set' => esc_html__('Not Set', 'ultimate-store-kit'),
242 ],
243 'condition' => [
244 'product_include_by' => 'price',
245 'product_source!' => ['manual_selection'],
246 ]
247 ]
248 );
249
250 $this->add_control(
251 'product_stock_include',
252 [
253 'label' => esc_html__('Stock', 'ultimate-store-kit'),
254 'type' => \Elementor\Controls_Manager::SELECT,
255 'default' => '',
256 'options' => [
257 '' => esc_html__('Default', 'ultimate-store-kit'),
258 'in_stock' => esc_html__('In Stock', 'ultimate-store-kit'),
259 'out_of_stock' => esc_html__('Out of Stock', 'ultimate-store-kit'),
260 ],
261 'condition' => [
262 'product_include_by' => 'stock',
263 'product_source!' => ['manual_selection'],
264 ]
265 ]
266 );
267
268
269 $this->end_controls_tab();
270
271 $this->start_controls_tab(
272 'tab_product_exclude',
273 [
274 'label' => __('Exclude', 'ultimate-store-kit'),
275 'condition' => [
276 'product_source!' => ['manual_selection', 'current_query'],
277 ]
278 ]
279 );
280
281 $this->add_control(
282 'product_exclude_by',
283 [
284 'label' => __('Exclude By', 'ultimate-store-kit'),
285 'type' => Controls_Manager::SELECT2,
286 'multiple' => true,
287 'label_block' => true,
288 'options' => [
289 'categories' => __('Categories', 'ultimate-store-kit'),
290 'tags' => __('Tags', 'ultimate-store-kit'),
291 'brands' => __('Brands', 'ultimate-store-kit'),
292 'attributes' => __('Attributes', 'ultimate-store-kit'),
293 'manual_selection' => __('Manual Selection', 'ultimate-store-kit'),
294 'date' => __('Date', 'ultimate-store-kit'),
295 'image' => __('Product Image', 'ultimate-store-kit'),
296 'price' => __('Price', 'ultimate-store-kit'),
297 'stock' => __('Stock', 'ultimate-store-kit'),
298 ],
299 'condition' => [
300 'product_source!' => ['manual_selection'],
301 ]
302 ]
303 );
304
305 $this->add_control(
306 'posts_exclude_ids',
307 [
308 'label' => __('Search & Select', 'ultimate-store-kit'),
309 'type' => Dynamic_Select::TYPE,
310 'multiple' => true,
311 'label_block' => true,
312 'query_args' => [
313 'query' => 'posts',
314 'post_type' => 'product',
315 ],
316 'condition' => [
317 'product_source!' => ['manual_selection'],
318 'product_exclude_by' => 'manual_selection',
319 ]
320 ]
321 );
322
323 $this->add_control(
324 'product_exclude_category_ids',
325 [
326 'label' => __('Categories', 'ultimate-store-kit'),
327 'type' => Dynamic_Select::TYPE,
328 'multiple' => true,
329 'label_block' => true,
330 'query_args' => [
331 'query' => 'product_cat',
332 ],
333 'condition' => [
334 'product_exclude_by' => 'categories',
335 'product_source!' => ['manual_selection'],
336 ]
337 ]
338 );
339 $this->add_control(
340 'product_exclude_tag_ids',
341 [
342 'label' => __('Tags', 'ultimate-store-kit'),
343 'type' => Dynamic_Select::TYPE,
344 'multiple' => true,
345 'label_block' => true,
346 'query_args' => [
347 'query' => 'product_tag',
348 ],
349 'condition' => [
350 'product_exclude_by' => 'tags',
351 'product_source!' => ['manual_selection'],
352 ]
353 ]
354 );
355 $this->add_control(
356 'product_exclude_brand_ids',
357 [
358 'label' => __('Brands', 'ultimate-store-kit'),
359 'type' => Dynamic_Select::TYPE,
360 'multiple' => true,
361 'label_block' => true,
362 'query_args' => [
363 'query' => 'product_brand',
364 ],
365 'condition' => [
366 'product_exclude_by' => 'brands',
367 'product_source!' => ['manual_selection'],
368 ]
369 ]
370 );
371 $this->add_control(
372 'product_exclude_attribute_ids',
373 [
374 'label' => __('Attributes', 'ultimate-store-kit'),
375 'type' => Dynamic_Select::TYPE,
376 'multiple' => true,
377 'label_block' => true,
378 'query_args' => [
379 'query' => 'product_attributes',
380 ],
381 'condition' => [
382 'product_exclude_by' => 'attributes',
383 'product_source!' => ['manual_selection'],
384 ]
385 ]
386 );
387
388 $this->add_control(
389 'product_exclude_date',
390 [
391 'label' => __('Date', 'ultimate-store-kit'),
392 'type' => Controls_Manager::SELECT,
393 'default' => 'anytime',
394 'options' => [
395 'anytime' => __('All', 'ultimate-store-kit'),
396 'today' => __('Past Day', 'ultimate-store-kit'),
397 'week' => __('Past Week', 'ultimate-store-kit'),
398 'month' => __('Past Month', 'ultimate-store-kit'),
399 'quarter' => __('Past Quarter', 'ultimate-store-kit'),
400 'year' => __('Past Year', 'ultimate-store-kit'),
401 'exact' => __('Custom', 'ultimate-store-kit'),
402 ],
403 'condition' => [
404 'product_exclude_by' => 'date',
405 'product_source!' => ['manual_selection'],
406 ]
407 ]
408 );
409
410 $this->add_control(
411 'product_exclude_date_before',
412 [
413 'label' => __('Before', 'ultimate-store-kit'),
414 'type' => Controls_Manager::DATE_TIME,
415 'description' => __('Show all posts published until the chosen date (inclusive).', 'ultimate-store-kit'),
416 'condition' => [
417 'product_exclude_date' => 'exact',
418 'product_source!' => ['manual_selection'],
419 ]
420 ]
421 );
422
423 $this->add_control(
424 'product_exclude_date_after',
425 [
426 'label' => __('After', 'ultimate-store-kit'),
427 'type' => Controls_Manager::DATE_TIME,
428 'description' => __('Show all posts published since the chosen date (inclusive).', 'ultimate-store-kit'),
429 'condition' => [
430 'product_exclude_date' => 'exact',
431 'product_source!' => ['manual_selection'],
432 ]
433 ]
434 );
435
436 $this->add_control(
437 'product_image_exclude',
438 [
439 'label' => esc_html__('Product Image', 'ultimate-store-kit'),
440 'type' => \Elementor\Controls_Manager::SELECT,
441 'default' => '',
442 'options' => [
443 '' => esc_html__('Default', 'ultimate-store-kit'),
444 'set' => esc_html__('Set', 'ultimate-store-kit'),
445 'not_set' => esc_html__('Not Set', 'ultimate-store-kit'),
446 ],
447 'condition' => [
448 'product_exclude_by' => 'image',
449 'product_source!' => ['manual_selection'],
450 ]
451 ]
452 );
453
454 $this->add_control(
455 'product_price_exclude',
456 [
457 'label' => esc_html__('Price', 'ultimate-store-kit'),
458 'type' => \Elementor\Controls_Manager::SELECT,
459 'default' => '',
460 'options' => [
461 '' => esc_html__('Default', 'ultimate-store-kit'),
462 'set' => esc_html__('Set', 'ultimate-store-kit'),
463 'not_set' => esc_html__('Not Set', 'ultimate-store-kit'),
464 ],
465 'condition' => [
466 'product_exclude_by' => 'price',
467 'product_source!' => ['manual_selection'],
468 ]
469 ]
470 );
471
472 $this->add_control(
473 'product_stock_exclude',
474 [
475 'label' => esc_html__('Stock', 'ultimate-store-kit'),
476 'type' => \Elementor\Controls_Manager::SELECT,
477 'default' => '',
478 'options' => [
479 '' => esc_html__('Default', 'ultimate-store-kit'),
480 'in_stock' => esc_html__('In Stock', 'ultimate-store-kit'),
481 'out_of_stock' => esc_html__('Out of Stock', 'ultimate-store-kit'),
482 ],
483 'condition' => [
484 'product_exclude_by' => 'stock',
485 'product_source!' => ['manual_selection'],
486 ]
487 ]
488 );
489
490 $this->add_control(
491 'product_exclude_term_ids',
492 [
493 'label' => __('Terms', 'ultimate-store-kit'),
494 'description' => __('Terms are items in a taxonomy. The available taxonomies are: Categories, Tags, Formats and custom taxonomies.', 'ultimate-store-kit'),
495 'type' => Dynamic_Select::TYPE,
496 'multiple' => true,
497 'label_block' => true,
498 'placeholder' => __('Type and select terms', 'ultimate-store-kit'),
499 'query_args' => [
500 'query' => 'terms',
501 'widget_props' => [
502 'post_type' => 'product_source'
503 ]
504 ],
505 'condition' => [
506 'product_exclude_by' => 'terms',
507 'product_source!' => ['manual_selection'],
508 ]
509 ]
510 );
511
512 $this->end_controls_tab();
513 $this->end_controls_tabs();
514
515 $this->add_control(
516 'product_divider',
517 [
518 'type' => Controls_Manager::DIVIDER,
519 'condition' => [
520 'product_source!' => 'current_query',
521 ]
522 ]
523 );
524
525 $this->add_control(
526 'product_orderby',
527 [
528 'label' => __('Order By', 'ultimate-store-kit'),
529 'type' => Controls_Manager::SELECT,
530 'default' => 'date',
531 'options' => [
532 'title' => __('Title', 'ultimate-store-kit'),
533 'ID' => __('ID', 'ultimate-store-kit'),
534 'date' => __('Date', 'ultimate-store-kit'),
535 'author' => __('Author', 'ultimate-store-kit'),
536 'comment_count' => __('Comment Count', 'ultimate-store-kit'),
537 'menu_order' => __('Menu Order', 'ultimate-store-kit'),
538 'modified' => __('Modified', 'ultimate-store-kit'),
539 'rand' => __('Random', 'ultimate-store-kit'),
540 'price' => __('Price', 'ultimate-store-kit'),
541 'sales' => __('Sales', 'ultimate-store-kit'),
542 ],
543 'condition' => [
544 'product_source!' => ['current_query'],
545 ]
546 ]
547 );
548 $this->add_control(
549 'product_order',
550 [
551 'label' => __('Order', 'ultimate-store-kit'),
552 'type' => Controls_Manager::SELECT,
553 'default' => 'desc',
554 'options' => [
555 'asc' => __('ASC', 'ultimate-store-kit'),
556 'desc' => __('DESC', 'ultimate-store-kit'),
557 ],
558 'condition' => [
559 'product_source!' => 'current_query',
560 ]
561 ]
562 );
563 }
564
565 public function register_controls_wc_additional()
566 {
567 $this->add_control(
568 'query_id',
569 [
570 'label' => __('Query ID', 'ultimate-store-kit'),
571 'description' => __('Give your Query a custom unique id to allow server side filtering', 'ultimate-store-kit'),
572 'type' => Controls_Manager::TEXT,
573 'separator' => 'before',
574 'dynamic' => [ 'active' => true ],
575 ]
576 );
577 }
578
579 /**
580 * @return array|string[]|\WP_Post_Type[]
581 */
582 private function getGroupControlQueryPostTypes()
583 {
584
585 $post_types = [
586 'product' => __('Product', 'ultimate-store-kit'),
587 'sale' => __('On Sale', 'ultimate-store-kit'),
588 'featured' => __('Featured', 'ultimate-store-kit'),
589 'manual_selection' => __('Manual Selection', 'ultimate-store-kit'),
590 'current_query' => __('Current Query', 'ultimate-store-kit'),
591 ];
592
593 return $post_types;
594 }
595
596 public function pre_get_posts_query_filter($wp_query)
597 {
598 if ($this) {
599 $query_id = $this->get_settings('query_id');
600 do_action("ultimate_store_kit/query/{$query_id}", $wp_query, $this);
601 }
602 }
603
604 private function get_product_source_args($source)
605 {
606 $args = [];
607
608 switch ($source) {
609 case 'sale':
610 $args['meta_query'] = [
611 [
612 'key' => '_sale_price',
613 'value' => 0,
614 'compare' => '>',
615 ],
616 ];
617 break;
618 case 'featured':
619 $args['tax_query'] = [
620 [
621 'taxonomy' => 'product_visibility',
622 'field' => 'name',
623 'terms' => 'featured',
624 'operator' => 'IN',
625 ],
626 ];
627 break;
628 case 'manual_selection':
629 $selected_ids = $this->get_settings('product_selected_ids');
630 if (!empty($selected_ids)) {
631 $args['post__in'] = $selected_ids;
632 }
633 break;
634 }
635
636 return $args;
637 }
638
639 /**
640 * Apply "Include By" logic to WP_Query args.
641 */
642 private function apply_include_by_args($args, $settings)
643 {
644 $include_by = $settings['product_include_by'] ?? [];
645
646 if (!is_array($include_by)) {
647 $include_by = [$include_by];
648 }
649
650 // Categories
651 if (in_array('categories', $include_by) && !empty($settings['product_include_category_ids'])) {
652 $args['tax_query'][] = [
653 'taxonomy' => 'product_cat',
654 'field' => 'term_id',
655 'terms' => $settings['product_include_category_ids'],
656 'operator' => 'IN',
657 ];
658 }
659
660 // Tags
661 if (in_array('tags', $include_by) && !empty($settings['product_include_tag_ids'])) {
662 $args['tax_query'][] = [
663 'taxonomy' => 'product_tag',
664 'field' => 'term_id',
665 'terms' => $settings['product_include_tag_ids'],
666 'operator' => 'IN',
667 ];
668 }
669
670 // Brands
671 if (in_array('brands', $include_by) && !empty($settings['product_include_brand_ids'])) {
672 $args['tax_query'][] = [
673 'taxonomy' => 'product_brand',
674 'field' => 'term_id',
675 'terms' => $settings['product_include_brand_ids'],
676 'operator' => 'IN',
677 ];
678 }
679
680 // Attributes
681 if (in_array('attributes', $include_by) && !empty($settings['product_include_attribute_ids'])) {
682 $args['tax_query'][] = [
683 'taxonomy' => 'product_attributes',
684 'field' => 'term_id',
685 'terms' => $settings['product_include_attribute_ids'],
686 'operator' => 'IN',
687 ];
688 }
689
690 // Date
691 if (in_array('date', $include_by) && !empty($settings['product_include_date'])) {
692 $date_query = [];
693 switch ($settings['product_include_date']) {
694 case 'today':
695 $date_query['after'] = '1 day ago';
696 break;
697 case 'week':
698 $date_query['after'] = '1 week ago';
699 break;
700 case 'month':
701 $date_query['after'] = '1 month ago';
702 break;
703 case 'quarter':
704 $date_query['after'] = '3 months ago';
705 break;
706 case 'year':
707 $date_query['after'] = '1 year ago';
708 break;
709 case 'exact':
710 if (!empty($settings['product_date_after'])) {
711 $date_query['after'] = $settings['product_date_after'];
712 }
713 if (!empty($settings['product_date_before'])) {
714 $date_query['before'] = $settings['product_date_before'];
715 }
716 $date_query['inclusive'] = true;
717 break;
718 }
719 if (!empty($date_query)) {
720 $args['date_query'][] = $date_query;
721 }
722 }
723
724 // Product Image
725 if (in_array('image', $include_by) && isset($settings['product_image_include']) && $settings['product_image_include'] !== '') {
726 if ($settings['product_image_include'] === 'set') {
727 $args['meta_query'][] = [
728 'key' => '_thumbnail_id',
729 'compare' => 'EXISTS',
730 ];
731 } elseif ($settings['product_image_include'] === 'not_set') {
732 $args['meta_query'][] = [
733 'key' => '_thumbnail_id',
734 'compare' => 'NOT EXISTS',
735 ];
736 }
737 }
738
739 // Price
740 if (in_array('price', $include_by) && isset($settings['product_price_include']) && $settings['product_price_include'] !== '') {
741 if ($settings['product_price_include'] === 'set') {
742 $args['meta_query'][] = [
743 'key' => '_price',
744 'compare' => 'EXISTS',
745 ];
746 } elseif ($settings['product_price_include'] === 'not_set') {
747 $args['meta_query'][] = [
748 'key' => '_price',
749 'compare' => 'NOT EXISTS',
750 ];
751 }
752 }
753
754 // Stock
755 if (in_array('stock', $include_by) && isset($settings['product_stock_include']) && $settings['product_stock_include'] !== '') {
756 if ($settings['product_stock_include'] === 'in_stock') {
757 $args['meta_query'][] = [
758 'key' => '_stock_status',
759 'value' => 'instock',
760 'compare' => '=',
761 ];
762 } elseif ($settings['product_stock_include'] === 'out_of_stock') {
763 $args['meta_query'][] = [
764 'key' => '_stock_status',
765 'value' => 'outofstock',
766 'compare' => '=',
767 ];
768 }
769 }
770
771 return $args;
772 }
773
774 /**
775 * Apply "Exclude By" logic to WP_Query args.
776 */
777 private function apply_exclude_by_args($args, $settings)
778 {
779 $exclude_by = $settings['product_exclude_by'] ?? [];
780
781 if (!is_array($exclude_by)) {
782 $exclude_by = [$exclude_by];
783 }
784
785 // Categories
786 if (in_array('categories', $exclude_by) && !empty($settings['product_exclude_category_ids'])) {
787 $args['tax_query'][] = [
788 'taxonomy' => 'product_cat',
789 'field' => 'term_id',
790 'terms' => $settings['product_exclude_category_ids'],
791 'operator' => 'NOT IN',
792 ];
793 }
794
795 // Tags
796 if (in_array('tags', $exclude_by) && !empty($settings['product_exclude_tag_ids'])) {
797 $args['tax_query'][] = [
798 'taxonomy' => 'product_tag',
799 'field' => 'term_id',
800 'terms' => $settings['product_exclude_tag_ids'],
801 'operator' => 'NOT IN',
802 ];
803 }
804
805 // Brands
806 if (in_array('brands', $exclude_by) && !empty($settings['product_exclude_brand_ids'])) {
807 $args['tax_query'][] = [
808 'taxonomy' => 'product_brand',
809 'field' => 'term_id',
810 'terms' => $settings['product_exclude_brand_ids'],
811 'operator' => 'NOT IN',
812 ];
813 }
814
815 // Attributes
816 if (in_array('attributes', $exclude_by) && !empty($settings['product_exclude_attribute_ids'])) {
817 $args['tax_query'][] = [
818 'taxonomy' => 'product_attributes',
819 'field' => 'term_id',
820 'terms' => $settings['product_exclude_attribute_ids'],
821 'operator' => 'NOT IN',
822 ];
823 }
824
825 // Manual Selection
826 if (in_array('manual_selection', $exclude_by) && !empty($settings['posts_exclude_ids'])) {
827 $args['post__not_in'] = $settings['posts_exclude_ids'];
828 }
829
830 // Date
831 if (in_array('date', $exclude_by) && !empty($settings['product_exclude_date'])) {
832 $date_query = [];
833 switch ($settings['product_exclude_date']) {
834 case 'today':
835 $date_query['before'] = '1 day ago';
836 break;
837 case 'week':
838 $date_query['before'] = '1 week ago';
839 break;
840 case 'month':
841 $date_query['before'] = '1 month ago';
842 break;
843 case 'quarter':
844 $date_query['before'] = '3 months ago';
845 break;
846 case 'year':
847 $date_query['before'] = '1 year ago';
848 break;
849 case 'exact':
850 if (!empty($settings['product_exclude_date_after'])) {
851 $date_query['after'] = $settings['product_exclude_date_after'];
852 }
853 if (!empty($settings['product_exclude_date_before'])) {
854 $date_query['before'] = $settings['product_exclude_date_before'];
855 }
856 $date_query['inclusive'] = true;
857 break;
858 }
859 if (!empty($date_query)) {
860 $args['date_query'][] = $date_query;
861 }
862 }
863
864 // Product Image
865 if (in_array('image', $exclude_by) && isset($settings['product_image_exclude']) && $settings['product_image_exclude'] !== '') {
866 if ($settings['product_image_exclude'] === 'set') {
867 $args['meta_query'][] = [
868 'key' => '_thumbnail_id',
869 'compare' => 'NOT EXISTS',
870 ];
871 } elseif ($settings['product_image_exclude'] === 'not_set') {
872 $args['meta_query'][] = [
873 'key' => '_thumbnail_id',
874 'compare' => 'EXISTS',
875 ];
876 }
877 }
878
879 // Price
880 if (in_array('price', $exclude_by) && isset($settings['product_price_exclude']) && $settings['product_price_exclude'] !== '') {
881 if ($settings['product_price_exclude'] === 'set') {
882 $args['meta_query'][] = [
883 'key' => '_price',
884 'compare' => 'NOT EXISTS',
885 ];
886 } elseif ($settings['product_price_exclude'] === 'not_set') {
887 $args['meta_query'][] = [
888 'key' => '_price',
889 'compare' => 'EXISTS',
890 ];
891 }
892 }
893
894 // Stock
895 if (in_array('stock', $exclude_by) && isset($settings['product_stock_exclude']) && $settings['product_stock_exclude'] !== '') {
896 if ($settings['product_stock_exclude'] === 'in_stock') {
897 $args['meta_query'][] = [
898 'key' => '_stock_status',
899 'value' => 'instock',
900 'compare' => '!=',
901 ];
902 } elseif ($settings['product_stock_exclude'] === 'out_of_stock') {
903 $args['meta_query'][] = [
904 'key' => '_stock_status',
905 'value' => 'outofstock',
906 'compare' => '!=',
907 ];
908 }
909 }
910
911 return $args;
912 }
913
914 protected function getGroupControlQueryArgs()
915 {
916 $settings = $this->get_settings();
917 $source = $settings['product_source'] ?? 'product';
918
919 // If current_query is selected, return special flag
920 if ($source === 'current_query') {
921 return ['__use_global_query' => true];
922 }
923
924 $page = max(1, get_query_var('paged'), get_query_var('page'));
925 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Pagination number off a public archive URL; cast to int and used only to page a query.
926 $page = absint(empty($_GET['product-page']) ? $page : $_GET['product-page']);
927 $paged = absint($page);
928
929 $args = [
930 'post_type' => 'product',
931 'paged' => $paged
932 ];
933
934 if (!empty($settings['product_limit'])) {
935 $args['posts_per_page'] = $settings['product_limit'];
936 }
937
938 $args = $args + $this->get_product_source_args($source);
939
940 // Apply "Include By" logic
941 $args = $this->apply_include_by_args($args, $settings);
942
943 // Apply "Exclude By" logic
944 $args = $this->apply_exclude_by_args($args, $settings);
945
946 // Apply Order and OrderBy
947 if (!empty($settings['product_orderby'])) {
948 switch ($settings['product_orderby']) {
949 case 'price':
950 $args['meta_key'] = '_price';
951 $args['orderby'] = 'meta_value_num';
952 break;
953 case 'sales':
954 $args['meta_key'] = 'total_sales';
955 $args['orderby'] = 'meta_value_num';
956 break;
957 default:
958 $args['orderby'] = $settings['product_orderby'];
959 }
960 }
961
962 if (!empty($settings['product_order'])) {
963 $args['order'] = $settings['product_order'];
964 }
965
966 $query_id = $this->get_settings('query_id');
967 if (! empty($query_id)) {
968 add_action('pre_get_posts', [$this, 'pre_get_posts_query_filter']);
969 }
970
971 return $args;
972 }
973
974 /**
975 * Get query - returns global query if current_query is selected, otherwise returns custom query
976 */
977 public function get_query()
978 {
979 return $this->_query;
980 }
981
982 /**
983 * Query products - handles both custom and global queries
984 * This is a helper method that can be called from widget's query_product() method
985 */
986 protected function build_query_from_args($query_args)
987 {
988 // Check if we should use global query
989 if (isset($query_args['__use_global_query']) && $query_args['__use_global_query'] === true) {
990 global $wp_query;
991
992 // Get the product limit setting
993 $settings = $this->get_settings();
994 $product_limit = $settings['product_limit'] ?? null;
995
996 // If product limit is set, create a new query based on global query args
997 if (!empty($product_limit)) {
998 $args = $wp_query->query_vars;
999 $args['posts_per_page'] = $product_limit;
1000 return new \WP_Query($args);
1001 }
1002
1003 return $wp_query;
1004 } else {
1005 return new \WP_Query($query_args);
1006 }
1007 }
1008
1009 /**
1010 * Query products - handles both custom and global queries
1011 * Default implementation - widgets can override this
1012 */
1013 public function query_product()
1014 {
1015 $query_args = $this->getGroupControlQueryArgs();
1016 $this->_query = $this->build_query_from_args($query_args);
1017 }
1018 }
1019