PluginProbe
Filter Everything — WordPress & WooCommerce Filters / 1.9.2.1
Filter Everything — WordPress & WooCommerce Filters v1.9.2.1
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
filter-everything / src / Entities / EntityManager.php

EntityManager.php in Filter Everything — WordPress & WooCommerce Filters 1.9.2.1, at src/Entities/EntityManager.php

1,353 lines 43.6 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 use FilterEverything\Filter\Pro\Entities\PostMetaExistsEntity;
10 use FilterEverything\Filter\Pro\Entities\TaxonomyNumEntity;
11
12 class EntityManager
13 {
14 const DEFAULT_PREFIX = 'filter-';
15
16 private $allConfiguredFilters;
17
18 public function __construct()
19 {
20 $this->allConfiguredFilters = $this->getAllConfiguredFilters();
21 }
22
23 public static function getTaxonomies()
24 {
25 $excludedTaxes = flrt_excluded_taxonomies();
26 $args = [];
27 $taxonomies = get_taxonomies($args, 'objects');
28
29 foreach ($taxonomies as $t => $taxonomy) {
30 if (in_array($taxonomy->name, $excludedTaxes)) {
31 unset($taxonomies[$t]);
32 }
33 }
34
35 return $taxonomies;
36 }
37
38 private function getData( $key )
39 {
40 return Container::instance()->getParam( $key );
41 }
42
43 private function storeData( $key, $data )
44 {
45 Container::instance()->storeParam( $key, $data );
46 }
47
48 /**
49 * @param $key = $entity.'-'.$name
50 */
51 public function createEntity( $key, $postType = '' )
52 {
53 if ( ! $key ) {
54 return false;
55 }
56
57 $fse = Container::instance()->getFilterService();
58 $name = '';
59 $parts = explode( $fse->sep, $key, 2 );
60
61 $entity = $parts[0];
62 $name = $parts[1];
63
64 $storeKey = $key;
65 /**
66 * We add post_type suffix to the key for filters
67 * which terms depend from post_type
68 * */
69 if ( in_array( $entity, array(
70 'post_meta_num',
71 'post_meta_exists',
72 'post_meta',
73 'tax_numeric',
74 'author',
75 'post_date',
76 'post_meta_date',
77 )
78 ) && $postType ) {
79 $storeKey = $key.'_'.$postType;
80 }
81
82 if ( $entityExists = $this->getData( $storeKey ) ) {
83 return $entityExists;
84 }
85
86 switch( $entity ) {
87 case 'taxonomy':
88 $this->storeData( $storeKey, new TaxonomyEntity( $name ) );
89 break;
90
91 case 'post_meta':
92 $this->storeData( $storeKey, new PostMetaEntity( $name, $postType ) );
93 break;
94
95 case 'post_meta_num':
96 $this->storeData( $storeKey, new PostMetaNumEntity( $name, $postType ) );
97 break;
98
99 case 'post_meta_exists':
100 if( class_exists('FilterEverything\Filter\Pro\Entities\PostMetaExistsEntity') ) {
101 $this->storeData($storeKey, new PostMetaExistsEntity( $name, $postType ) );
102 }else{
103 // For the plugin version downgrade compatibility
104 $this->storeData($storeKey, new DefaultEntity( $name ) );
105 }
106 break;
107
108 case 'author':
109 $this->storeData( $storeKey, new AuthorEntity( $name, $postType ) );
110 break;
111
112 case 'tax_numeric':
113 if ( class_exists('FilterEverything\Filter\Pro\Entities\TaxonomyNumEntity') ) {
114 $this->storeData( $storeKey, new TaxonomyNumEntity( $name, $postType ) );
115 } else {
116 // For the plugin version downgrade compatibility
117 $this->storeData($storeKey, new DefaultEntity( $name ) );
118 }
119
120 break;
121
122 case 'post_date':
123 $this->storeData( $storeKey, new PostDateEntity( $name, $postType ) );
124 break;
125
126 case 'post_meta_date':
127 $this->storeData( $storeKey, new PostMetaDateEntity( $name, $postType ) );
128 break;
129 }
130
131 unset( $fse, $parts, $entity, $name );
132
133 return $this->getData( $storeKey );
134 }
135
136 public function getEntityByFilter( $filter, $postType = '' )
137 {
138 $fse = Container::instance()->getFilterService();
139
140 if ( ! isset( $filter['entity'] ) ) {
141 return false;
142 }
143
144 if ( ! isset( $filter['e_name'] ) ) {
145 return false;
146 }
147
148 return $this->createEntity( $fse->getEntityKey( $filter['entity'], $filter['e_name'] ), $postType );
149
150 }
151
152 public function getPossibleTaxonomies()
153 {
154 $entities = [];
155 $args = apply_filters( 'wpc_get_taxonomies_args', [] );
156 $taxonomies = get_taxonomies( $args, 'objects' );
157 $excluded_taxonomies = flrt_excluded_taxonomies();
158
159 foreach ( $taxonomies as $taxonomy ){
160 if( in_array( $taxonomy->name, $excluded_taxonomies ) ){
161 continue;
162 }
163 // It is better to save value as 'taxonomy_pa_size' because
164 // user potentially can create post_meta with the same name
165 $label = ucwords( flrt_ucfirst( mb_strtolower( $taxonomy->label ) ) );
166 if( ! $label ){
167 $label = $taxonomy->name;
168 }
169 $entities[ 'taxonomy_' . $taxonomy->name ] = $label;
170 }
171
172 return $entities;
173 }
174
175 public function getPossibleEntities()
176 {
177 $entities = [];
178
179 $entities['taxonomy']['group_label'] = esc_html__( 'Taxonomy Filters', 'filter-everything' );
180 $entities['taxonomy']['entities'] = $this->getPossibleTaxonomies();
181
182 $other = array(
183 'post_meta' => array(
184 'group_label' => esc_html__('Custom Field Filters', 'filter-everything'),
185 'entities' => array(
186 'post_meta' => esc_html__('Custom Field', 'filter-everything'),
187 'post_meta_num' => esc_html__('Custom Field Numeric', 'filter-everything'),
188 'post_meta_date' => esc_html__('Custom Field Date', 'filter-everything'),
189 'post_meta_exists' => esc_html__('Custom Field Exists - Available in PRO', 'filter-everything'),
190 )
191 ),
192 'other' => array(
193 'group_label' => esc_html__( 'Other Filters', 'filter-everything' ),
194 'entities' => array(
195 'post_date' => esc_html__( 'Post Date', 'filter-everything' ),
196 'author_author' => esc_html__( 'Post Author', 'filter-everything' ),
197 'tax_numeric' => esc_html__( 'Taxonomy Numeric - Available in PRO', 'filter-everything' ),
198 )
199 )
200 );
201
202 $result = array_merge( $entities, $other );
203
204 $result = apply_filters( 'wpc_possible_entities', $result );
205
206 return $result;
207 }
208
209 public function prepareFilterCommon( $entityAndEname, $filters = array() )
210 {
211 $filter = [];
212 $e_name = '';
213 $fs = Container::instance()->getFilterService();
214
215 $allPermalinksSettings = get_option( 'wpc_filter_permalinks', [] );
216 $entityName = explode( $fs->sep, $entityAndEname, 2 );
217
218 $filter['e_name'] = $e_name = $fs->getEntityEname( $entityAndEname );
219 $filter['entity'] = $entityName[0];
220
221 if( isset( $allPermalinksSettings[$entityAndEname] ) && $allPermalinksSettings[$entityAndEname] ){
222 $filter['slug'] = $allPermalinksSettings[$entityAndEname];
223 // This is messed up because label is not good index for this item, but it already exists
224 // And I will try ty change it later. Maybe. Very very maybe :-)
225 $filter['label'] = '{'.$allPermalinksSettings[$entityAndEname].'}';
226 }
227
228 if( isset( $filters[$e_name] ) ){
229 $filter['title'] = $filters[$e_name]['label'];
230 }
231
232 return $filter;
233 }
234
235 public function getCommonFilterValues( $eName, $postType )
236 {
237 $filter = [];
238
239 if( ! $eName || ! $postType ){
240 return false;
241 }
242
243 $fs = Container::instance()->getFilterService();
244 $allIndexedFilters = get_option( 'wpc_seo_rules_settings' );
245
246 foreach ( $allIndexedFilters as $item => $value ) {
247 $itemKey = explode(":", $item, 2 );
248 $maybePostType = $itemKey[0]; // post|product etc
249
250 if( $maybePostType === $postType ){
251 $entityAndEname = $itemKey[1];
252 $entityName = explode( $fs->sep, $entityAndEname, 2 );
253
254 if( $entityName[1] === $eName ){
255 $filter = $this->prepareFilterCommon( $itemKey[1] );
256 break;
257 }
258 }
259 }
260
261 return $filter;
262 }
263
264 public function getFilterByEname( $e_name, $sets = [] )
265 {
266 $container = Container::instance();
267
268 if( ! empty( $sets ) ){
269 $actualSets = $sets;
270 }else{
271 $wpManager = $container->getWpManager();
272 $actualSets = $wpManager->getQueryVar( 'wpc_page_related_set_ids' );
273 }
274
275 if( empty( $actualSets ) || ! $actualSets ){
276 return false;
277 }
278
279 $theFilter = [];
280
281 foreach ( $actualSets as $actualSet ) {
282 $key = $e_name . '_'.$actualSet['ID'];
283 if ( isset( $this->allConfiguredFilters[$key] ) ){
284 $theFilter = $this->allConfiguredFilters[$key];
285 break;
286 }
287 }
288
289 return $theFilter;
290 }
291
292 public function getFilterById( $filter_id )
293 {
294 if( ! $filter_id ) {
295 return [];
296 }
297
298 $theFilter = [];
299 foreach( $this->allConfiguredFilters as $filter ){
300
301 if( $filter['ID'] === $filter_id ){
302 $theFilter = $filter;
303 break;
304 }
305 }
306
307 return $theFilter;
308 }
309
310 /**
311 * @param string $slug filters prefix e.g. 'cat', 'tag'
312 * @param array $onlyKeys optional. Keys from filter array that should be extracted
313 * @return array all matched filters from DB.
314 */
315 public function getAllFiltersBySlug( $slug, $onlyKeys = [] )
316 {
317 $theFilters = [];
318
319 foreach( $this->allConfiguredFilters as $filter ){
320 if( $filter['slug'] === $slug ){
321 $theFilters[] = $filter;
322 }
323 }
324
325 if( ! empty( $onlyKeys ) ){
326 $extractedFilters = [];
327 foreach( $theFilters as $filter ){
328 $extractedFilters[] = flrt_extract_vars( $filter, $onlyKeys );
329 }
330 return $extractedFilters;
331 }
332
333 return $theFilters;
334 }
335
336 public function getFilterBySlug( $slug )
337 {
338 $theFilter = [];
339
340 foreach( $this->allConfiguredFilters as $filter ) {
341 if ( $filter['slug'] === $slug ) {
342 $theFilter = $filter;
343 break;
344 }
345 }
346
347 // if( ! empty( $onlyKeys ) ){
348 // return flrt_extract_vars( $theFilter, $onlyKeys );
349 // }
350
351 return $theFilter;
352 }
353
354 public function getGlobalConfiguredSlugs()
355 {
356 $permalinksTab = new PermalinksTab();
357 return get_option( $permalinksTab->optionName, [] );
358 }
359
360 public function getFlatEntities( $entities = false )
361 {
362 if( ! $entities ){
363 $entities = $this->getPossibleEntities();
364 }
365
366 $flat_entities = [];
367
368 array_walk_recursive( $entities, function ( $value, $key ) use ( &$flat_entities ) {
369 if( $key !== 'group_label' ){
370 // $value = label
371 $flat_entities[ $key ] = $value;
372 }
373 }, $flat_entities );
374
375 return $flat_entities;
376 }
377
378 /**
379 * @return array
380 */
381 public function extractBelongsFilters( $all_filters )
382 {
383 $result = [];
384 if( ! is_array( $all_filters ) ){
385 return $result;
386 }
387
388 $ffs = Container::instance()->getFilterFieldsService();
389
390 foreach( $all_filters as $filter ) {
391 if( $ffs->filterBelongsToPostType( $filter['parent'], $filter['entity'], $filter['e_name'] ) ){
392 $result[ $filter['ID'] ] = $filter;
393 }
394 }
395
396 return $result;
397 }
398
399 public function hasPostTypeFilters( $postType )
400 {
401 global $wpdb;
402
403 $sql[] = "SELECT {$wpdb->posts}.ID FROM {$wpdb->posts}";
404 $sql[] = "WHERE {$wpdb->posts}.post_type = '%s'";
405 $sql[] = "AND {$wpdb->posts}.post_status = 'publish'";
406 $sql[] = "AND {$wpdb->posts}.post_excerpt = '%s'";
407 $sql[] = "LIMIT 0, 1";
408
409 $sql = implode(' ', $sql);
410
411 $query = $wpdb->prepare( $sql, FLRT_FILTERS_SET_POST_TYPE, $postType );
412 $results = $wpdb->get_results( $query, OBJECT );
413
414 if( ! empty( $results ) ){
415 return true;
416 }
417
418 return false;
419 }
420
421 public function getFiltersRelatedWithPostType( $postType, $entityName = '')
422 {
423 global $wpdb;
424
425 $container = Container::instance();
426
427 // There shouldn't be duplicates in entities
428 // Because of validation
429 $sql[] = "SELECT {$wpdb->posts}.ID, {$wpdb->posts}.post_title, {$wpdb->posts}.post_content, {$wpdb->posts}.post_excerpt";
430 $sql[] = "FROM {$wpdb->posts}";
431 $sql[] = "WHERE {$wpdb->posts}.post_type = '%s'";
432 $sql[] = "AND {$wpdb->posts}.post_status = 'publish'";
433
434 $sql[] = "AND {$wpdb->posts}.post_parent IN (";
435 $sql[] = "SELECT {$wpdb->posts}.ID FROM {$wpdb->posts}";
436 $sql[] = "WHERE {$wpdb->posts}.post_type = '%s'";
437 $sql[] = "AND {$wpdb->posts}.post_status = 'publish'";
438 $sql[] = "AND {$wpdb->posts}.post_excerpt = '%s'"; // $postType
439 $sql[] = ")";
440 $sql[] = "ORDER BY {$wpdb->posts}.ID ASC";
441
442 $sql = implode(' ', $sql);
443
444 $query = $wpdb->prepare($sql, FLRT_FILTERS_POST_TYPE, FLRT_FILTERS_SET_POST_TYPE, $postType);
445
446 $key = $postType.'_get_related_filters';
447 $results = $container->getParam( $key );
448
449 if( ! $results ){
450 $results = $wpdb->get_results( $query, OBJECT );
451 if( ! $results ){
452 $results = '-1';
453 }
454 $container->storeParam( $key, $results );
455 }
456
457 if( $results === '-1' ){
458 return [];
459 }
460
461 $filters = [];
462 foreach ( $results as $result ) {
463 $filterData = maybe_unserialize( $result->post_content );
464
465 if( ! is_array( $filterData ) ){
466 continue;
467 }
468
469 if( $entityName && mb_strpos( $result->post_excerpt, $entityName ) === false ){
470 continue;
471 }
472
473 if( isset( $filters[ $filterData['e_name'] ] ) ){
474 continue;
475 }
476
477 $filters[$filterData['e_name']]['ID'] = $result->ID;
478 $filters[$filterData['e_name']]['label'] = $result->post_title;
479
480 $filters[$filterData['e_name']] = array_merge( $filters[$filterData['e_name']], $filterData );
481 }
482
483 return $filters;
484 }
485
486 private function makeFiltersQuery()
487 {
488 $transient_key = 'wpc_filters_query';
489 if ( false === ( $results = flrt_get_transient( $transient_key ) ) ) {
490 global $wpdb;
491
492 $sql[] = "SELECT {$wpdb->posts}.ID, {$wpdb->posts}.post_title, {$wpdb->posts}.post_content,";
493 $sql[] = "{$wpdb->posts}.post_name, {$wpdb->posts}.post_parent, {$wpdb->posts}.menu_order";
494 $sql[] = "FROM {$wpdb->posts}";
495 $sql[] = "WHERE {$wpdb->posts}.post_type = '%s'";
496 $sql[] = "AND {$wpdb->posts}.post_status = 'publish'";
497 $sql[] = "AND {$wpdb->posts}.post_parent != 0";
498 $sql[] = "ORDER BY {$wpdb->posts}.menu_order ASC, {$wpdb->posts}.ID DESC";
499
500 $sql = implode(' ', $sql);
501
502 $query = $wpdb->prepare( $sql, FLRT_FILTERS_POST_TYPE );
503 $results = $wpdb->get_results( $query, OBJECT );
504
505 flrt_set_transient( $transient_key, $results, FLRT_TRANSIENT_PERIOD_HOURS * HOUR_IN_SECONDS );
506 }
507
508 if( ! $results ){
509 return [];
510 }
511
512 return $results;
513 }
514
515 public function getAllConfiguredFilters()
516 {
517 $key = 'wpc_filters';
518
519 if( ! $filters = $this->getData( $key ) ){
520 $filters = [];
521
522 foreach( $this->makeFiltersQuery() as $k => $filter_post ){
523 $filter = $this->prepareFilter( $filter_post );
524 $k = $filter['e_name'].'_'.$filter['parent'];
525 $filters[$k] = $filter;
526 }
527
528 $this->storeData( $key, $filters );
529 }
530
531 return $filters;
532 }
533
534 public function selectOnlySetFilters( $set_id, $keys = [] )
535 {
536 if( ! $set_id ){
537 return false;
538 }
539 $setFilters = [];
540
541 foreach( $this->allConfiguredFilters as $filter ){
542 if( $filter['parent'] == $set_id ){
543 if( ! empty( $keys ) ){
544 $setFilters[ $filter['ID'] ] = flrt_extract_vars( $filter, $keys );
545 }else{
546 $setFilters[ $filter['ID'] ] = $filter;
547 }
548 }
549 }
550
551 return $setFilters;
552 }
553 /**
554 * @return array
555 */
556 public function getParamFromFilters( $filters, $key )
557 {
558 $result = [];
559 if( ! is_array( $filters ) ){
560 return $result;
561 }
562
563 foreach ( $filters as $_key => $_filter ){
564 if( isset( $_filter[ $key ] ) ){
565 $result[] = $_filter[ $key ];
566 }
567 }
568
569 return $result;
570 }
571
572 public function getConfiguredPathSlugs( $filters = NULL )
573 {
574 $slugs = [];
575
576 if( ! $filters ){
577 $filters = $this->allConfiguredFilters;
578 }
579
580 foreach( $filters as $filter ){
581 if( $filter['in_path'] === 'yes' ){
582 $slugs[] = $filter['slug'];
583 }
584 }
585
586 return $slugs;
587 }
588
589 public function getConfiguredQuerySlugs( $filters = NULL )
590 {
591 $slugs = [];
592
593 if( ! $filters ){
594 $filters = $this->allConfiguredFilters;
595 }
596
597 foreach( $filters as $filter ){
598
599 if( FLRT_PERMALINKS_ENABLED ) {
600 if ($filter['in_path'] === 'no') {
601 $slugs[] = $filter['slug'];
602 }
603 }else{
604 $slugs[] = $filter['slug'];
605 }
606 }
607
608 return array_unique( $slugs );
609 }
610
611 public function prepareFilter( $filter_post )
612 {
613 if( ! isset( $filter_post->ID ) ){
614 return false;
615 }
616
617 $raw_data = (array) maybe_unserialize( $filter_post->post_content );
618 $fse = Container::instance()->getFilterService();
619 $empty_filter = $fse->getEmptyFilter();
620
621 $defaults = array(
622 'ID' => $filter_post->ID,
623 'parent' => $filter_post->post_parent,
624 'menu_order' => $filter_post->menu_order,
625 'label' => $filter_post->post_title,
626 'slug' => $filter_post->post_name
627 );
628
629 $filter = wp_parse_args( $raw_data, wp_parse_args( $defaults, $empty_filter ) );
630 $filter = apply_filters( 'wpc_after_get_filter', $filter );
631
632 unset($fse);
633
634 return $filter;
635 }
636
637 /**
638 * @param $sets
639 * @return array
640 */
641 public function getOnlyBelongsFilters( $sets )
642 {
643 $relevantFilters = [];
644
645 if ( is_array( $sets ) ) {
646 foreach ( $sets as $set ) {
647 $relevantFilters[] = $this->selectOnlySetFilters( $set['ID'] );
648 }
649 }
650
651 $relevantFilters = flrt_remove_level_array($relevantFilters);
652
653 return $this->extractBelongsFilters($relevantFilters);
654 }
655
656 public function checkForbiddenFilters( $queriedFilters, $allowedFilters )
657 {
658 $requestedSlugs = $this->getParamFromFilters( $queriedFilters, 'slug' );
659 $allowedSlugs = $this->getParamFromFilters( $allowedFilters, 'slug' );
660
661 foreach( $requestedSlugs as $slug ){
662 if( ! in_array( $slug, $allowedSlugs ) ){
663 return false;
664 }
665 }
666
667 return true;
668 }
669
670 public function getEntityAllTermsSlugs( $slug )
671 {
672 $slugs = [];
673 $terms = $this->getEntityTermsBySlug( $slug );
674
675 if( is_wp_error( $terms ) ){
676 return $slugs;
677 }
678
679 foreach( $terms as $k => $termObject ){
680 $slugs[ $termObject->term_id ] = $termObject->slug;
681 }
682
683 return $slugs;
684 }
685
686 /**
687 * @param array $sets
688 * @return array|false|mixed
689 */
690 public function getSetsRelatedFilters( $sets = [] )
691 {
692 /**
693 * @todo we have to change this. Not page related, but Set related filters.
694 */
695 $subkey = '';
696 $wpManager = Container::instance()->getWpManager();
697
698 if ( empty( $sets ) ) {
699 $sets = $wpManager->getQueryVar( 'wpc_page_related_set_ids' );
700 }
701
702 if ( ! empty( $sets ) ) {
703 foreach ( $sets as $set ) {
704 $subkey .= '_' . $set['ID'];
705 }
706 }
707
708 $key = 'wpc_related_filters'.$subkey;
709
710 if ( ! $actual = Container::instance()->getParam( $key ) ) {
711
712 $queried = $wpManager->getQueryVar( 'queried_values', [] );
713 $configured = $this->getOnlyBelongsFilters( $sets );
714 $actual = $configured;
715
716 /**
717 * @feature Create new method that populates filters with requested values
718 * This should be there in EntityMananger because for all entities it should be done in the same way
719 */
720 foreach ( $configured as $k => $filter ) {
721 // Merge with queried values
722 $values = isset( $queried[$filter['slug']]['values'] ) ? $queried[$filter['slug']]['values'] : [];
723 $actual[$k]['values'] = $values;
724 }
725
726 Container::instance()->storeParam( $key, $actual );
727 }
728
729 return $actual;
730 }
731
732 /**
733 * Should always return postIDs
734 */
735 public function getAlreadyFilteredPostIds( $setId, $exceptEntity = false )
736 {
737 /**
738 * @bug searching of all queried post IDs doesn't work properly for two or more PostMetaNum filters in one set.
739 */
740
741 $wpManager = Container::instance()->getWpManager();
742 $allWpQueriedPostIds = $this->getAllSetWpQueriedPostIds( $setId );
743
744 $postIds = $allWpQueriedPostIds ? $allWpQueriedPostIds : [];
745
746 if( $wpManager->getQueryVar('wpc_is_filter_request') ){
747 $filteredPostsIdsKeys = $this->collectFilteredPostsIds( $setId );
748
749 $allWpQueriedPostIdsKeys = array_flip( $allWpQueriedPostIds );
750 $allWpQueriedPostIdsKeys = apply_filters( 'wpc_from_products_to_variations', $allWpQueriedPostIdsKeys );
751
752 if( $exceptEntity ){
753 unset( $filteredPostsIdsKeys[$exceptEntity->getName()] );
754 }
755
756 if( ! empty( $filteredPostsIdsKeys ) ) {
757 $intersection_keys = $this->getBetweenFiltersIntersect($filteredPostsIdsKeys, $allWpQueriedPostIdsKeys);
758
759 // Replace back from Variation IDs to Product IDs
760 $postIds = apply_filters( 'wpc_from_variations_to_products', array_flip($intersection_keys) );
761 }
762 }
763
764 return $postIds;
765 }
766
767 public function getAllSetWpQueriedPostIds( $setId )
768 {
769 $ids = [];
770 if( ! $setId ){
771 return $ids;
772 }
773
774 $wpManager = Container::instance()->getWpManager();
775 $key = 'wpc_all_set_queried_post_ids_' . $setId;
776 if ( isset( $_GET['srch'] ) && $_GET['srch'] ) {
777 $keyword = filter_input( INPUT_GET, 'srch', FILTER_SANITIZE_SPECIAL_CHARS );
778 $key .= '_' . $keyword;
779 }
780 $ids = $wpManager->getQueryVar( $key );
781
782 $query_on_the_page = true;
783
784 if ( $ids === false ) {
785
786 // Let's check if the set is related with a query on the page
787 $sets = $wpManager->getQueryVar( 'wpc_page_related_set_ids' );
788 foreach ( $sets as $set ){
789 if( isset( $set['query_on_the_page'] ) && $set['query_on_the_page'] === false ){
790 $query_on_the_page = false;
791 }
792 }
793
794 $set_filter_query = $wpManager->getQueryVar( 'wpc_set_filter_query_' . $setId );
795
796 if( ! $set_filter_query ){
797 /**
798 * string
799 */
800 $theGet = Container::instance()->getTheGet();
801 $savedQueryVars = get_post_meta( $setId, 'wpc_filter_set_query_vars', true );
802
803 if( $savedQueryVars ){
804 $query_vars = maybe_unserialize( $savedQueryVars );
805
806 if( is_array( $query_vars ) ){
807 $set_filter_query = new \WP_Query();
808
809 if( $query_on_the_page ){
810 if( isset( $theGet['s'] ) ){
811 $query_vars['s'] = $theGet['s'];
812 }
813 }
814
815 $set_filter_query->query_vars = $query_vars;
816 }
817 }
818 }
819
820
821 $detected_source = '';
822 if (!empty($set_filter_query->query_vars['flrt_detected_source'])) {
823 $detected_source = $set_filter_query->query_vars['flrt_detected_source'];
824 }
825
826 if ($set_filter_query instanceof \WP_Query) {
827
828 // Configure WP_Query object to select only posts IDs
829 $set_filter_query->set( 'fields', 'ids' );
830 $set_filter_query->set( 'posts_per_page', -1 );
831 $set_filter_query->set( 'nopaging', true );
832 $set_filter_query->set( 'post_status', 'publish' );
833 $set_filter_query->set( 'flrt_query_clone', true );
834
835 do_action( 'wpc_all_set_wp_queried_posts' , $set_filter_query, $setId );
836
837 $ids = $set_filter_query->get_posts();
838
839 }
840
841 if(!empty($detected_source) && isset($set_filter_query->query_vars['flrt_detected_source'])){
842 $set_filter_query->query_vars['flrt_detected_source'] = $detected_source;
843 }
844
845 $ids = apply_filters( 'wpc_check_errors_ids', $ids, $set_filter_query);
846
847 $ids = (! empty( $ids ) ) ? $ids : [];
848
849 $wpManager->setQueryVar($key, $ids);
850
851 unset($set_filter_query);
852 }
853
854 return $ids;
855 }
856
857 // This method must be executed before output
858 public function prepareEntitiesToDisplay( $sets )
859 {
860 $container = Container::instance();
861 $wpManager = $container->getWpManager();
862 $subkey = '';
863
864 $post_type = $sets[0]['filtered_post_type'];
865 $setId = $sets[0]['ID'];
866 $current_set = $sets[0];
867
868 $all_sets = $wpManager->getQueryVar( 'wpc_page_related_set_ids' );
869 $queryRelatedSets = flrt_get_sets_with_the_same_query( $all_sets, $current_set );
870
871 $filter_by_stock_exists = false;
872
873 $relatedSets = [];
874 foreach ( $queryRelatedSets as $set_id ) {
875 $relatedSets[] = array( 'ID' => $set_id );
876 }
877
878 $subkey = implode( '_', $queryRelatedSets );
879 $key = 'wpc_entities_prepared_' . $subkey;
880
881 if( ! $container->getParam( $key ) ) {
882
883 $relatedFilters = $this->getSetsRelatedFilters( $relatedSets );
884
885 if ( $post_type === 'product' && ! empty( $relatedFilters ) ) {
886 foreach ($relatedFilters as $filter) {
887 if (isset($filter['e_name']) && $filter['e_name'] === '_stock_status') {
888 $filter_by_stock_exists = true;
889 break;
890 }
891 }
892 }
893
894 $relatedFilters = apply_filters( 'wpc_related_filters_before_terms_count', $relatedFilters, $sets );
895
896 $allPostsIds = $this->getAllSetWpQueriedPostIds( $setId );
897
898 if ( ! empty( $allPostsIds ) ) {
899 $allPostsIds = array_flip( $allPostsIds );
900 }
901
902 $allEntities = [];
903
904 if ( ! empty( $relatedFilters ) ) {
905
906 foreach ( $relatedFilters as $filter ) {
907 // Part 1 collect all entities
908 $entity = $this->getEntityByFilter( $filter, $post_type );
909
910 if ( $entity instanceof PostMetaExistsEntity ) {
911 $entity->setPostTypes( array( $post_type ) );
912 }
913
914 $entity->populateTermsWithPostIds( $filter['parent'], $post_type );
915
916 $allEntities[$entity->getName()] = $entity;
917 }
918 }
919
920 // Post IDs with variations instead of parent products
921 // This must be called here, after the $entity->populateTermsWithPostIds(); method called
922 $filteredAllPostsIds = $this->collectFilteredPostsIds( $setId );
923
924 /**
925 * Allows to modify filtered post ids
926 */
927 $filteredAllPostsIds = apply_filters( 'wpc_filtered_all_posts_before_terms_count', $filteredAllPostsIds, $allEntities );
928
929 /**
930 * Allows to modify all post ids
931 */
932 $allPostsIds = apply_filters( 'wpc_from_products_to_variations', $allPostsIds );
933
934 foreach ( $allEntities as $entityName => $entity ) {
935 if ( $entityName === '_stock_status' && ! $filter_by_stock_exists && $post_type === 'product' ) {
936 $filter = flrt_get_stock_status_filter_emulation();
937 } else {
938 $filter = $this->getFilterByEname( $entityName, $relatedSets );
939 }
940 foreach ( $entity->items as $index => $term ) {
941 $entity->items[$index]->count = count( $entity->items[$index]->posts );
942 }
943
944 if ( $entity instanceof PostMetaNumEntity || $entity instanceof TaxonomyNumEntity || $entity instanceof PostDateEntity || $entity instanceof PostMetaDateEntity ) {
945 $postsIn = apply_filters( 'wpc_min_and_max_values_numeric_filters', $this->getAlreadyFilteredPostIds( $setId, $entity ), $entity );
946 $entity->updateMinAndMaxValues( $postsIn );
947 }
948
949 if ( in_array( $filter['orderby'], ['menuasc', 'menudesc'] ) ) {
950 if( $entity instanceof TaxonomyEntity ){
951 foreach ($entity->items as $k => $term) {
952 $termOrder = get_term_meta( $term->term_id, 'order', true );
953 $term->menu_order = $termOrder ? $termOrder : 0;
954 $entity->items[$k] = $term;
955 }
956 }
957 }
958
959 $is_rating = (isset($filter['view']) && $filter['view'] === 'rating' && $filter['e_name'] === 'product_visibility' ) ? true : false;
960 if($is_rating){
961 $filter['orderby'] = 'default';
962 }
963
964 if( $filter['orderby'] === 'default' ) {
965 $entity->items = apply_filters( 'wpc_default_sorting_terms', $entity->items, $filter );
966 } else {
967 $entity->items = $this->sortTerms( $entity->items, $filter['orderby'] );
968 }
969
970 /**
971 * @feature move selected terms to top
972 */
973 $used_for_variations = isset( $filter['used_for_variations'] ) ? $filter['used_for_variations'] : false;
974 $entity->items = apply_filters( 'wpc_items_before_calc_term_count', $entity->items, $entity, $used_for_variations );
975
976 foreach ( $entity->items as $index => $term ) {
977 $entity->items[$index]->cross_count = $this->calcTermCount( array_flip($term->posts), $filteredAllPostsIds, $allPostsIds, $filter );
978 }
979 }
980
981 $container->storeParam( $key, true );
982 }
983 }
984
985 /**
986 * @param array $terms list of terms
987 * @param string $sortby
988 * @return array sorted list of terms
989 */
990 private function sortTerms( $terms, $sortby )
991 {
992 if( ! $sortby ){
993 $sortby = 'nameasc';
994 }
995
996 switch( $sortby ){
997 case 'nameasc':
998 usort( $terms, self::compareAsc('name') );
999 break;
1000 case 'postcountasc':
1001 usort( $terms, self::compareAsc('count') );
1002 break;
1003 case 'idasc':
1004 usort( $terms, self::compareAsc('term_id') );
1005 break;
1006 case 'menuasc':
1007 usort( $terms, self::compareAsc('menu_order') );
1008 break;
1009 case 'namedesc':
1010 usort( $terms, self::compareDesc('name') );
1011 break;
1012 case 'postcountdesc':
1013 usort( $terms, self::compareDesc('count') );
1014 break;
1015 case 'iddesc':
1016 usort( $terms, self::compareDesc('term_id') );
1017 break;
1018 case 'menudesc':
1019 usort( $terms, self::compareDesc('menu_order') );
1020 break;
1021 }
1022
1023 return $terms;
1024 }
1025
1026 public static function compareAsc( $key ){
1027
1028 return function ($a, $b) use ($key) {
1029 $value_1 = isset( $a->$key ) ? strtolower($a->$key) : 0;
1030 $value_2 = isset( $b->$key ) ? strtolower($b->$key) : 0;
1031
1032 if ($value_1 == $value_2) {
1033 return 0;
1034 }
1035
1036 return ($value_1 > $value_2) ? +1 : -1;
1037 };
1038 }
1039
1040 public static function compareDesc( $key ){
1041 return function ($a, $b) use ($key) {
1042 $value_1 = isset( $a->$key ) ? strtolower($a->$key) : 0;
1043 $value_2 = isset( $b->$key ) ? strtolower($b->$key) : 0;
1044
1045 if ($value_1 == $value_2) {
1046 return 0;
1047 }
1048
1049 return ($value_1 < $value_2) ? +1 : -1;
1050 };
1051 }
1052
1053 public function calcTermCount( $termPostsIds, $filteredPostsIds, $allPostsIds, $filter )
1054 {
1055 if( empty( $termPostsIds ) ){
1056 return 0;
1057 }
1058
1059 $e_name = $filter['e_name'];
1060 $logic = $filter['logic'];
1061
1062 if( ! isset( $filteredPostsIds[$e_name] ) ){
1063 $filteredPostsIds[$e_name] = [];
1064 }
1065
1066 // Intersection for logic OR between filter terms
1067 if ( $logic === 'or' ) {
1068
1069 $filteredPostsIds[$e_name] += $termPostsIds;
1070
1071 // Intersection for logic AND between filter terms
1072 } elseif ( $logic === 'and' ) {
1073 if( ! empty( $filteredPostsIds[$e_name] ) ){
1074 $filteredPostsIds[$e_name] = array_intersect_key( $allPostsIds, $filteredPostsIds[$e_name], $termPostsIds );
1075 }else{
1076 $filteredPostsIds[$e_name] = array_intersect_key( $allPostsIds, $termPostsIds );
1077 }
1078 }
1079
1080 $betweenFiltersIntersect = $this->getBetweenFiltersIntersect( $filteredPostsIds, $allPostsIds );
1081 $finalInterSection = apply_filters( 'wpc_from_variations_to_products', array_flip( array_intersect_key( $betweenFiltersIntersect, $termPostsIds ) ) );
1082
1083 return count( $finalInterSection );
1084 }
1085
1086 public function getBetweenFiltersIntersect( $filteredPostsIds, $allPostsIds )
1087 {
1088 $betweenFiltersAND = [];
1089 /**
1090 * @bug when intersection is already empty, this count another intersection as more than 0
1091 * example - http://filter.stepasyuk.com/shop/category-hoodies/color-red/alphabet-alpha/
1092 * when there are hidden product, counter also doesn't work properly
1093 * example - http://filter.stepasyuk.com/shop/category-music/
1094 */
1095
1096 // This implements logic AND between separate filters
1097 if ( ! empty( $filteredPostsIds ) ) {
1098 $i = 1;
1099 foreach ( $filteredPostsIds as $e_name => $singleFilterPostsIds ) {
1100 if( $i !== 1 ) {
1101 $betweenFiltersAND = array_intersect_key( $allPostsIds, $betweenFiltersAND, $singleFilterPostsIds );
1102 } else {
1103 $betweenFiltersAND = array_intersect_key( $allPostsIds, $singleFilterPostsIds );
1104 }
1105 $i++;
1106 }
1107 }
1108
1109 // Final intersect between All posts and Filtered
1110 return array_intersect_key( $allPostsIds, $betweenFiltersAND );
1111 }
1112
1113 public function getTaxonomyTermsForDropdown( $taxonomyName, $optionGroup = false )
1114 {
1115 $terms = [];
1116 if( ! $taxonomyName ){
1117 return $terms;
1118 }
1119
1120 $filter['e_name'] = $taxonomyName;
1121 $filter['entity'] = 'taxonomy';
1122 $entity = $this->getEntityByFilter( $filter );
1123
1124 if( ! $entity ){
1125 return $terms;
1126 }
1127
1128 return $entity->getTermsForSelect( $optionGroup );
1129 }
1130
1131 public function getAuthorTermsForDropdown( $optionGroup = false )
1132 {
1133 $args = array(
1134 'has_published_posts' => true,
1135 'orderby' => 'display_name'
1136 );
1137
1138 $key = 'wpc_users';
1139 if( $optionGroup ){
1140 $key .= '_group';
1141 }
1142
1143 if( ! $authors = $this->getData( $key ) ) {
1144
1145 $authors = [];
1146
1147 $users = get_users($args);
1148
1149 foreach ($users as $user) {
1150 if ($optionGroup) {
1151 $authors['author:' . $user->ID] = $user->data->display_name;
1152 } else {
1153 $authors[$user->ID] = $user->data->display_name;
1154 }
1155 }
1156
1157 $this->storeData( $key, $authors );
1158 }
1159
1160 return $authors;
1161 }
1162
1163 public function safeExplodeFilterValues($params, $slug, $sep, $explode = true)
1164 {
1165 if (!is_string($params)) {
1166 $params = (string) $params;
1167 }
1168
1169 if ($sep === '') {
1170 return $explode ? [$params] : $params;
1171 }
1172
1173 // Get all term slugs for the given filter slug
1174 $allEntityTerms = $this->getEntityAllTermsSlugs($slug);
1175
1176 // Extract pure values (slugs)
1177 $terms = array_values($allEntityTerms);
1178
1179 // Skip empty terms
1180 $terms = array_filter($terms, static function ($t) {
1181 return $t !== '';
1182 });
1183
1184 // Keep only terms that contain the separator
1185 $terms = array_filter($terms, static function ($t) use ($sep) {
1186 return mb_strpos($t, $sep) !== false;
1187 });
1188
1189 // Sort by length in descending order to protect longer terms first
1190 usort($terms, static function ($a, $b) {
1191 return mb_strlen($b) <=> mb_strlen($a);
1192 });
1193
1194 // Replace the separator inside matched terms with a placeholder
1195 // so explode() won't split inside a single term
1196 foreach ($terms as $entityTerm) {
1197 $escapedTerm = preg_quote($entityTerm, '/');
1198 $pattern = "/$escapedTerm/u";
1199
1200 $params = preg_replace_callback($pattern, function ($matches) use ($sep) {
1201 // Replace internal separators only inside the matched term
1202 return str_replace($sep, '#', $matches[0]);
1203 }, $params);
1204 }
1205
1206 // Finally, split by the separator if requested
1207 if ($explode) {
1208 return explode($sep, $params);
1209 }
1210
1211 return $params;
1212 }
1213
1214
1215
1216 public function safeImplodeFilterValues( $filterValues, $sep )
1217 {
1218 if( ! is_array( $filterValues ) ){
1219 return [];
1220 }
1221 // Replace back
1222 array_walk($filterValues, function (&$value) use ($sep) {
1223 $value = str_replace('#', $sep, $value);
1224 });
1225
1226 return $filterValues;
1227 }
1228
1229 /**
1230 * @return array List of all queried posts or empty array
1231 */
1232 public function collectFilteredPostsIds( $setId )
1233 {
1234 $wpManager = Container::instance()->getWpManager();
1235 $fss = Container::instance()->getFilterSetService();
1236
1237 $theSet = $fss->getSet( $setId );
1238 $postType = isset( $theSet['post_type']['value'] ) ? $theSet['post_type']['value'] : '';
1239 $filtered_wp_query = isset( $theSet['wp_filter_query'] ) ? $theSet['wp_filter_query']: '-1';
1240
1241 $current_set = array(
1242 'ID' => $setId,
1243 'filtered_post_type' => $postType,
1244 'query' => $filtered_wp_query
1245 );
1246
1247 $allSetPostsIds = $this->getAllSetWpQueriedPostIds( $setId );
1248 $queriedFilters = $wpManager->getQueryVar('queried_values');
1249
1250 if( $allSetPostsIds ){
1251 $allSetPostsIds = array_flip( $allSetPostsIds );
1252 }
1253
1254 $allSetPostsIds = apply_filters( 'wpc_from_products_to_variations', $allSetPostsIds );
1255
1256 if ( ! $wpManager->getQueryVar('wpc_is_filter_request') ) {
1257 return [];
1258 }
1259
1260 $queriedAllPosts = [];
1261
1262 $all_sets = $wpManager->getQueryVar( 'wpc_page_related_set_ids' );
1263 $queryRelatedSets = flrt_get_sets_with_the_same_query( $all_sets, $current_set );
1264
1265 $set_filter_keys = $this->getSetFilterKeys( $queryRelatedSets );
1266
1267 foreach ( $queriedFilters as $slug => $queriedFilter ) {
1268
1269 $queried_value_key = $queriedFilter['entity'].'#'.$queriedFilter['e_name'];
1270 $do_filter_request = apply_filters( 'wpc_do_filter_request', true, $queriedFilter, null );
1271
1272 if( ! in_array( $queried_value_key, $set_filter_keys ) || ! $do_filter_request ) {
1273 continue;
1274 }
1275
1276 $entity = $this->getEntityByFilter( $queriedFilter, $postType );
1277 $e_name = $queriedFilter['e_name'];
1278 $queriedAllPosts[$e_name] = [];
1279
1280 // Allows to replace product IDs with their variation IDs
1281 $entity->items = apply_filters( 'wpc_items_before_calc_term_count', $entity->items, $entity, $queriedFilter['used_for_variations'] );
1282 foreach ( $entity->items as $term ) {
1283
1284 if ( ! isset( $term->posts ) ) {
1285 continue;
1286 }
1287
1288 if ( in_array( $queriedFilter['entity'], [ 'post_meta_num', 'tax_numeric', 'post_date', 'post_meta_date' ] ) ) {
1289 $doCalculate = in_array( $term->slug, array_keys( $queriedFilter['values'] ) );
1290 } else {
1291 $doCalculate = in_array( $term->slug, $queriedFilter['values'] );
1292 }
1293
1294 if ( $doCalculate ) {
1295
1296 // Intersection for logic OR between filter terms
1297 if ( $queriedFilter['logic'] === 'or' ) {
1298
1299 $queriedAllPosts[$e_name] += array_flip( $term->posts );
1300
1301 // Intersection for logic AND between filter terms
1302 } elseif ( $queriedFilter['logic'] === 'and' ) {
1303
1304 if( ! empty( $queriedAllPosts[$e_name] ) ){
1305 $queriedAllPosts[$e_name] = array_intersect_key( $allSetPostsIds, $queriedAllPosts[$e_name], array_flip($term->posts) );
1306 } else {
1307 $queriedAllPosts[$e_name] = array_intersect_key( $allSetPostsIds, array_flip($term->posts) );
1308 }
1309 }
1310 }
1311 }
1312 }
1313
1314 return apply_filters( 'wpc_queried_all_posts', $queriedAllPosts, $allSetPostsIds, $queriedFilters, $setId );
1315 }
1316
1317 public function getSetFilterKeys( $setIds )
1318 {
1319 $set_filter_keys = [];
1320
1321 if( ! $setIds || empty( $setIds )){
1322 return $set_filter_keys;
1323 }
1324
1325 $sets = [];
1326 foreach( $setIds as $setId ){
1327 $sets[] = array( 'ID' => $setId );
1328 }
1329
1330 $set_filters = $this->getSetsRelatedFilters( $sets );
1331
1332 foreach ( $set_filters as $filter ){
1333 $set_filter_keys[] = $filter['entity'].'#'.$filter['e_name'];
1334 }
1335
1336 return $set_filter_keys;
1337 }
1338
1339 public function getEntityTermsBySlug( $slug )
1340 {
1341 // Slug is not unique param, but it's ok for this case.
1342 $filterEntity = $this->getFilterBySlug( $slug /*, array( 'entity', 'e_name' )*/ );
1343 $entity = $this->getEntityByFilter( $filterEntity );
1344
1345 return $entity->getAllExistingTerms();
1346 }
1347
1348 public function addTermsToWpQuery( $queried_value, $wp_query )
1349 {
1350 $entity = $this->getEntityByFilter( $queried_value );
1351 return $entity->addTermsToWpQuery( $queried_value, $wp_query );
1352 }
1353 }