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 +241 -58 1.2.41.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,53 +59,115 @@
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 )
83 + public function getTermTaxonomyPostsIds( $termTaxonomyIds, $termIds, $filter )
70 84 {
71 85 global $wpdb;
86 + $include_variation_atts = false;
87 + $ids = [];
72 88
73 - $query[] = "SELECT DISTINCT {$wpdb->term_relationships}.term_taxonomy_id,{$wpdb->term_relationships}.object_id";
74 - $query[] = "FROM {$wpdb->term_relationships}";
75 - $query[] = "WHERE {$wpdb->term_relationships}.term_taxonomy_id IN ('" . implode("','", $termTaxonomyIds) . "')";
89 + if(! isset( $filter['slug'] ) ){
90 + return $ids;
91 + }
76 92
77 - $query = implode(' ', $query);
93 + // Check if it is already stored
94 + $transient_key = flrt_get_post_ids_transient_key( $filter['slug'] );
78 95
79 - $results = $wpdb->get_results($query, ARRAY_A);
80 - $results = apply_filters( 'wpc_term_taxonomy_terms', $results, $this );
96 + $ids = flrt_get_transient( $transient_key );
81 97
82 - $ids = [];
83 - foreach ($results as $key => $result) {
84 - $ids[$result['term_taxonomy_id']][] = $result['object_id'];
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 + }
85 108 }
86 109
87 - $ids = apply_filters( 'wpc_term_taxonomy_post_ids', $ids, $this );
110 + if ( ! $is_v2 ) {
88 111
112 + // It wasn't there, so regenerate the data and save the transient
113 + if( defined('FLRT_FILTERS_PRO') && FLRT_FILTERS_PRO ) {
114 + if( strpos( $this->getName(), 'pa_' ) === 0 ) {
115 + $include_variation_atts = true;
116 + }
117 + }
118 +
119 + $query[] = "SELECT DISTINCT {$wpdb->term_relationships}.term_taxonomy_id";
120 + $query[] = ", {$wpdb->term_relationships}.object_id";
121 + $query[] = ", tt.term_id";
122 +
123 + if( $include_variation_atts ){
124 + $query[] = ", tm.slug";
125 + }
126 +
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 )";
130 +
131 + if( $include_variation_atts ){
132 + $query[] = "LEFT JOIN {$wpdb->terms} AS tm";
133 + $query[] = "ON ( tt.term_id = tm.term_id )";
134 + }
135 +
136 + $query[] = "WHERE {$wpdb->term_relationships}.term_taxonomy_id IN ('" . implode("','", $termTaxonomyIds) . "')";
137 +
138 + $query = implode(' ', $query);
139 +
140 + $results = $wpdb->get_results($query, ARRAY_A);
141 +
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 );
154 + }
155 +
156 + // Add possible empty terms without posts
157 + foreach( $termIds as $term_id ){
158 + if( ! isset( $ids[$term_id] ) ){
159 + $ids[$term_id] = [];
160 + }
161 + }
162 +
89 163 // Fix for counts for parent, when 'include_children=true'
90 164 // Add posts from children terms to their parents
91 165 // To make correct counts for their parents
92 - if( ! empty( $this->descendants ) ){
93 - $parents = array_keys( $this->descendants );
94 -
166 + if( ! empty( $this->descendants ) && $filter['logic'] === 'or' ){
167 + // array of parents term_ids, that have children
95 168 foreach ( $ids as $term_id => $post_ids_array ){
96 - if( in_array( $term_id, $parents ) ){
169 + if( isset( $this->descendants[$term_id] ) ){
97 170 foreach ( $this->descendants[$term_id] as $child_term_id ){
98 171 if( isset( $ids[$child_term_id] ) ){
99 172 $ids[$term_id] = array_unique( array_merge( $ids[$term_id], $ids[$child_term_id] ) );
100 173 }
@@ -108,25 +181,86 @@
108 181
109 182 public function populateTermsWithPostIds( $setId, $post_type )
110 183 {
111 184 $termTaxonomyIds = [];
185 + $termIds = [];
186 + $termPosts = [];
187 + $the_filter = [];
188 + $allWpQueriedPostIds = [];
112 189 $em = Container::instance()->getEntityManager();
113 190 $allWpQueriedPostIds = $em->getAllSetWpQueriedPostIds( $setId );
114 191
115 - foreach ( $this->getAllExistingTerms() as $term ){
192 + $relatedFilters = $em->getSetsRelatedFilters( array( array( 'ID' => $setId) ) );
193 +
194 + foreach ( $relatedFilters as $filter ){
195 + if( isset( $filter['e_name'] ) && $filter['e_name'] === $this->getName() ){
196 + $the_filter = $filter;
197 + break;
198 + }
199 + }
200 +
201 + foreach ( $this->getAllExistingTerms() as $term ) {
116 202 $termTaxonomyIds[] = $term->term_taxonomy_id;
203 + $termIds[] = $term->term_id;
117 204 }
118 205
119 - $termPosts = $this->getTermTaxonomyPostsIds( $termTaxonomyIds );
120 206
121 - foreach( $this->items as $index => $term ){
122 - if( isset( $termPosts[$term->term_taxonomy_id] ) ){
123 - $this->items[$index]->posts = array_intersect( $allWpQueriedPostIds, $termPosts[$term->term_taxonomy_id] );
207 + if( ! empty( $the_filter ) ){
208 + $termPosts = $this->getTermTaxonomyPostsIds( $termTaxonomyIds, $termIds, $the_filter );
209 + }
210 +
211 + if( $this->getName() === 'product_shipping_class' ) {
212 + foreach ( $termPosts as $term_id => $the_posts ) {
213 + $termPosts[$term_id] = apply_filters( 'wpc_from_variations_to_products', $the_posts );
214 + }
215 + }
216 +
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;
220 +
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] ) ) {
246 + $intersected_posts = [];
247 + foreach ( $termPosts[$term->term_id] as $post_id ){
248 + if( isset( $allWpQueriedPostIds[$post_id] ) ){
249 + $intersected_posts[] = $post_id;
250 + }
251 + }
252 + $this->items[$index]->posts = $intersected_posts;
124 253 }else{
254 + // Here could be items that have no posts, but their descendants have
125 255 $this->items[$index]->posts = [];
126 256 }
257 +
258 + if( $wp_queried_term_id === $this->items[$index]->term_id ){
259 + $this->items[$index]->wp_queried = true;
260 + }
261 +
127 262 }
128 -
129 263 }
130 264
131 265 public function getTerms()
132 266 {
@@ -202,11 +336,12 @@
202 336
203 337 if( empty( $this->items ) || $force ){
204 338
205 339 $args = array(
206 - 'taxonomy' => $this->entityName,
207 - 'hide_empty' => false,
208 - 'order' => 'ASC'
340 + 'taxonomy' => $this->getName(),
341 + 'hide_empty' => false,
342 + 'orderby' => 'none',
343 + 'order' => 'ASC'
209 344 );
210 345
211 346 /**
212 347 * Filter terms query $args to allow handle cases with some specific taxonomies
@@ -219,9 +354,13 @@
219 354
220 355 if( ! empty( $result ) && ! is_wp_error( $result ) ) {
221 356
222 357 foreach ($result as $i => $termObject) {
223 - $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 );
359 + $termObject->cross_count = 0;
360 + $termObject->posts = [];
361 + $termObject->wp_queried = false;
362 +
224 363 $termsUpdated[$i] = $termObject;
225 364
226 365 if( ! empty( $termObject->parent ) ) {
227 366 $children_terms[$termObject->parent][] = $termObject->term_id;
@@ -228,9 +367,8 @@
228 367 }
229 368 }
230 369 // Solution for 'include_children=true' problem and parent counts
231 370 $this->descendants = flrt_find_all_descendants( $children_terms );
232 -
233 371 $this->items = $termsUpdated;
234 372 }
235 373
236 374 }
@@ -283,10 +421,10 @@
283 421 return false;
284 422 }
285 423
286 424 private function isTheSameTaxQuery( $tax_query_1, $tax_query_2 ){
287 - $tax_query_1 = $this->normalizeTaxQueryArray($tax_query_1);
288 - $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 );
289 427
290 428 $diff = array_diff( $tax_query_1, $tax_query_2 );
291 429
292 430 if ( empty( $diff ) ){
@@ -298,9 +436,9 @@
298 436
299 437 private function normalizeTaxQueryArray( $tax_query ){
300 438 $normalized_tax_query = [];
301 439
302 - if( ! is_array( $tax_query ) || ! isset( $tax_query['taxonomy'] ) ){
440 + if( ! is_array( $tax_query ) || ! isset( $tax_query['taxonomy'] ) ) {
303 441 return false;
304 442 }
305 443
306 444 if( is_array( $tax_query['terms'] ) ){
@@ -307,9 +445,11 @@
307 445 sort( $tax_query['terms'] );
308 446 }
309 447
310 448 $normalized_tax_query['taxonomy'] = $tax_query['taxonomy'];
311 - $normalized_tax_query['field'] = $tax_query['field'];
449 + if( isset( $tax_query['field'] ) ){
450 + $normalized_tax_query['field'] = $tax_query['field'];
451 + }
312 452
313 453 if( is_array($tax_query['terms']) ){
314 454 $normalized_tax_query['terms'] = implode( '-', $tax_query['terms'] );
315 455 }else{
@@ -318,8 +458,17 @@
318 458
319 459 return $normalized_tax_query;
320 460 }
321 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 +
322 471 private function addTaxQueryArray( $tax_query_array ){
323 472 if( ! isset( $tax_query_array['taxonomy'] ) ){
324 473 return false;
325 474 }
@@ -337,19 +486,12 @@
337 486 /**
338 487 * @return mixed object WP_Query|string;
339 488 */
340 489 public function addTermsToWpQuery($queried_value, $wp_query ){
341 -
342 - if( $term = $this->isTermAlreadyInQuery( $queried_value, $wp_query ) ){
343 - // It is be better to return 404 result and process it in WpManager
344 - /**
345 - * @todo replace with with just false. Or better - show this only in debug mode.
346 - */
347 - return 'Term already in query';
348 - }
349 490 /**
350 491 * @feature Include children should be optionally configured in Settings. Maybe.
351 492 */
493 + //@bug WordPress bug if slug is '0' SQL query is wrong
352 494 $args = array(
353 495 'taxonomy' => $queried_value['e_name'],
354 496 'field' => 'slug',
355 497 'terms' => $queried_value['values'],
@@ -354,8 +496,44 @@
354 496 'field' => 'slug',
355 497 'terms' => $queried_value['values'],
356 498 );
357 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 +
532 + if( isset( $queried_value['logic'] ) && $queried_value['logic'] === 'and' ){
533 + $args['include_children'] = false;
534 + }
535 +
358 536 $args['operator'] = $this->getSqlLogicOperator( $queried_value );
359 537
360 538 if( isset( $wp_query->tax_query->queries ) && count( $wp_query->tax_query->queries ) ){
361 539 foreach($wp_query->tax_query->queries as $single_tax_query ){
@@ -368,8 +546,13 @@
368 546 $already_existing_tax_query = $wp_query->get('tax_query');
369 547
370 548 if( is_array( $already_existing_tax_query ) ){
371 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 + }
372 555 $this->addTaxQueryArray( $value );
373 556 }
374 557 }
375 558