PluginProbe
Filter Everything — WordPress & WooCommerce Filters / 1.9.7
Filter Everything — WordPress & WooCommerce Filters v1.9.7
1.9.7 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 All 52 releases
← All changes | src/Entities/TaxonomyEntity.php +171 -50 1.5.11.9.7 View file →
@@ -1,26 +1,36 @@
1 1 <?php
2 2
3 3 namespace FilterEverything\Filter;
4 4
5 -if ( ! defined('WPINC') ) {
6 - wp_die();
5 +if ( ! defined('ABSPATH') ) {
6 + exit;
7 7 }
8 8
9 9 class TaxonomyEntity implements Entity
10 10 {
11 - public $items = [];
11 + public $items = [];
12 + /**
13 + * Declared explicitly: assigned from the outside in
14 + * EntityManager::prepareEntitiesToDisplay(); dynamic properties are
15 + * deprecated since PHP 8.2.
16 + */
17 + public $items_sort = [];
12 18
13 - public $excludedTerms = [];
19 + public $filter = [];
14 20
15 - public $descendants = [];
21 + public $excludedTerms = [];
16 22
17 - private $entityName = '';
23 + public $isInclude = false;
18 24
19 - private $new_tax_query = [];
25 + public $descendants = [];
20 26
21 - private $postTypes = [];
27 + private $entityName = '';
22 28
29 + private $new_tax_query = [];
30 +
31 + private $postTypes = [];
32 +
23 33 public function __construct( $taxName ){
24 34 $this->entityName = $taxName;
25 35 $this->getAllExistingTerms();
26 36 $this->passTermNames();
@@ -30,11 +40,12 @@
30 40 {
31 41 $this->postTypes = $postTypes;
32 42 }
33 43
34 - public function setExcludedTerms( $excludedTerms )
44 + public function setExcludedTerms( $excludedTerms, $isInclude )
35 45 {
36 46 $this->excludedTerms = (array) $excludedTerms;
47 + $this->isInclude = $isInclude;
37 48 }
38 49
39 50 public function getName()
40 51 {
@@ -48,26 +59,29 @@
48 59 if( ! empty( $this->excludedTerms ) ){
49 60 $exclude = $this->excludedTerms;
50 61 }
51 62
52 - // Exclude taxonomy term if it already exists in wp_query
53 - $wpManager = Container::instance()->getWpManager();
54 - $wp_queried_object = $wpManager->getQueryVar( 'wp_queried_object' );
55 -
56 - if( isset( $wp_queried_object['taxonomy'] ) && $wp_queried_object['taxonomy'] === $this->getName() ){
57 - $exclude[] = $wp_queried_object['term_id'];
58 - }
59 -
60 - foreach( $terms as $index => $term ){
61 - if( in_array( $term->term_id, $exclude ) ){
62 - unset( $terms[$index] );
63 + $exclude_flipped = array_flip( $exclude );
64 + if( $this->isInclude ){
65 + $included_terms = [];
66 + foreach( $terms as $index => $term ){
67 + if( isset( $exclude_flipped[$term->term_id] ) ){
68 + $included_terms[$index] = $term;
69 + }
63 70 }
71 + $terms = $included_terms;
72 + }else{
73 + foreach( $terms as $index => $term ){
74 + if( isset( $exclude_flipped[$term->term_id] ) ){
75 + unset( $terms[$index] );
76 + }
77 + }
64 78 }
65 79
66 80 return $terms;
67 81 }
68 82
69 - public function getTermTaxonomyPostsIds( $termTaxonomyIds, $filter )
83 + public function getTermTaxonomyPostsIds( $termTaxonomyIds, $termIds, $filter )
70 84 {
71 85 global $wpdb;
72 86 $include_variation_atts = false;
73 87 $ids = [];
@@ -78,10 +92,24 @@
78 92
79 93 // Check if it is already stored
80 94 $transient_key = flrt_get_post_ids_transient_key( $filter['slug'] );
81 95
82 - if ( false === ( $results = get_transient( $transient_key ) ) ) {
96 + $ids = flrt_get_transient( $transient_key );
83 97
98 + // Cache format v2: aggregated [term_id => [object_id, ...]] int map.
99 + // Legacy format cached raw SQL rows (each an assoc array with a 'term_id'
100 + // key) — detect and rebuild those. The compact map is 15-30x smaller in
101 + // memory and in wp_options.
102 + $is_v2 = is_array( $ids );
103 + if ( $is_v2 && ! empty( $ids ) ) {
104 + $first_item = reset( $ids );
105 + if ( is_array( $first_item ) && isset( $first_item['term_id'] ) ) {
106 + $is_v2 = false;
107 + }
108 + }
109 +
110 + if ( ! $is_v2 ) {
111 +
84 112 // It wasn't there, so regenerate the data and save the transient
85 113 if( defined('FLRT_FILTERS_PRO') && FLRT_FILTERS_PRO ) {
86 114 if( strpos( $this->getName(), 'pa_' ) === 0 ) {
87 115 $include_variation_atts = true;
@@ -87,9 +115,11 @@
87 115 $include_variation_atts = true;
88 116 }
89 117 }
90 118
91 - $query[] = "SELECT DISTINCT {$wpdb->term_relationships}.term_taxonomy_id,{$wpdb->term_relationships}.object_id";
119 + $query[] = "SELECT DISTINCT {$wpdb->term_relationships}.term_taxonomy_id";
120 + $query[] = ", {$wpdb->term_relationships}.object_id";
121 + $query[] = ", tt.term_id";
92 122
93 123 if( $include_variation_atts ){
94 124 $query[] = ", tm.slug";
95 125 }
@@ -94,12 +124,12 @@
94 124 $query[] = ", tm.slug";
95 125 }
96 126
97 127 $query[] = "FROM {$wpdb->term_relationships}";
128 + $query[] = "LEFT JOIN {$wpdb->term_taxonomy} AS tt";
129 + $query[] = "ON ( {$wpdb->term_relationships}.term_taxonomy_id = tt.term_taxonomy_id )";
98 130
99 131 if( $include_variation_atts ){
100 - $query[] = "LEFT JOIN {$wpdb->term_taxonomy} AS tt";
101 - $query[] = "ON ( {$wpdb->term_relationships}.term_taxonomy_id = tt.term_taxonomy_id )";
102 132 $query[] = "LEFT JOIN {$wpdb->terms} AS tm";
103 133 $query[] = "ON ( tt.term_id = tm.term_id )";
104 134 }
105 135
@@ -108,15 +138,27 @@
108 138 $query = implode(' ', $query);
109 139
110 140 $results = $wpdb->get_results($query, ARRAY_A);
111 141
112 - set_transient( $transient_key, $results, FLRT_TRANSIENT_PERIOD_HOURS * HOUR_IN_SECONDS );
142 + // Note: with the v2 cache this filter runs on the raw rows only when
143 + // the cache is being (re)built, not on every request as before.
144 + $taxonomy_terms = apply_filters( 'wpc_term_taxonomy_terms', $results, $this );
145 + unset( $results );
146 +
147 + $ids = [];
148 + foreach ($taxonomy_terms as $key => $result) {
149 + $ids[ (int) $result['term_id'] ][] = (int) $result['object_id'];
150 + }
151 + unset( $taxonomy_terms );
152 +
153 + flrt_set_transient( $transient_key, $ids, FLRT_TRANSIENT_PERIOD_HOURS * HOUR_IN_SECONDS );
113 154 }
114 155
115 - $taxonomy_terms = apply_filters( 'wpc_term_taxonomy_terms', $results, $this );
116 -
117 - foreach ($taxonomy_terms as $key => $result) {
118 - $ids[$result['term_taxonomy_id']][] = (int) $result['object_id'];
156 + // Add possible empty terms without posts
157 + foreach( $termIds as $term_id ){
158 + if( ! isset( $ids[$term_id] ) ){
159 + $ids[$term_id] = [];
160 + }
119 161 }
120 162
121 163 // Fix for counts for parent, when 'include_children=true'
122 164 // Add posts from children terms to their parents
@@ -122,9 +164,8 @@
122 164 // Add posts from children terms to their parents
123 165 // To make correct counts for their parents
124 166 if( ! empty( $this->descendants ) && $filter['logic'] === 'or' ){
125 167 // array of parents term_ids, that have children
126 -
127 168 foreach ( $ids as $term_id => $post_ids_array ){
128 169 if( isset( $this->descendants[$term_id] ) ){
129 170 foreach ( $this->descendants[$term_id] as $child_term_id ){
130 171 if( isset( $ids[$child_term_id] ) ){
@@ -140,8 +181,9 @@
140 181
141 182 public function populateTermsWithPostIds( $setId, $post_type )
142 183 {
143 184 $termTaxonomyIds = [];
185 + $termIds = [];
144 186 $termPosts = [];
145 187 $the_filter = [];
146 188 $allWpQueriedPostIds = [];
147 189 $em = Container::instance()->getEntityManager();
@@ -155,40 +197,70 @@
155 197 break;
156 198 }
157 199 }
158 200
159 - foreach ( $this->getAllExistingTerms() as $term ){
201 + foreach ( $this->getAllExistingTerms() as $term ) {
160 202 $termTaxonomyIds[] = $term->term_taxonomy_id;
203 + $termIds[] = $term->term_id;
161 204 }
162 205
163 206
164 207 if( ! empty( $the_filter ) ){
165 - $termPosts = $this->getTermTaxonomyPostsIds( $termTaxonomyIds, $the_filter );
208 + $termPosts = $this->getTermTaxonomyPostsIds( $termTaxonomyIds, $termIds, $the_filter );
166 209 }
167 210
168 -
169 - if( $this->getName() === 'product_shipping_class' ){
170 - foreach ( $termPosts as $term_id => $the_posts ){
211 + if( $this->getName() === 'product_shipping_class' ) {
212 + foreach ( $termPosts as $term_id => $the_posts ) {
171 213 $termPosts[$term_id] = apply_filters( 'wpc_from_variations_to_products', $the_posts );
172 214 }
173 215 }
174 216
175 - $allWpQueriedPostIds = array_flip($allWpQueriedPostIds);
217 + $wpManager = Container::instance()->getWpManager();
218 + $wp_queried_object = $wpManager->getQueryVar( 'wp_queried_object' );
219 + $wp_queried_term_id = ( isset( $wp_queried_object['term_id'] ) && $wp_queried_object['term_id'] > 0 ) ? $wp_queried_object['term_id'] : 0;
176 220
177 - foreach( $this->items as $index => $term ){
178 - if( isset( $termPosts[$term->term_taxonomy_id] ) ){
221 + // On shops that list variations as standalone products (e.g. XStore's
222 + // "variable_products_detach") the set universe contains VARIATIONS while
223 + // the variable parents are excluded via post__not_in. Term relationships,
224 + // however, live on the parents — a plain intersection below would drop
225 + // them and zero out every taxonomy counter. Treat a parent as
226 + // "in universe" when any of its variations is: map the in-universe
227 + // variations back to their parents (PRO hooks mapVariationIdsToParentIds
228 + // here; in the free build the filter is a no-op) and union both lists.
229 + // NOTE: this deliberately uses the UNGATED mapping filter — the parent
230 + // identity is needed here regardless of how the catalog lists its items,
231 + // while wpc_from_variations_to_products is a counting-time collapse that
232 + // stays off on variations-as-products shops.
233 + // The wpc_items_before_calc_term_count replacement later turns parents
234 + // back into their variations, and calcTermCount() intersects against the
235 + // real universe again, so the final counts stay exact. On sites whose
236 + // universe holds no variation IDs the union changes nothing.
237 + $universeParents = apply_filters( 'wpc_variations_to_parents_always', $allWpQueriedPostIds );
238 + if ( is_array( $universeParents ) && $universeParents !== $allWpQueriedPostIds ) {
239 + $allWpQueriedPostIds = array_merge( $allWpQueriedPostIds, $universeParents );
240 + }
241 +
242 + $allWpQueriedPostIds = array_flip( $allWpQueriedPostIds );
243 +
244 + foreach( $this->items as $index => $term ) {
245 + if( isset( $termPosts[$term->term_id] ) ) {
179 246 $intersected_posts = [];
180 - foreach ( $termPosts[$term->term_taxonomy_id] as $post_id ){
181 - if(isset( $allWpQueriedPostIds[$post_id] )){
247 + foreach ( $termPosts[$term->term_id] as $post_id ){
248 + if( isset( $allWpQueriedPostIds[$post_id] ) ){
182 249 $intersected_posts[] = $post_id;
183 250 }
184 251 }
185 252 $this->items[$index]->posts = $intersected_posts;
186 253 }else{
254 + // Here could be items that have no posts, but their descendants have
187 255 $this->items[$index]->posts = [];
188 256 }
257 +
258 + if( $wp_queried_term_id === $this->items[$index]->term_id ){
259 + $this->items[$index]->wp_queried = true;
260 + }
261 +
189 262 }
190 -
191 263 }
192 264
193 265 public function getTerms()
194 266 {
@@ -264,11 +336,12 @@
264 336
265 337 if( empty( $this->items ) || $force ){
266 338
267 339 $args = array(
268 - 'taxonomy' => $this->entityName,
269 - 'hide_empty' => false,
270 - 'order' => 'ASC'
340 + 'taxonomy' => $this->getName(),
341 + 'hide_empty' => false,
342 + 'orderby' => 'none',
343 + 'order' => 'ASC'
271 344 );
272 345
273 346 /**
274 347 * Filter terms query $args to allow handle cases with some specific taxonomies
@@ -281,11 +354,13 @@
281 354
282 355 if( ! empty( $result ) && ! is_wp_error( $result ) ) {
283 356
284 357 foreach ($result as $i => $termObject) {
285 - $termObject->name = apply_filters('wpc_filter_' . $this->getName() . '_term_name', $termObject->name, $termObject);
358 + $termObject->name = apply_filters( 'wpc_filter_' . $this->getName() . '_term_name', $termObject->name, $termObject );
286 359 $termObject->cross_count = 0;
287 360 $termObject->posts = [];
361 + $termObject->wp_queried = false;
362 +
288 363 $termsUpdated[$i] = $termObject;
289 364
290 365 if( ! empty( $termObject->parent ) ) {
291 366 $children_terms[$termObject->parent][] = $termObject->term_id;
@@ -292,9 +367,8 @@
292 367 }
293 368 }
294 369 // Solution for 'include_children=true' problem and parent counts
295 370 $this->descendants = flrt_find_all_descendants( $children_terms );
296 -
297 371 $this->items = $termsUpdated;
298 372 }
299 373
300 374 }
@@ -347,10 +421,10 @@
347 421 return false;
348 422 }
349 423
350 424 private function isTheSameTaxQuery( $tax_query_1, $tax_query_2 ){
351 - $tax_query_1 = $this->normalizeTaxQueryArray($tax_query_1);
352 - $tax_query_2 = $this->normalizeTaxQueryArray($tax_query_2);
425 + $tax_query_1 = $this->normalizeTaxQueryArray( $tax_query_1 );
426 + $tax_query_2 = $this->normalizeTaxQueryArray( $tax_query_2 );
353 427
354 428 $diff = array_diff( $tax_query_1, $tax_query_2 );
355 429
356 430 if ( empty( $diff ) ){
@@ -362,9 +436,9 @@
362 436
363 437 private function normalizeTaxQueryArray( $tax_query ){
364 438 $normalized_tax_query = [];
365 439
366 - if( ! is_array( $tax_query ) || ! isset( $tax_query['taxonomy'] ) ){
440 + if( ! is_array( $tax_query ) || ! isset( $tax_query['taxonomy'] ) ) {
367 441 return false;
368 442 }
369 443
370 444 if( is_array( $tax_query['terms'] ) ){
@@ -384,8 +458,17 @@
384 458
385 459 return $normalized_tax_query;
386 460 }
387 461
462 + private function hasNestedQueries( $tax_query )
463 + {
464 + if( isset( $tax_query[0]['taxonomy'] ) ){
465 + return true;
466 + }
467 +
468 + return false;
469 + }
470 +
388 471 private function addTaxQueryArray( $tax_query_array ){
389 472 if( ! isset( $tax_query_array['taxonomy'] ) ){
390 473 return false;
391 474 }
@@ -406,8 +489,9 @@
406 489 public function addTermsToWpQuery($queried_value, $wp_query ){
407 490 /**
408 491 * @feature Include children should be optionally configured in Settings. Maybe.
409 492 */
493 + //@bug WordPress bug if slug is '0' SQL query is wrong
410 494 $args = array(
411 495 'taxonomy' => $queried_value['e_name'],
412 496 'field' => 'slug',
413 497 'terms' => $queried_value['values'],
@@ -412,8 +496,40 @@
412 496 'field' => 'slug',
413 497 'terms' => $queried_value['values'],
414 498 );
415 499
500 + /**
501 + * On multilingual sites, resolve slugs to term IDs of the current language
502 + * before querying.
503 + *
504 + * With Polylang / WPML a single slug can belong to several terms in
505 + * different languages (e.g. an English "leather" term and a Latvian one
506 + * sharing the slug "leather"). Querying by slug then matches the
507 + * wrong-language term, which makes Polylang fire a canonical 301 redirect.
508 + * getTermId() resolves against getAllExistingTerms(), which is already
509 + * scoped to the active language, so the query targets the right term.
510 + * Falls back to slug matching if any value can't be resolved.
511 + *
512 + * Gated to multilingual setups so single-language sites keep the original
513 + * slug-based path untouched (no extra term lookups).
514 + */
515 + if ( function_exists( 'pll_current_language' ) || flrt_wpml_active() ) {
516 + $term_ids = array();
517 + foreach ( (array) $queried_value['values'] as $term_slug ) {
518 + $term_id = $this->getTermId( $term_slug );
519 + if ( ! $term_id ) {
520 + $term_ids = array();
521 + break;
522 + }
523 + $term_ids[] = $term_id;
524 + }
525 +
526 + if ( ! empty( $term_ids ) ) {
527 + $args['field'] = 'term_id';
528 + $args['terms'] = $term_ids;
529 + }
530 + }
531 +
416 532 if( isset( $queried_value['logic'] ) && $queried_value['logic'] === 'and' ){
417 533 $args['include_children'] = false;
418 534 }
419 535
@@ -430,8 +546,13 @@
430 546 $already_existing_tax_query = $wp_query->get('tax_query');
431 547
432 548 if( is_array( $already_existing_tax_query ) ){
433 549 foreach( $already_existing_tax_query as $value ){
550 + if( $this->hasNestedQueries( $value ) ){
551 + foreach( $value as $n => $nested_tax_query ){
552 + $this->addTaxQueryArray( $nested_tax_query );
553 + }
554 + }
434 555 $this->addTaxQueryArray( $value );
435 556 }
436 557 }
437 558