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 +138 -40 1.6.11.9.7 View file →
@@ -1,28 +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 $isInclude = false;
21 + public $excludedTerms = [];
16 22
17 - public $descendants = [];
23 + public $isInclude = false;
18 24
19 - private $entityName = '';
25 + public $descendants = [];
20 26
21 - private $new_tax_query = [];
27 + private $entityName = '';
22 28
23 - private $postTypes = [];
29 + private $new_tax_query = [];
24 30
31 + private $postTypes = [];
32 +
25 33 public function __construct( $taxName ){
26 34 $this->entityName = $taxName;
27 35 $this->getAllExistingTerms();
28 36 $this->passTermNames();
@@ -71,9 +79,9 @@
71 79
72 80 return $terms;
73 81 }
74 82
75 - public function getTermTaxonomyPostsIds( $termTaxonomyIds, $filter )
83 + public function getTermTaxonomyPostsIds( $termTaxonomyIds, $termIds, $filter )
76 84 {
77 85 global $wpdb;
78 86 $include_variation_atts = false;
79 87 $ids = [];
@@ -84,10 +92,24 @@
84 92
85 93 // Check if it is already stored
86 94 $transient_key = flrt_get_post_ids_transient_key( $filter['slug'] );
87 95
88 - if ( false === ( $results = get_transient( $transient_key ) ) ) {
96 + $ids = flrt_get_transient( $transient_key );
89 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 +
90 112 // It wasn't there, so regenerate the data and save the transient
91 113 if( defined('FLRT_FILTERS_PRO') && FLRT_FILTERS_PRO ) {
92 114 if( strpos( $this->getName(), 'pa_' ) === 0 ) {
93 115 $include_variation_atts = true;
@@ -93,9 +115,11 @@
93 115 $include_variation_atts = true;
94 116 }
95 117 }
96 118
97 - $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";
98 122
99 123 if( $include_variation_atts ){
100 124 $query[] = ", tm.slug";
101 125 }
@@ -100,12 +124,12 @@
100 124 $query[] = ", tm.slug";
101 125 }
102 126
103 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 )";
104 130
105 131 if( $include_variation_atts ){
106 - $query[] = "LEFT JOIN {$wpdb->term_taxonomy} AS tt";
107 - $query[] = "ON ( {$wpdb->term_relationships}.term_taxonomy_id = tt.term_taxonomy_id )";
108 132 $query[] = "LEFT JOIN {$wpdb->terms} AS tm";
109 133 $query[] = "ON ( tt.term_id = tm.term_id )";
110 134 }
111 135
@@ -114,19 +138,24 @@
114 138 $query = implode(' ', $query);
115 139
116 140 $results = $wpdb->get_results($query, ARRAY_A);
117 141
118 - set_transient( $transient_key, $results, FLRT_TRANSIENT_PERIOD_HOURS * HOUR_IN_SECONDS );
119 - }
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 );
120 146
121 - $taxonomy_terms = apply_filters( 'wpc_term_taxonomy_terms', $results, $this );
147 + $ids = [];
148 + foreach ($taxonomy_terms as $key => $result) {
149 + $ids[ (int) $result['term_id'] ][] = (int) $result['object_id'];
150 + }
151 + unset( $taxonomy_terms );
122 152
123 - foreach ($taxonomy_terms as $key => $result) {
124 - $ids[$result['term_taxonomy_id']][] = (int) $result['object_id'];
153 + flrt_set_transient( $transient_key, $ids, FLRT_TRANSIENT_PERIOD_HOURS * HOUR_IN_SECONDS );
125 154 }
126 155
127 156 // Add possible empty terms without posts
128 - foreach( $termTaxonomyIds as $term_id ){
157 + foreach( $termIds as $term_id ){
129 158 if( ! isset( $ids[$term_id] ) ){
130 159 $ids[$term_id] = [];
131 160 }
132 161 }
@@ -152,8 +181,9 @@
152 181
153 182 public function populateTermsWithPostIds( $setId, $post_type )
154 183 {
155 184 $termTaxonomyIds = [];
185 + $termIds = [];
156 186 $termPosts = [];
157 187 $the_filter = [];
158 188 $allWpQueriedPostIds = [];
159 189 $em = Container::instance()->getEntityManager();
@@ -167,19 +197,20 @@
167 197 break;
168 198 }
169 199 }
170 200
171 - foreach ( $this->getAllExistingTerms() as $term ){
201 + foreach ( $this->getAllExistingTerms() as $term ) {
172 202 $termTaxonomyIds[] = $term->term_taxonomy_id;
203 + $termIds[] = $term->term_id;
173 204 }
174 205
175 206
176 207 if( ! empty( $the_filter ) ){
177 - $termPosts = $this->getTermTaxonomyPostsIds( $termTaxonomyIds, $the_filter );
208 + $termPosts = $this->getTermTaxonomyPostsIds( $termTaxonomyIds, $termIds, $the_filter );
178 209 }
179 210
180 - if( $this->getName() === 'product_shipping_class' ){
181 - foreach ( $termPosts as $term_id => $the_posts ){
211 + if( $this->getName() === 'product_shipping_class' ) {
212 + foreach ( $termPosts as $term_id => $the_posts ) {
182 213 $termPosts[$term_id] = apply_filters( 'wpc_from_variations_to_products', $the_posts );
183 214 }
184 215 }
185 216
@@ -184,22 +215,44 @@
184 215 }
185 216
186 217 $wpManager = Container::instance()->getWpManager();
187 218 $wp_queried_object = $wpManager->getQueryVar( 'wp_queried_object' );
188 - $wp_queried_term_id = (isset( $wp_queried_object['term_id'] ) && $wp_queried_object['term_id'] > 0) ? $wp_queried_object['term_id'] : 0;
219 + $wp_queried_term_id = ( isset( $wp_queried_object['term_id'] ) && $wp_queried_object['term_id'] > 0 ) ? $wp_queried_object['term_id'] : 0;
189 220
190 - $allWpQueriedPostIds = array_flip($allWpQueriedPostIds);
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 + }
191 241
192 - foreach( $this->items as $index => $term ){
193 - if( isset( $termPosts[$term->term_taxonomy_id] ) ){
242 + $allWpQueriedPostIds = array_flip( $allWpQueriedPostIds );
243 +
244 + foreach( $this->items as $index => $term ) {
245 + if( isset( $termPosts[$term->term_id] ) ) {
194 246 $intersected_posts = [];
195 - foreach ( $termPosts[$term->term_taxonomy_id] as $post_id ){
196 - if(isset( $allWpQueriedPostIds[$post_id] )){
247 + foreach ( $termPosts[$term->term_id] as $post_id ){
248 + if( isset( $allWpQueriedPostIds[$post_id] ) ){
197 249 $intersected_posts[] = $post_id;
198 250 }
199 251 }
200 252 $this->items[$index]->posts = $intersected_posts;
201 253 }else{
254 + // Here could be items that have no posts, but their descendants have
202 255 $this->items[$index]->posts = [];
203 256 }
204 257
205 258 if( $wp_queried_term_id === $this->items[$index]->term_id ){
@@ -206,9 +259,8 @@
206 259 $this->items[$index]->wp_queried = true;
207 260 }
208 261
209 262 }
210 -
211 263 }
212 264
213 265 public function getTerms()
214 266 {
@@ -284,12 +336,12 @@
284 336
285 337 if( empty( $this->items ) || $force ){
286 338
287 339 $args = array(
288 - 'taxonomy' => $this->entityName,
289 - 'hide_empty' => false,
290 - 'orderby' => 'none',
291 - 'order' => 'ASC'
340 + 'taxonomy' => $this->getName(),
341 + 'hide_empty' => false,
342 + 'orderby' => 'none',
343 + 'order' => 'ASC'
292 344 );
293 345
294 346 /**
295 347 * Filter terms query $args to allow handle cases with some specific taxonomies
@@ -302,9 +354,9 @@
302 354
303 355 if( ! empty( $result ) && ! is_wp_error( $result ) ) {
304 356
305 357 foreach ($result as $i => $termObject) {
306 - $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 );
307 359 $termObject->cross_count = 0;
308 360 $termObject->posts = [];
309 361 $termObject->wp_queried = false;
310 362
@@ -315,9 +367,8 @@
315 367 }
316 368 }
317 369 // Solution for 'include_children=true' problem and parent counts
318 370 $this->descendants = flrt_find_all_descendants( $children_terms );
319 -
320 371 $this->items = $termsUpdated;
321 372 }
322 373
323 374 }
@@ -370,10 +421,10 @@
370 421 return false;
371 422 }
372 423
373 424 private function isTheSameTaxQuery( $tax_query_1, $tax_query_2 ){
374 - $tax_query_1 = $this->normalizeTaxQueryArray($tax_query_1);
375 - $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 );
376 427
377 428 $diff = array_diff( $tax_query_1, $tax_query_2 );
378 429
379 430 if ( empty( $diff ) ){
@@ -385,9 +436,9 @@
385 436
386 437 private function normalizeTaxQueryArray( $tax_query ){
387 438 $normalized_tax_query = [];
388 439
389 - if( ! is_array( $tax_query ) || ! isset( $tax_query['taxonomy'] ) ){
440 + if( ! is_array( $tax_query ) || ! isset( $tax_query['taxonomy'] ) ) {
390 441 return false;
391 442 }
392 443
393 444 if( is_array( $tax_query['terms'] ) ){
@@ -407,8 +458,17 @@
407 458
408 459 return $normalized_tax_query;
409 460 }
410 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 +
411 471 private function addTaxQueryArray( $tax_query_array ){
412 472 if( ! isset( $tax_query_array['taxonomy'] ) ){
413 473 return false;
414 474 }
@@ -429,8 +489,9 @@
429 489 public function addTermsToWpQuery($queried_value, $wp_query ){
430 490 /**
431 491 * @feature Include children should be optionally configured in Settings. Maybe.
432 492 */
493 + //@bug WordPress bug if slug is '0' SQL query is wrong
433 494 $args = array(
434 495 'taxonomy' => $queried_value['e_name'],
435 496 'field' => 'slug',
436 497 'terms' => $queried_value['values'],
@@ -435,8 +496,40 @@
435 496 'field' => 'slug',
436 497 'terms' => $queried_value['values'],
437 498 );
438 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 +
439 532 if( isset( $queried_value['logic'] ) && $queried_value['logic'] === 'and' ){
440 533 $args['include_children'] = false;
441 534 }
442 535
@@ -453,8 +546,13 @@
453 546 $already_existing_tax_query = $wp_query->get('tax_query');
454 547
455 548 if( is_array( $already_existing_tax_query ) ){
456 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 + }
457 555 $this->addTaxQueryArray( $value );
458 556 }
459 557 }
460 558