PluginProbe
Filter Everything — WordPress & WooCommerce Filters / trunk
Filter Everything — WordPress & WooCommerce Filters vtrunk
1.9.6 1.9.5 1.9.4 1.9.3 1.9.2.2 1.9.2.1 trunk 1.2.1 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.4.1 1.4.4 1.4.5 1.4.8 1.4.9 1.5.0 1.5.1 1.6.0 1.6.1 1.6.2 1.6.3 All 51 releases
filter-everything / src / Entities / PostMetaNumEntity.php

PostMetaNumEntity.php in Filter Everything — WordPress & WooCommerce Filters trunk, at src/Entities/PostMetaNumEntity.php

669 lines 22.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FilterEverything\Filter;
4
5 if ( ! defined('ABSPATH') ) {
6 exit;
7 }
8
9 class PostMetaNumEntity implements Entity
10 {
11 use PostMetaTrait;
12 public $items = [];
13 /**
14 * Declared explicitly: assigned from the outside in
15 * EntityManager::prepareEntitiesToDisplay(); dynamic properties are
16 * deprecated since PHP 8.2.
17 */
18 public $items_sort = [];
19
20 public $filter = [];
21
22 protected $product_cache = [];
23
24 public $entityName = '';
25
26 public $excludedTerms = [];
27
28 public $isInclude = false;
29
30 public $new_meta_query = [];
31
32 public $postTypes = [];
33
34 public $wdr_product_ids = [];
35
36 public $is_woo_discount_rules = false;
37
38 public function __construct( $postMetaName, $postType ){
39 /**
40 * @feature clean code from unused methods
41 */
42 $this->entityName = $postMetaName;
43 if(flrt_is_woo_discount_rules()){
44 $this->is_woo_discount_rules = true;
45 }
46 $this->setPostTypes( array($postType) );
47 }
48
49 public function setPostTypes( $postTypes = false )
50 {
51 $wpManager = Container::instance()->getWpManager();
52 $wpQueriedObject = $wpManager->getQueryVar('wp_queried_object');
53
54 if ($postTypes) {
55 $this->postTypes = $postTypes;
56 }elseif( ! empty( $wpQueriedObject['post_types'] ) ){
57 $this->postTypes = $wpQueriedObject['post_types'];
58 }else{
59 $this->postTypes = array('post');
60 }
61
62 if( flrt_is_woocommerce()
63 && in_array( 'product', $this->postTypes )
64 && ! in_array( 'product_variation', $this->postTypes ) ){
65 $this->postTypes[] = 'product_variation';
66 }
67
68 $this->getAllExistingTerms();
69 }
70
71 public static function inputName( $slug, $edge = 'min' )
72 {
73 return $edge . '_' . $slug;
74 }
75
76 public function setExcludedTerms( $excludedTerms, $isInclude )
77 {
78 $this->excludedTerms = $excludedTerms;
79 $this->isInclude = $isInclude;
80 }
81
82 public function getName()
83 {
84 return $this->entityName;
85 }
86
87 function excludeTerms( $terms )
88 {
89 return $terms;
90 }
91
92 function getTerms()
93 {
94 return $this->excludeTerms( $this->getAllExistingTerms() );
95 }
96
97 /**
98 * @param int $id term id
99 * @return false|object term object of false
100 */
101 public function getTerm( $termId ){
102 if( ! $termId ){
103 return false;
104 }
105
106 foreach ( $this->getTerms() as $term ){
107 if( $termId == $term->term_id ){
108 return $term;
109 }
110 }
111
112 return false;
113 }
114
115 public function getTermId( $slug )
116 {
117 /**
118 * Post meta num value has no typical ID, so slug will be instead
119 */
120 return $slug . '_' . $this->getName();
121 }
122
123 /**
124 * @return array list of term_id and names useful to create Select dropdown
125 */
126 public function getTermsForSelect()
127 {
128 $toSelect = [];
129 foreach ( $this->getTerms() as $term ) {
130 $toSelect[$term->slug] = $term->name;
131 }
132
133 return $toSelect;
134 }
135
136 public function getTermsForSelect2()
137 {
138 $toSelect = [];
139 foreach ( $this->getTerms() as $term ) {
140 $toSelect[] = array( 'id' => $term->slug, 'text' =>$term->name );
141 }
142
143 return $toSelect;
144 }
145
146 function getAllExistingTerms( $force = false )
147 {
148 if( empty( $this->items ) || $force ) {
149 $this->items = $this->selectTerms();
150 }
151 return $this->items;
152 }
153
154 public function isDecimal( $step = 0, $value = 0 )
155 {
156 if( strpos( $step, '.') !== false ){
157 return true;
158 }
159
160 if( strpos( $value, '.') !== false ){
161 return true;
162 }
163
164 return false;
165 }
166
167 function populateTermsWithPostIds( $setId, $post_type )
168 {
169 // Does nothing. It was already done before.
170 }
171
172 /**
173 * @param array $alreadyFilteredPosts
174 * @return array
175 */
176 public function selectTerms( $alreadyFilteredPosts = [] ) {
177 global $wpdb;
178
179 $IN = false;
180 $key_in = '';
181 $new_result = [];
182 $min_and_max = [
183 'min' => 0,
184 'max' => 0
185 ];
186 $post_and_types = [];
187 $post_and_meta_values = [];
188 $translatable_post_type_exists = false;
189
190 /**
191 * Set Post types
192 */
193 if( ! empty( $this->postTypes ) && isset($this->postTypes[0]) && $this->postTypes[0] ){
194 foreach ( $this->postTypes as $postType ){
195 $key_in .= '_' . $postType;
196 $pieces[] = $wpdb->prepare( "%s", $postType );
197 }
198
199 $IN = implode(", ", $pieces );
200 }
201
202 /**
203 * Set transient key
204 */
205 $transient_key = flrt_get_terms_transient_key( 'post_meta_num_'. $this->getName() . $key_in );
206
207 $result = flrt_get_transient( $transient_key );
208
209 // Cache format v2: ['vals' => [post_id => float|float[]], 'types' => [post_id => post_type]].
210 // Legacy format cached raw SQL rows — detect and rebuild those.
211 if ( ! is_array( $result ) || ! isset( $result['vals'], $result['types'] ) ) {
212 // Get all post meta values
213 $sql[] = "SELECT {$wpdb->postmeta}.post_id,{$wpdb->postmeta}.meta_value,{$wpdb->posts}.post_type";
214 $sql[] = "FROM {$wpdb->postmeta}";
215 $sql[] = "LEFT JOIN {$wpdb->posts} ON ({$wpdb->postmeta}.post_id = {$wpdb->posts}.ID)";
216
217 /**
218 * If post type is translatable with WPML, get post meta values only with current language
219 */
220 if( flrt_wpml_active() && defined( 'ICL_LANGUAGE_CODE' ) ){
221
222 $wpml_settings = get_option( 'icl_sitepress_settings' );
223
224 foreach ( $this->postTypes as $type ) {
225 if( isset( $wpml_settings['custom_posts_sync_option'][$type] ) ){
226 if( $wpml_settings['custom_posts_sync_option'][$type] === '1' ){
227 $translatable_post_type_exists = true;
228 break;
229 }
230 }
231 }
232
233 if ( $translatable_post_type_exists ) {
234 $sql[] = "LEFT JOIN {$wpdb->prefix}icl_translations AS wpml_translations";
235 $sql[] = "ON {$wpdb->postmeta}.post_id = wpml_translations.element_id";
236
237 if (!empty($this->postTypes)) {
238
239 $sql[] = "AND wpml_translations.element_type IN(";
240
241 foreach ($this->postTypes as $type) {
242 $LANG_IN[] = $wpdb->prepare("CONCAT('post_', '%s')", $type);
243 }
244 $sql[] = implode(",", $LANG_IN);
245
246 $sql[] = ")";
247 }
248 }
249 }
250 /**
251 * There is NULL problem because posts with meta_value = '' are also included in the list
252 * And condition (NULL <= 0) is true
253 * */
254 $sql[] = "WHERE {$wpdb->postmeta}.meta_key = %s";
255 $sql[] = "AND {$wpdb->postmeta}.meta_value IS NOT NULL";
256
257 if( $IN ){
258 $sql[] = "AND {$wpdb->posts}.post_type IN( {$IN} )";
259 }
260
261 if( flrt_wpml_active() && defined( 'ICL_LANGUAGE_CODE' ) && $translatable_post_type_exists ){
262
263 $sql[] = $wpdb->prepare("AND wpml_translations.language_code = '%s'", ICL_LANGUAGE_CODE);
264 }
265
266 $sql = implode(' ', $sql);
267
268 $e_name = wp_unslash( $this->entityName );
269 $sql = $wpdb->prepare( $sql, $e_name );
270
271 /**
272 * Filters terms SQL-query and allows to modify it
273 */
274 $sql = apply_filters( 'wpc_filter_get_post_meta_num_terms_sql', $sql, $e_name );
275
276 $rows = $wpdb->get_results( $sql, ARRAY_A );
277
278 // Aggregate raw rows into the compact v2 maps. A post can have
279 // several meta rows with different values (e.g. WC variable
280 // products keep one _price row per distinct child price) — such
281 // posts get an array of floats instead of a single float.
282 $vals = [];
283 $types = [];
284 foreach ( $rows as $single_post ) {
285 if ( preg_match( '/[^\d\.\-]+/', $single_post['meta_value'] ) ) {
286 continue;
287 }
288
289 $post_id = (int) $single_post['post_id'];
290 $meta_value = (float) $single_post['meta_value'];
291
292 if ( isset( $vals[ $post_id ] ) ) {
293 if ( ! is_array( $vals[ $post_id ] ) ) {
294 $vals[ $post_id ] = array( $vals[ $post_id ] );
295 }
296 $vals[ $post_id ][] = $meta_value;
297 } else {
298 $vals[ $post_id ] = $meta_value;
299 }
300
301 $types[ $post_id ] = $single_post['post_type'];
302 }
303 unset( $rows );
304
305 $result = array( 'vals' => $vals, 'types' => $types );
306 unset( $vals, $types );
307
308 flrt_set_transient( $transient_key, $result, FLRT_TRANSIENT_PERIOD_HOURS * HOUR_IN_SECONDS );
309 }
310
311 $meta_vals = isset( $result['vals'] ) ? $result['vals'] : [];
312 $meta_types = isset( $result['types'] ) ? $result['types'] : [];
313
314 if( ! empty( $meta_vals ) ) {
315
316 $postsIn_flipped = array_flip( $alreadyFilteredPosts );
317 $wpManager = Container::instance()->getWpManager();
318 $queried_values = $wpManager->getQueryVar( 'queried_values', [] );
319 $fss = Container::instance()->getFilterSetService();
320 $filter_slug = false;
321 $setId = false;
322
323 /**
324 * Check if this filter was queried
325 */
326 foreach ( $queried_values as $slug => $filter ) {
327 if ( $filter[ 'e_name' ] === $this->getName() ) {
328 $filter_slug = $slug;
329 $setId = $filter['parent'];
330 break;
331 }
332 }
333
334 $max = false;
335 $min = false;
336
337 /**
338 * If this filter was queried we have to receive its $max and $min values
339 */
340 if ( $filter_slug ) {
341 if ( isset( $queried_values[ $filter_slug ][ 'values' ][ 'max' ] ) ) {
342 $max = (float) $queried_values[ $filter_slug ][ 'values' ][ 'max' ];
343 $max = apply_filters( 'wpc_unset_num_shift', $max, $this->getName() );
344 }
345
346 if ( isset( $queried_values[ $filter_slug ][ 'values' ][ 'min' ] ) ) {
347 $min = (float) $queried_values[ $filter_slug ][ 'values' ][ 'min' ];
348 $min = apply_filters( 'wpc_unset_num_shift', $min, $this->getName() );
349 }
350 }
351
352 if($this->is_woo_discount_rules && (strpos($this->entityName, '_price') !== false)){
353 $wdr_woo_discount_rules = $this->getWooDiscountRulesClass();
354 $this->preloadProducts( array_keys( $meta_vals ) );
355 }
356
357 foreach ( $meta_vals as $post_id => $post_values ) {
358 /**
359 * If there are already filtered posts, we have to skip posts
360 * that are out of the queried list
361 */
362 if( ! empty( $alreadyFilteredPosts ) ) {
363 if( ! isset( $postsIn_flipped[ $post_id ] ) ) {
364 continue;
365 }
366 }
367
368 if ($this->is_woo_discount_rules) {
369 if (strpos($this->entityName, '_price') !== false) {
370 $product = $this->getProductCached($post_id);
371 if ($product) {
372 $wdr_product_has_sale = $wdr_woo_discount_rules->getProductPriceToDisplay($product);
373 if ($wdr_product_has_sale) {
374 $post_values = (float) $wdr_product_has_sale['discounted_price'];
375 }
376 }
377 }
378 }
379
380 /**
381 * We have to generate and fill two arrays
382 * First to detect $min and $max values
383 * Second to map post_types with post IDs
384 */
385 foreach ( (array) $post_values as $meta_value ) {
386 $post_and_meta_values[ $post_id ][] = $meta_value;
387 $new_result[] = $meta_value;
388
389 if ( $min !== false && $meta_value < $min ){
390 continue;
391 }
392
393 if ( $max !== false && $meta_value > $max ){
394 continue;
395 }
396
397 if($this->is_woo_discount_rules){
398 $this->wdr_product_ids[] = $post_id;
399 }
400
401 $post_and_types[ $post_id ] = $meta_types[ $post_id ];
402 }
403 }
404
405 }
406 $abs_min_and_max = [
407 'abs_min' => 0,
408 'abs_max' => 0,
409 ];
410 if( ! empty( $new_result ) ){
411 $min_and_max = [
412 'min' => apply_filters( 'wpc_set_num_shift', min( $new_result ), $this->getName(), 'min' ),
413 'max' => apply_filters( 'wpc_set_num_shift', max( $new_result ), $this->getName(), 'max' ),
414 ];
415
416 $flat = [];
417 foreach ($post_and_meta_values as $post_id => $values) {
418 foreach ($values as $value) {
419 $flat[$post_id][] = $value;
420 }
421 }
422
423 $all_values = array_merge(...array_values($post_and_meta_values));
424
425 $abs_min = min($all_values);
426 $abs_max = max($all_values);
427
428 $min_post_id = null;
429 $max_post_id = null;
430
431 foreach ($post_and_meta_values as $post_id => $values) {
432 if (in_array($abs_min, $values)) {
433 $min_post_id = $post_id;
434 }
435 if (in_array($abs_max, $values)) {
436 $max_post_id = $post_id;
437 }
438 }
439
440 $abs_min_and_max = [
441 'abs_min' => $abs_min,
442 'abs_max' => $abs_max,
443 ];
444
445 $post_and_meta_values[$min_post_id] = [$min_and_max['min']];
446 $post_and_meta_values[$max_post_id] = [$min_and_max['max']];
447 }
448
449 $min_and_max = apply_filters( 'wpc_set_min_max', $min_and_max, $this->getName() );
450 return $this->convertSelectResult( $min_and_max, $post_and_types, $post_and_meta_values, $abs_min_and_max);
451 }
452
453 public function updateMinAndMaxValues( $postsIn )
454 {
455 if( ! empty( $this->items ) && ! empty( $postsIn ) ){
456 $newItems = $this->selectTerms( $postsIn );
457 foreach ( $this->items as $index => $term ) {
458 if( isset( $this->items[$index]->$index ) ){
459 $this->items[$index]->$index = $newItems[$index]->$index;
460 }
461 }
462 }
463 }
464
465 private function createTermName( $edge, $value, $queried_values )
466 {
467 $name = $edge;
468 $queriedFilter = false;
469
470 if( $queried_values ){
471 foreach ( $queried_values as $slug => $filter ){
472 if( $filter['e_name'] === $this->getName() ){
473 $queriedFilter = $filter;
474 break;
475 }
476 }
477
478 if ( $queriedFilter ) {
479 $name = $name .' '. $slug;
480 }
481 }
482
483 $name = ($name === 'min') ? esc_html__('min', 'filter-everything') : $name;
484 $name = ($name === 'max') ? esc_html__('max', 'filter-everything') : $name;
485
486 if( isset( $queriedFilter['values'][$edge] ) ) {
487 $name = $name .' '. $queriedFilter['values'][$edge];
488 }else{
489 $name = $name .' '. $value;
490 }
491
492 return apply_filters( 'wpc_filter_post_meta_num_term_name', $name, $this->getName() );
493 }
494
495 public function convertSelectResult( $result, $post_and_types = [], $post_and_meta_values = [], $abs_min_and_max = [] ){
496 $return = [];
497
498 if( ! is_array( $result ) ){
499 return $return;
500 }
501
502 // To make standard format for terms array;
503 $i = 1;
504 $wpManager = Container::instance()->getWpManager();
505 $queried_values = $wpManager->getQueryVar( 'queried_values' );
506
507 foreach ( $result as $edge => $value ){
508
509 $termObject = new \stdClass();
510 $termObject->slug = $edge;
511 $termObject->name = $this->createTermName( $edge, $value, $queried_values );
512 $termObject->term_id = $edge . '_' . $this->getName();
513 $termObject->posts = array_keys( $post_and_types );
514 $termObject->meta_values = $post_and_meta_values;
515 $termObject->count = 0;
516 $termObject->cross_count = 0;
517 $termObject->post_types = $post_and_types; //[];
518 $termObject->$edge = $value;
519 $termObject->wp_queried = false;
520 $termObject->abs_values = $abs_min_and_max;
521
522 $return[ $edge ] = $termObject;
523
524 $i++;
525 }
526
527 return $return;
528 }
529
530 /**
531 * @return object WP_Query
532 */
533 public function addTermsToWpQuery( $queried_value, $wp_query )
534 {
535 $meta_query = [];
536 $key = $queried_value['e_name'];
537 // Add existing Meta Query if present
538 $this->importExistingMetaQuery( $wp_query );
539
540 /**
541 * @bug for Woo Products if we don't specify Max value it makes it 0.0000
542 */
543 $min = isset( $queried_value['values']['min'] ) ? $queried_value['values']['min'] : false;
544 $max = isset( $queried_value['values']['max'] ) ? $queried_value['values']['max'] : false;
545
546 // Compare with false because $min can be 0
547 if ((strpos($key, '_price') !== false) && flrt_is_woocommerce() && $this->is_woo_discount_rules) {
548 $query_post_in = $this->wdrAddProductsRangeToFilterQuery($wp_query);
549 $wp_query->set('post__in', $query_post_in);
550 if (empty($query_post_in)){
551 $wp_query->set('posts_per_page', $query_post_in);
552 }
553 } else {
554 if ($min !== false && $max !== false) {
555 $min = apply_filters('wpc_unset_num_shift', $min, $this->getName());
556 $max = apply_filters('wpc_unset_num_shift', $max, $this->getName());
557
558 $type = ( $this->isDecimal($queried_value['step'], $min) || $this->isDecimal($queried_value['step'], $max) ) ? 'DECIMAL(15,6)' : 'NUMERIC';
559 /**
560 * Both bounds must live in ONE clause. WP_Meta_Query gives every
561 * clause its own postmeta JOIN, so separate >= and <= clauses can
562 * match two DIFFERENT meta rows: a post with several values of the
563 * key (e.g. variable product _price rows) passed the range even
564 * when no single value was inside it, and the results disagreed
565 * with the term counters. BETWEEN checks the same row.
566 */
567 $meta_query = array(
568 'key' => $key,
569 'value' => array( $min, $max ),
570 'compare' => 'BETWEEN',
571 'type' => $type
572 );
573 $this->addMetaQueryArray($meta_query);
574
575 } elseif ($min !== false) {
576 $min = apply_filters('wpc_unset_num_shift', $min, $this->getName());
577
578 $type = $this->isDecimal($queried_value['step'], $min) ? 'DECIMAL(15,6)' : 'NUMERIC';
579 $meta_query = array(
580 'key' => $key,
581 'value' => $min,
582 'compare' => '>=',
583 'type' => $type
584 );
585 $this->addMetaQueryArray($meta_query);
586
587 } elseif ($max !== false) {
588 $max = apply_filters('wpc_unset_num_shift', $max, $this->getName());
589
590 $type = $this->isDecimal($queried_value['step'], $max) ? 'DECIMAL(15,6)' : 'NUMERIC';
591 $meta_query = array(
592 'key' => $key,
593 'value' => $max,
594 'compare' => '<=',
595 'type' => $type
596 );
597 $this->addMetaQueryArray($meta_query);
598 }
599
600 if (count($this->new_meta_query) > 1) {
601 $this->new_meta_query['relation'] = 'AND';
602 }
603 }
604
605 $wp_query->set('meta_query', $this->new_meta_query);
606
607 $this->new_meta_query = [];
608
609 return $wp_query;
610 }
611
612 public function wdrAddProductsRangeToFilterQuery($wp_query)
613 {
614 $args = array(
615 'status' => 'publish',
616 'limit' => -1,
617 'return' => 'ids'
618 );
619
620 //Added for work woo_discount_rules and query hash
621 $args['flrt_wdr_pagination'] = true;
622
623 if (isset($wp_query->queried_object->taxonomy) && isset($wp_query->queried_object->taxonomy)) {
624 $args['tax_query'] = array(
625 array(
626 'taxonomy' => $wp_query->queried_object->taxonomy,
627 'field' => 'term_id',
628 'terms' => array($wp_query->queried_object->term_id),
629 )
630 );
631 }
632
633 $products = wc_get_products($args);
634 $query_post_in = array_intersect($products, $this->wdr_product_ids);
635 if(isset($wp_query->query_vars['post__in']) && !empty($wp_query->query_vars['post__in'])){
636 $query_post_in = array_intersect($query_post_in, $wp_query->query_vars['post__in']);
637 }
638 if(empty($query_post_in)){
639 return [0];
640 }
641 return $query_post_in;
642 }
643
644 public function getWooDiscountRulesClass(){
645 return flrt_woo_discount_rules_class();
646 }
647
648 protected function preloadProducts($product_ids) {
649 if (empty($product_ids)) {
650 return;
651 }
652
653 $products = wc_get_products([
654 'include' => $product_ids,
655 'limit' => -1,
656 ]);
657
658 foreach ($products as $product) {
659 $this->product_cache[$product->get_id()] = $product;
660 }
661 }
662
663 protected function getProductCached($product_id) {
664 if (!isset($this->product_cache[$product_id])) {
665 $this->product_cache[$product_id] = wc_get_product($product_id);
666 }
667 return $this->product_cache[$product_id];
668 }
669 }