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 / TaxonomyEntity.php

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

568 lines 19.2 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 TaxonomyEntity implements Entity
10 {
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 = [];
18
19 public $filter = [];
20
21 public $excludedTerms = [];
22
23 public $isInclude = false;
24
25 public $descendants = [];
26
27 private $entityName = '';
28
29 private $new_tax_query = [];
30
31 private $postTypes = [];
32
33 public function __construct( $taxName ){
34 $this->entityName = $taxName;
35 $this->getAllExistingTerms();
36 $this->passTermNames();
37 }
38
39 public function setPostTypes( $postTypes )
40 {
41 $this->postTypes = $postTypes;
42 }
43
44 public function setExcludedTerms( $excludedTerms, $isInclude )
45 {
46 $this->excludedTerms = (array) $excludedTerms;
47 $this->isInclude = $isInclude;
48 }
49
50 public function getName()
51 {
52 return $this->entityName;
53 }
54
55 function excludeTerms( $terms )
56 {
57 $exclude = [];
58
59 if( ! empty( $this->excludedTerms ) ){
60 $exclude = $this->excludedTerms;
61 }
62
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 }
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 }
78 }
79
80 return $terms;
81 }
82
83 public function getTermTaxonomyPostsIds( $termTaxonomyIds, $termIds, $filter )
84 {
85 global $wpdb;
86 $include_variation_atts = false;
87 $ids = [];
88
89 if(! isset( $filter['slug'] ) ){
90 return $ids;
91 }
92
93 // Check if it is already stored
94 $transient_key = flrt_get_post_ids_transient_key( $filter['slug'] );
95
96 $ids = flrt_get_transient( $transient_key );
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
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
163 // Fix for counts for parent, when 'include_children=true'
164 // Add posts from children terms to their parents
165 // To make correct counts for their parents
166 if( ! empty( $this->descendants ) && $filter['logic'] === 'or' ){
167 // array of parents term_ids, that have children
168 foreach ( $ids as $term_id => $post_ids_array ){
169 if( isset( $this->descendants[$term_id] ) ){
170 foreach ( $this->descendants[$term_id] as $child_term_id ){
171 if( isset( $ids[$child_term_id] ) ){
172 $ids[$term_id] = array_unique( array_merge( $ids[$term_id], $ids[$child_term_id] ) );
173 }
174 }
175 }
176 }
177 }
178
179 return $ids;
180 }
181
182 public function populateTermsWithPostIds( $setId, $post_type )
183 {
184 $termTaxonomyIds = [];
185 $termIds = [];
186 $termPosts = [];
187 $the_filter = [];
188 $allWpQueriedPostIds = [];
189 $em = Container::instance()->getEntityManager();
190 $allWpQueriedPostIds = $em->getAllSetWpQueriedPostIds( $setId );
191
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 ) {
202 $termTaxonomyIds[] = $term->term_taxonomy_id;
203 $termIds[] = $term->term_id;
204 }
205
206
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;
253 }else{
254 // Here could be items that have no posts, but their descendants have
255 $this->items[$index]->posts = [];
256 }
257
258 if( $wp_queried_term_id === $this->items[$index]->term_id ){
259 $this->items[$index]->wp_queried = true;
260 }
261
262 }
263 }
264
265 public function getTerms()
266 {
267 return $this->excludeTerms( $this->getAllExistingTerms() );
268 }
269
270 /**
271 * @param int $id term id
272 * @return false|object term object of false
273 */
274 public function getTerm( $id ){
275 if( ! $id ){
276 return false;
277 }
278
279 foreach ( $this->getAllExistingTerms() as $term ){
280 if( $id == $term->term_id ){
281 return $term;
282 }
283 }
284
285 return false;
286 }
287
288 public function getTermId( $termSlug )
289 {
290 foreach ( $this->getAllExistingTerms() as $term ){
291 if( $termSlug == $term->slug ){
292 return $term->term_id;
293 }
294 }
295
296 return false;
297 }
298
299 /**
300 * @return array list of term_id and names useful to create Select dropdown
301 */
302 public function getTermsForSelect( $optionGroup = false )
303 {
304 $toSelect = [];
305 foreach ( $this->getTerms() as $term ) {
306 if( $optionGroup ){
307 $key = $term->taxonomy.":".$term->term_id;
308 $toSelect[$key] = $term->name;
309 }else{
310 $toSelect[$term->term_id] = $term->name;
311 }
312
313 }
314
315 return $toSelect;
316 }
317
318 public function getTermsForSelect2()
319 {
320 $toSelect = [];
321 foreach ( $this->getTerms() as $term ) {
322 $toSelect[] = array( 'id' => $term->term_id, 'text' =>$term->name );
323 }
324 return $toSelect;
325 }
326
327 public function passTermNames()
328 {
329 foreach ($this->getAllExistingTerms() as $index => $term ) {
330 $this->items[$index]->name = apply_filters( 'wpc_filter_taxonomy_term_name', $term->name, $this->getName() );
331 }
332 }
333
334 function getAllExistingTerms( $force = false )
335 {
336
337 if( empty( $this->items ) || $force ){
338
339 $args = array(
340 'taxonomy' => $this->getName(),
341 'hide_empty' => false,
342 'orderby' => 'none',
343 'order' => 'ASC'
344 );
345
346 /**
347 * Filter terms query $args to allow handle cases with some specific taxonomies
348 */
349 $args = apply_filters( 'wpc_filter_term_query_args', $args, 'taxonomy', $this->getName() );
350 $result = apply_filters( 'wpc_filter_get_taxonomy_terms', get_terms( $args ), $this->getName() );
351
352 $termsUpdated = [];
353 $children_terms = [];
354
355 if( ! empty( $result ) && ! is_wp_error( $result ) ) {
356
357 foreach ($result as $i => $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
363 $termsUpdated[$i] = $termObject;
364
365 if( ! empty( $termObject->parent ) ) {
366 $children_terms[$termObject->parent][] = $termObject->term_id;
367 }
368 }
369 // Solution for 'include_children=true' problem and parent counts
370 $this->descendants = flrt_find_all_descendants( $children_terms );
371 $this->items = $termsUpdated;
372 }
373
374 }
375
376 return $this->items;
377 }
378
379 private function getSqlLogicOperator( $filter ){
380 if( $filter['logic'] === 'and' ){
381 return 'AND';
382 } else {
383 return 'IN';
384 }
385 }
386
387 public function isTermAlreadyInQuery( $queried_value, $wp_query ){
388 $duplicate = [];
389
390 if ( ! empty( $wp_query->tax_query->queried_terms ) ) {
391 $native_query_terms = $wp_query->tax_query->queried_terms;
392 $queried_taxonomies = array_keys( $native_query_terms );
393
394 foreach ( $queried_taxonomies as $q_taxonomy ) {
395 if( $q_taxonomy === $queried_value['e_name'] ){
396 $query = $native_query_terms[$q_taxonomy];
397
398 if ( ! empty( $query['terms'] ) ) {
399 if ( 'term_id' == $query['field'] ) {
400 $term = get_term( reset( $query['terms'] ), $q_taxonomy );
401 } else {
402 $term = get_term_by( $query['field'], reset( $query['terms'] ), $q_taxonomy );
403 }
404
405 if( ! $term || is_wp_error( $term ) ){
406 return false;
407 }
408
409 foreach( $queried_value['values'] as $filter_slug ){
410 if( $filter_slug === $term->slug ){
411 $duplicate['taxonomy'] = $q_taxonomy;
412 $duplicate['term'] = $term->slug;
413 return $duplicate;
414 }
415 }
416 }
417 }
418 }
419 }
420
421 return false;
422 }
423
424 private function isTheSameTaxQuery( $tax_query_1, $tax_query_2 ){
425 $tax_query_1 = $this->normalizeTaxQueryArray( $tax_query_1 );
426 $tax_query_2 = $this->normalizeTaxQueryArray( $tax_query_2 );
427
428 $diff = array_diff( $tax_query_1, $tax_query_2 );
429
430 if ( empty( $diff ) ){
431 return true;
432 }
433
434 return false;
435 }
436
437 private function normalizeTaxQueryArray( $tax_query ){
438 $normalized_tax_query = [];
439
440 if( ! is_array( $tax_query ) || ! isset( $tax_query['taxonomy'] ) ) {
441 return false;
442 }
443
444 if( is_array( $tax_query['terms'] ) ){
445 sort( $tax_query['terms'] );
446 }
447
448 $normalized_tax_query['taxonomy'] = $tax_query['taxonomy'];
449 if( isset( $tax_query['field'] ) ){
450 $normalized_tax_query['field'] = $tax_query['field'];
451 }
452
453 if( is_array($tax_query['terms']) ){
454 $normalized_tax_query['terms'] = implode( '-', $tax_query['terms'] );
455 }else{
456 $normalized_tax_query['terms'] = $tax_query['terms'];
457 }
458
459 return $normalized_tax_query;
460 }
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
471 private function addTaxQueryArray( $tax_query_array ){
472 if( ! isset( $tax_query_array['taxonomy'] ) ){
473 return false;
474 }
475
476 $existing_tax_query = $this->new_tax_query;
477 foreach( $existing_tax_query as $index => $present_query ){
478 if( $this->isTheSameTaxQuery( $present_query, $tax_query_array ) ){
479 return false;
480 }
481 }
482
483 $this->new_tax_query[] = $tax_query_array;
484 }
485
486 /**
487 * @return mixed object WP_Query|string;
488 */
489 public function addTermsToWpQuery($queried_value, $wp_query ){
490 /**
491 * @feature Include children should be optionally configured in Settings. Maybe.
492 */
493 //@bug WordPress bug if slug is '0' SQL query is wrong
494 $args = array(
495 'taxonomy' => $queried_value['e_name'],
496 'field' => 'slug',
497 'terms' => $queried_value['values'],
498 );
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
536 $args['operator'] = $this->getSqlLogicOperator( $queried_value );
537
538 if( isset( $wp_query->tax_query->queries ) && count( $wp_query->tax_query->queries ) ){
539 foreach($wp_query->tax_query->queries as $single_tax_query ){
540 $this->addTaxQueryArray( $single_tax_query );
541 }
542 }
543
544 $this->addTaxQueryArray( $args );
545
546 $already_existing_tax_query = $wp_query->get('tax_query');
547
548 if( is_array( $already_existing_tax_query ) ){
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 }
555 $this->addTaxQueryArray( $value );
556 }
557 }
558
559 if( count($this->new_tax_query) > 1 ){
560 $this->new_tax_query['relation'] = 'AND';
561 }
562
563 $wp_query->set('tax_query', $this->new_tax_query );
564 $this->new_tax_query = [];
565
566 return $wp_query;
567 }
568 }