PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.63
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.63
51.1.86 51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 All 40 releases
king-addons / includes / features / faceted-filters / class-faceted-query-builder.php

class-faceted-query-builder.php in King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder 51.1.63, at includes/features/faceted-filters/class-faceted-query-builder.php

333 lines 10.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Faceted Query Builder helper.
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons;
9
10 if (!defined('ABSPATH')) {
11 exit;
12 }
13
14 /**
15 * Translates filter state into WP_Query arguments.
16 */
17 class Faceted_Query_Builder
18 {
19 /**
20 * Widget query baseline settings.
21 *
22 * @var array<string, mixed>
23 */
24 protected array $grid_settings;
25
26 /**
27 * Filter state from frontend.
28 *
29 * @var array<string, mixed>
30 */
31 protected array $filter_state;
32
33 /**
34 * Base query args from widget settings.
35 *
36 * @var array<string, mixed>
37 */
38 protected array $base_args;
39
40 /**
41 * Cached post IDs under current filters.
42 *
43 * @var array<int>
44 */
45 private array $cached_ids = [];
46
47 /**
48 * Constructor.
49 *
50 * @param array<string, mixed> $grid_settings Widget settings.
51 * @param array<string, mixed> $filter_state Filter state.
52 * @param array<string, mixed> $base_args Baseline query args.
53 */
54 public function __construct(array $grid_settings, array $filter_state, array $base_args = [])
55 {
56 $this->grid_settings = $grid_settings;
57 $this->filter_state = $filter_state;
58 $this->base_args = $base_args;
59 }
60
61 /**
62 * Build WP_Query arguments from state.
63 *
64 * @return array<string, mixed>
65 */
66 public function build_query_args(): array
67 {
68 $args = $this->base_args;
69
70 if (!empty($this->grid_settings['kng_post_type'])) {
71 $args['post_type'] = $this->grid_settings['kng_post_type'];
72 }
73
74 if (empty($args['post_type']) && !empty($this->filter_state['filters']['post_type'])) {
75 $args['post_type'] = sanitize_key((string) $this->filter_state['filters']['post_type']);
76 }
77
78 if (!empty($this->filter_state['page'])) {
79 $args['paged'] = max(1, (int) $this->filter_state['page']);
80 }
81
82 if (!empty($this->filter_state['filters']['search'])) {
83 $args['s'] = sanitize_text_field((string) $this->filter_state['filters']['search']);
84 }
85
86 if (!empty($this->filter_state['filters']['taxonomy']) && is_array($this->filter_state['filters']['taxonomy'])) {
87 $tax_query = [];
88 foreach ($this->filter_state['filters']['taxonomy'] as $taxonomy => $terms) {
89 if (empty($terms) || !is_array($terms)) {
90 continue;
91 }
92
93 $tax_query[] = [
94 'taxonomy' => sanitize_key((string) $taxonomy),
95 'field' => 'slug',
96 'terms' => array_map('sanitize_title', $terms),
97 'operator' => 'IN',
98 ];
99 }
100
101 if (!empty($tax_query)) {
102 $args['tax_query'] = $tax_query;
103 }
104 }
105
106 if (!empty($this->filter_state['filters']['meta']) && is_array($this->filter_state['filters']['meta'])) {
107 $meta_query = [];
108 foreach ($this->filter_state['filters']['meta'] as $meta_key => $meta_value) {
109 if (is_array($meta_value) && array_key_exists('min', $meta_value) && array_key_exists('max', $meta_value)) {
110 $meta_query[] = [
111 'key' => sanitize_key((string) $meta_key),
112 'value' => [
113 (float) $meta_value['min'],
114 (float) $meta_value['max'],
115 ],
116 'type' => 'NUMERIC',
117 'compare' => 'BETWEEN',
118 ];
119 continue;
120 }
121
122 if (empty($meta_value)) {
123 continue;
124 }
125
126 $meta_query[] = [
127 'key' => sanitize_key((string) $meta_key),
128 'value' => array_map('sanitize_text_field', (array) $meta_value),
129 'compare' => 'IN',
130 ];
131 }
132
133 if (!empty($meta_query)) {
134 $args['meta_query'] = $meta_query;
135 }
136 }
137
138 if (!isset($args['meta_query'])) {
139 $args['meta_query'] = [];
140 }
141
142 if (!empty($this->filter_state['filters']['price']) && is_array($this->filter_state['filters']['price'])) {
143 $price = $this->filter_state['filters']['price'];
144 if (isset($price['min'], $price['max'])) {
145 $args['meta_query'][] = [
146 'key' => '_price',
147 'value' => [
148 (float) $price['min'],
149 (float) $price['max'],
150 ],
151 'type' => 'NUMERIC',
152 'compare' => 'BETWEEN',
153 ];
154 }
155 }
156
157 if (!empty($this->filter_state['filters']['orderby'])) {
158 $args['orderby'] = sanitize_key((string) $this->filter_state['filters']['orderby']);
159 }
160
161 if (!empty($this->filter_state['filters']['order'])) {
162 $args['order'] = 'ASC' === strtoupper((string) $this->filter_state['filters']['order']) ? 'ASC' : 'DESC';
163 }
164
165 if (isset($args['meta_query']) && empty($args['meta_query'])) {
166 unset($args['meta_query']);
167 }
168
169 return $args;
170 }
171
172 /**
173 * Build taxonomy counts constrained by current filters.
174 *
175 * @param array<string,string> $taxonomies Taxonomies to count.
176 *
177 * @return array<string,array<string,int>>
178 */
179 public function build_taxonomy_counts(array $taxonomies): array
180 {
181 $counts = [];
182 $base_args = $this->build_query_args();
183
184 // Remove pagination for counts
185 unset($base_args['paged']);
186 unset($base_args['offset']);
187 $base_args['posts_per_page'] = -1;
188 $base_args['fields'] = 'ids';
189
190 foreach ($taxonomies as $taxonomy => $label) {
191 $query_args = $base_args;
192 // Remove existing tax filter on the same taxonomy to get full distribution under other filters
193 if (!empty($query_args['tax_query']) && is_array($query_args['tax_query'])) {
194 $query_args['tax_query'] = array_values(
195 array_filter(
196 $query_args['tax_query'],
197 static function ($t) use ($taxonomy) {
198 return ($t['taxonomy'] ?? '') !== $taxonomy;
199 }
200 )
201 );
202 if (empty($query_args['tax_query'])) {
203 unset($query_args['tax_query']);
204 }
205 }
206
207 $post_ids = get_posts($query_args);
208 if (empty($post_ids)) {
209 $counts[$taxonomy] = [];
210 continue;
211 }
212
213 $terms = wp_get_object_terms($post_ids, $taxonomy, ['fields' => 'all']);
214 $tax_counts = [];
215 if (!is_wp_error($terms)) {
216 foreach ($terms as $term) {
217 $tax_counts[$term->slug] = ($tax_counts[$term->slug] ?? 0) + (int) $term->count;
218 }
219 }
220 $counts[$taxonomy] = $tax_counts;
221 }
222
223 return $counts;
224 }
225
226 /**
227 * Build price range counts (bucketed).
228 *
229 * @param array<int,array<string,float|int>> $buckets Buckets with min/max.
230 *
231 * @return array<int,int>
232 */
233 public function build_price_counts(array $buckets): array
234 {
235 if (empty($buckets)) {
236 return [];
237 }
238 $ids = $this->get_filtered_ids();
239 if (empty($ids)) {
240 return [];
241 }
242
243 $counts = array_fill(0, count($buckets), 0);
244 foreach ($ids as $pid) {
245 $price = (float) get_post_meta($pid, '_price', true);
246 foreach ($buckets as $idx => $bucket) {
247 $min = isset($bucket['min']) ? (float) $bucket['min'] : 0.0;
248 $max = isset($bucket['max']) ? (float) $bucket['max'] : PHP_FLOAT_MAX;
249 if ($price >= $min && $price <= $max) {
250 $counts[$idx] += 1;
251 break;
252 }
253 }
254 }
255 return $counts;
256 }
257
258 /**
259 * Build meta value counts (e.g., ACF/select).
260 *
261 * @param array<string,int> $meta_keys Meta keys with optional value limit.
262 *
263 * @return array<string,array<string,int>>
264 */
265 public function build_meta_counts(array $meta_keys): array
266 {
267 global $wpdb;
268 if (empty($meta_keys)) {
269 return [];
270 }
271 $ids = $this->get_filtered_ids();
272 if (empty($ids)) {
273 return [];
274 }
275
276 $placeholders = implode(',', array_fill(0, count($ids), '%d'));
277 $out = [];
278
279 foreach ($meta_keys as $meta_key => $limit) {
280 $limit = $limit ? (int) $limit : 30;
281 $sql = $wpdb->prepare(
282 "SELECT meta_value, COUNT(*) AS cnt
283 FROM {$wpdb->postmeta}
284 WHERE meta_key = %s
285 AND post_id IN ($placeholders)
286 AND meta_value != ''
287 GROUP BY meta_value
288 ORDER BY cnt DESC
289 LIMIT %d",
290 array_merge([$meta_key], $ids, [$limit])
291 );
292 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
293 $rows = $wpdb->get_results($sql, ARRAY_A);
294 $counts = [];
295 if (!empty($rows)) {
296 foreach ($rows as $row) {
297 $val = is_array($row['meta_value']) ? wp_json_encode($row['meta_value']) : (string) $row['meta_value'];
298 $counts[$val] = (int) $row['cnt'];
299 }
300 }
301 $out[$meta_key] = $counts;
302 }
303
304 return $out;
305 }
306
307 /**
308 * Get filtered post IDs with current filters (cached).
309 *
310 * @return array<int>
311 */
312 private function get_filtered_ids(): array
313 {
314 if (!empty($this->cached_ids)) {
315 return $this->cached_ids;
316 }
317 $args = $this->build_query_args();
318 $args['fields'] = 'ids';
319 $args['posts_per_page'] = -1;
320 unset($args['paged'], $args['offset']);
321
322 $ids = get_posts($args);
323 $this->cached_ids = array_map('absint', $ids);
324 return $this->cached_ids;
325 }
326 }
327
328
329
330
331
332
333