PluginProbe
ElasticPress / 5.0.0
ElasticPress v5.0.0
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
elasticpress / includes / classes / Indexable / Term / Term.php

Term.php in ElasticPress 5.0.0, at includes/classes/Indexable/Term/Term.php

953 lines 22.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Term indexable
4 *
5 * @since 3.1
6 * @package elasticpress
7 */
8
9 namespace ElasticPress\Indexable\Term;
10
11 use \WP_Term_Query;
12 use ElasticPress\Elasticsearch;
13 use ElasticPress\Indexable;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 // @codeCoverageIgnoreStart
17 exit; // Exit if accessed directly.
18 // @codeCoverageIgnoreEnd
19 }
20
21 /**
22 * Term indexable class
23 */
24 class Term extends Indexable {
25
26 /**
27 * Indexable slug
28 *
29 * @var string
30 * @since 3.1
31 */
32 public $slug = 'term';
33
34 /**
35 * Create indexable and initialize dependencies
36 *
37 * @since 3.1
38 */
39 public function __construct() {
40 $this->labels = [
41 'plural' => esc_html__( 'Terms', 'elasticpress' ),
42 'singular' => esc_html__( 'Term', 'elasticpress' ),
43 ];
44 }
45
46 /**
47 * Instantiate the indexable SyncManager and QueryIntegration, the main responsibles for the WP integration.
48 *
49 * @since 4.5.0
50 * @return void
51 */
52 public function setup() {
53 $this->sync_manager = new SyncManager( $this->slug );
54 $this->query_integration = new QueryIntegration( $this->slug );
55 }
56
57 /**
58 * Format query vars into ES query
59 *
60 * @param array $query_vars WP_Term_Query args.
61 * @since 3.1
62 * @return array
63 */
64 public function format_args( $query_vars ) {
65 /**
66 * Support `number` query var
67 */
68 if ( ! empty( $query_vars['number'] ) ) {
69 $number = (int) $query_vars['number'];
70 } else {
71 /**
72 * Set the maximum results window size.
73 *
74 * The request will return a HTTP 500 Internal Error if the size of the
75 * request is larger than the [index.max_result_window] parameter in ES.
76 * See the scroll api for a more efficient way to request large data sets.
77 *
78 * @return int The max results window size.
79 *
80 * @since 2.3.0
81 */
82 $number = apply_filters( 'ep_max_results_window', 10000 );
83 }
84
85 $formatted_args = [
86 'from' => 0,
87 'size' => $number,
88 ];
89
90 /**
91 * Support `offset` query var
92 */
93 if ( isset( $query_vars['offset'] ) ) {
94 $formatted_args['from'] = (int) $query_vars['offset'];
95 }
96
97 /**
98 * Support `order` and `orderby` query vars
99 */
100
101 // Set sort order, default is 'ASC'.
102 if ( ! empty( $query_vars['order'] ) ) {
103 $order = $this->parse_order( $query_vars['order'] );
104 } else {
105 $order = 'desc';
106 }
107
108 // Set orderby, default is 'name'.
109 if ( empty( $query_vars['orderby'] ) ) {
110 $query_vars['orderby'] = 'name';
111 }
112
113 // Set sort type.
114 $formatted_args['sort'] = $this->parse_orderby( $query_vars['orderby'], $order, $query_vars );
115
116 $filter = [
117 'bool' => [
118 'must' => [],
119 ],
120 ];
121
122 $use_filters = false;
123
124 /**
125 * Support `taxonomy` query var
126 */
127 $taxonomy = [];
128 if ( ! empty( $query_vars['taxonomy'] ) ) {
129 $taxonomy = (array) $query_vars['taxonomy'];
130 $terms_map_name = 'terms';
131
132 if ( count( $taxonomy ) < 2 ) {
133 $terms_map_name = 'term';
134 $taxonomy = $taxonomy[0];
135 }
136
137 $filter['bool']['must'][] = [
138 $terms_map_name => [
139 'taxonomy.raw' => $taxonomy,
140 ],
141 ];
142
143 $use_filters = true;
144 }
145
146 /**
147 * Support `object_ids` query var
148 */
149 if ( ! empty( $query_vars['object_ids'] ) ) {
150 $filter['bool']['must'][]['bool']['must'][] = [
151 'terms' => [
152 'object_ids.value' => (array) $query_vars['object_ids'],
153 ],
154 ];
155
156 $use_filters = true;
157 }
158
159 /**
160 * Support `get` query var
161 */
162 if ( ! empty( $query_vars['get'] ) && 'all' === $query_vars['get'] ) {
163 $query_vars['childless'] = false;
164 $query_vars['child_of'] = 0;
165 $query_vars['hide_empty'] = false;
166 $query_vars['hierarchical'] = false;
167 $query_vars['pad_counts'] = false;
168 }
169
170 /**
171 * Support `include` query var
172 */
173 if ( ! empty( $query_vars['include'] ) ) {
174 $filter['bool']['must'][]['bool']['must'] = [
175 'terms' => [
176 'term_id' => array_values( (array) $query_vars['include'] ),
177 ],
178 ];
179
180 $use_filters = true;
181 }
182
183 /**
184 * Support `exclude` query var
185 */
186 if ( empty( $query_vars['include'] ) && ! empty( $query_vars['exclude'] ) ) {
187 $filter['bool']['must'][]['bool']['must_not'] = [
188 'terms' => [
189 'term_id' => array_values( (array) $query_vars['exclude'] ),
190 ],
191 ];
192
193 $use_filters = true;
194 }
195
196 /**
197 * Support `exclude_tree` query var
198 */
199 if ( empty( $query_vars['include'] ) && ! empty( $query_vars['exclude_tree'] ) ) {
200 $filter['bool']['must'][]['bool']['must_not'] = [
201 'terms' => [
202 'term_id' => array_values( (array) $query_vars['exclude_tree'] ),
203 ],
204 ];
205
206 $filter['bool']['must'][]['bool']['must_not'] = [
207 'terms' => [
208 'parent' => array_values( (array) $query_vars['exclude_tree'] ),
209 ],
210 ];
211
212 $use_filters = true;
213 }
214
215 /**
216 * Support `name` query var
217 */
218 if ( ! empty( $query_vars['name'] ) ) {
219 $filter['bool']['must'][] = [
220 'terms' => [
221 'name.raw' => (array) $query_vars['name'],
222 ],
223 ];
224
225 $use_filters = true;
226 }
227
228 /**
229 * Support `slug` query var
230 */
231 if ( ! empty( $query_vars['slug'] ) ) {
232 if ( ! is_array( $query_vars['slug'] ) ) {
233 $query_vars['slug'] = array( $query_vars['slug'] );
234 }
235
236 $query_vars['slug'] = array_map( 'sanitize_title', $query_vars['slug'] );
237
238 $filter['bool']['must'][] = [
239 'terms' => [
240 'slug.raw' => (array) $query_vars['slug'],
241 ],
242 ];
243
244 $use_filters = true;
245 }
246
247 /**
248 * Support `term_taxonomy_id` query var
249 */
250 if ( ! empty( $query_vars['term_taxonomy_id'] ) ) {
251 $filter['bool']['must'][]['bool']['must'] = [
252 'terms' => [
253 'term_taxonomy_id' => array_values( (array) $query_vars['term_taxonomy_id'] ),
254 ],
255 ];
256
257 $use_filters = true;
258 }
259
260 /**
261 * Support `hierarchical` and `hide_empty` query var
262 *
263 * `hierarchical` needs to work in conjunction with `hide_empty`, as per WP docs:
264 * > `hierarchical`: Whether to include terms that have non-empty descendants (even if $hide_empty is set to true).
265 *
266 * In summary:
267 * - hide_empty AND hierarchical: count > 1 OR hierarchy.children > 1
268 * - hide_empty AND NOT hierarchical: count > 1 (ignore hierarchy.children)
269 * - NOT hide_empty (AND hierarchical): there is no need to limit the query
270 *
271 * @see https://developer.wordpress.org/reference/classes/WP_Term_Query/__construct/
272 */
273 $hide_empty = isset( $query_vars['hide_empty'] ) ? $query_vars['hide_empty'] : '';
274 if ( $hide_empty ) {
275 $hierarchical = isset( $query_vars['hierarchical'] ) ? $query_vars['hierarchical'] : '';
276
277 if ( $hierarchical ) {
278 $filter_clause = [ 'bool' => [ 'should' => [] ] ];
279
280 $filter_clause['bool']['should'][] = [
281 'range' => [
282 'count' => [
283 'gte' => 1,
284 ],
285 ],
286 ];
287
288 $filter_clause['bool']['should'][] = [
289 'range' => [
290 'hierarchy.children.count' => [
291 'gte' => 1,
292 ],
293 ],
294 ];
295
296 $filter['bool']['must'][] = $filter_clause;
297
298 $use_filters = true;
299 } else {
300 $filter['bool']['must'][] = [
301 'range' => [
302 'count' => [
303 'gte' => 1,
304 ],
305 ],
306 ];
307 }
308
309 $use_filters = true;
310 }
311
312 /**
313 * Support `search`, `name__like` and `description__like` query_vars
314 */
315 if ( ! empty( $query_vars['search'] ) || ! empty( $query_vars['name__like'] ) || ! empty( $query_vars['description__like'] ) ) {
316
317 $search = ! empty( $query_vars['search'] ) ? $query_vars['search'] : '';
318 $search_fields = [];
319
320 if ( ! empty( $query_vars['name__like'] ) ) {
321 $search = $query_vars['name__like'];
322 $search_fields[] = 'name';
323 }
324
325 if ( ! empty( $query_vars['description__like'] ) ) {
326 $search = $query_vars['description__like'];
327 $search_fields[] = 'description';
328 }
329
330 /**
331 * Allow for search field specification
332 */
333 if ( ! empty( $query_vars['search_fields'] ) ) {
334 $search_fields = $query_vars['search_fields'];
335 }
336
337 if ( ! empty( $search_fields ) ) {
338 $prepared_search_fields = [];
339
340 if ( ! empty( $search_fields['meta'] ) ) {
341 $metas = (array) $search_fields['meta'];
342
343 foreach ( $metas as $meta ) {
344 $prepared_search_fields[] = 'meta.' . $meta . '.value';
345 }
346
347 unset( $search_fields['meta'] );
348 }
349
350 $prepared_search_fields = array_merge( $search_fields, $prepared_search_fields );
351 } else {
352 $prepared_search_fields = [
353 'name',
354 'slug',
355 'taxonomy',
356 'description',
357 ];
358 }
359
360 /**
361 * Filter fields to search on Term query
362 *
363 * @hook ep_term_search_fields
364 * @param {array} $search_fields Search fields
365 * @param {array} $query_vars Query variables
366 * @since 3.4
367 * @return {array} New search fields
368 */
369 $prepared_search_fields = apply_filters( 'ep_term_search_fields', $prepared_search_fields, $query_vars );
370
371 $search_algorithm = $this->get_search_algorithm( $search, $prepared_search_fields, $query_vars );
372 $formatted_args['query'] = $search_algorithm->get_query( 'term', $search, $prepared_search_fields, $query_vars );
373 } else {
374 $formatted_args['query']['match_all'] = [
375 'boost' => 1,
376 ];
377 }
378
379 /**
380 * Support `child_of` query var.
381 */
382 if ( ! empty( $query_vars['child_of'] ) && ( is_string( $taxonomy ) || count( $taxonomy ) < 2 ) ) {
383 $filter['bool']['must'][]['bool']['must'][] = [
384 'match_phrase' => [
385 'hierarchy.ancestors.terms' => (int) $query_vars['child_of'],
386 ],
387 ];
388
389 $use_filters = true;
390 }
391
392 /**
393 * Support `parent` query var.
394 */
395 if ( isset( $query_vars['parent'] ) && '' !== $query_vars['parent'] ) {
396 $filter['bool']['must'][]['bool']['must'] = [
397 'term' => [
398 'parent' => (int) $query_vars['parent'],
399 ],
400 ];
401
402 $use_filters = true;
403 }
404
405 /**
406 * Support `childless` query var.
407 */
408 if ( ! empty( $query_vars['childless'] ) ) {
409 $filter['bool']['must'][]['bool']['must'] = [
410 'term' => [
411 'hierarchy.children.terms' => 0,
412 ],
413 ];
414
415 $use_filters = true;
416 }
417
418 $meta_queries = [];
419
420 /**
421 * Support `meta_key`, `meta_value`, and `meta_compare` query args
422 */
423 if ( ! empty( $query_vars['meta_key'] ) ) {
424 $meta_query_array = [
425 'key' => $query_vars['meta_key'],
426 ];
427
428 if ( isset( $query_vars['meta_value'] ) && '' !== $query_vars['meta_value'] ) {
429 $meta_query_array['value'] = $query_vars['meta_value'];
430 }
431
432 if ( isset( $query_vars['meta_compare'] ) ) {
433 $meta_query_array['compare'] = $query_vars['meta_compare'];
434 }
435
436 $meta_queries[] = $meta_query_array;
437 }
438
439 /**
440 * Support 'meta_query' query var.
441 */
442 if ( ! empty( $query_vars['meta_query'] ) ) {
443 $meta_queries = array_merge( $meta_queries, $query_vars['meta_query'] );
444 }
445
446 if ( ! empty( $meta_queries ) ) {
447 $built_meta_queries = $this->build_meta_query( $meta_queries );
448
449 if ( $built_meta_queries ) {
450 $filter['bool']['must'][] = $built_meta_queries;
451 $use_filters = true;
452 }
453 }
454
455 /**
456 * Support `fields` query var.
457 */
458 if ( isset( $query_vars['fields'] ) ) {
459 switch ( $query_vars['fields'] ) {
460 case 'ids':
461 $formatted_args['_source'] = [
462 'includes' => [
463 'term_id',
464 ],
465 ];
466 break;
467
468 case 'id=>name':
469 $formatted_args['_source'] = [
470 'includes' => [
471 'term_id',
472 'name',
473 ],
474 ];
475 break;
476
477 case 'id=>parent':
478 $formatted_args['_source'] = [
479 'includes' => [
480 'term_id',
481 'parent',
482 ],
483 ];
484 break;
485
486 case 'id=>slug':
487 $formatted_args['_source'] = [
488 'includes' => [
489 'term_id',
490 'slug',
491 ],
492 ];
493 break;
494
495 case 'names':
496 $formatted_args['_source'] = [
497 'includes' => [
498 'name',
499 ],
500 ];
501 break;
502 case 'tt_ids':
503 $formatted_args['_source'] = [
504 'includes' => [
505 'term_taxonomy_id',
506 ],
507 ];
508 break;
509 }
510 }
511
512 if ( $use_filters ) {
513 $formatted_args['post_filter'] = $filter;
514 }
515
516 /**
517 * Filter full Elasticsearch query for Terms indexable
518 *
519 * @hook ep_term_formatted_args
520 * @param {array} $query Elasticsearch query
521 * @param {array} $query_vars Query variables
522 * @since 3.4
523 * @return {array} New query
524 */
525 return apply_filters( 'ep_term_formatted_args', $formatted_args, $query_vars );
526 }
527
528 /**
529 * Generate the mapping array
530 *
531 * @since 3.6.0
532 * @return array
533 */
534 public function generate_mapping() {
535 $es_version = Elasticsearch::factory()->get_elasticsearch_version();
536
537 if ( empty( $es_version ) ) {
538 $es_version = apply_filters( 'ep_fallback_elasticsearch_version', '2.0' );
539 }
540 $es_version = (string) $es_version;
541
542 $mapping_file = '7-0.php';
543
544 if ( version_compare( $es_version, '7.0', '<' ) ) {
545 $mapping_file = 'initial.php';
546 }
547
548 /**
549 * Filter mapping file for Terms indexable
550 *
551 * @hook ep_term_mapping_file
552 * @param {string} $file File name
553 * @since 3.4
554 * @return {string} New file name
555 */
556 $mapping = require apply_filters( 'ep_term_mapping_file', __DIR__ . '/../../../mappings/term/' . $mapping_file );
557
558 /**
559 * Filter full Elasticsearch query for Terms indexable
560 *
561 * @hook ep_term_mapping
562 * @param {array} $mapping Elasticsearch mapping
563 * @since 3.4
564 * @return {array} New mapping
565 */
566 $mapping = apply_filters( 'ep_term_mapping', $mapping );
567
568 return $mapping;
569 }
570
571 /**
572 * Prepare a term document for indexing
573 *
574 * @param int $term_id Term ID
575 * @since 3.1
576 * @return bool|array
577 */
578 public function prepare_document( $term_id ) {
579 $term = get_term( $term_id );
580
581 if ( ! $term || ! is_a( $term, 'WP_Term' ) ) {
582 return false;
583 }
584
585 $term_args = [
586 'term_id' => $term->term_id,
587 'ID' => $term->term_id,
588 'name' => $term->name,
589 'slug' => $term->slug,
590 'term_group' => $term->group,
591 'term_taxonomy_id' => $term->term_taxonomy_id,
592 'taxonomy' => $term->taxonomy,
593 'description' => $term->description,
594 'parent' => $term->parent,
595 'count' => $term->count,
596 'meta' => $this->prepare_meta_types( $this->prepare_meta( $term->term_id ) ),
597 'hierarchy' => $this->prepare_term_hierarchy( $term->term_id, $term->taxonomy ),
598 'object_ids' => $this->prepare_object_ids( $term->term_id, $term->taxonomy ),
599 ];
600
601 /**
602 * Filter term fields pre-sync
603 *
604 * @hook ep_term_sync_args
605 * @param {array} $term_args Current term fields
606 * @param {int} $term_id Term ID
607 * @since 3.4
608 * @return {array} New fields
609 */
610 $term_args = apply_filters( 'ep_term_sync_args', $term_args, $term_id );
611
612 return $term_args;
613 }
614
615 /**
616 * Query DB for terms
617 *
618 * @param array $args Query arguments
619 * @since 3.1
620 * @return array
621 */
622 public function query_db( $args ) {
623
624 $defaults = [
625 'number' => $this->get_bulk_items_per_page(),
626 'offset' => 0,
627 'orderby' => 'id',
628 'order' => 'desc',
629 'taxonomy' => $this->get_indexable_taxonomies(),
630 'hide_empty' => false,
631 ];
632
633 if ( isset( $args['per_page'] ) ) {
634 $args['number'] = $args['per_page'];
635 }
636
637 /**
638 * Filter database arguments for term query
639 *
640 * @hook ep_term_query_db_args
641 * @param {array} $args Query arguments based to WP_Term_Query
642 * @since 3.4
643 * @return {array} New arguments
644 */
645 $args = apply_filters( 'ep_term_query_db_args', wp_parse_args( $args, $defaults ) );
646
647 $all_query_args = $args;
648
649 unset( $all_query_args['number'] );
650 unset( $all_query_args['offset'] );
651 unset( $all_query_args['fields'] );
652
653 /**
654 * This just seems so inefficient.
655 *
656 * @todo Better way to do this?
657 */
658
659 /**
660 * Filter database arguments for term count query
661 *
662 * @hook ep_term_all_query_db_args
663 * @param {array} $args Query arguments based to WP_Term_Query
664 * @since 3.4
665 * @return {array} New arguments
666 */
667 $all_query = new WP_Term_Query( apply_filters( 'ep_term_all_query_db_args', $all_query_args, $args ) );
668
669 $total_objects = count( $all_query->terms );
670
671 if ( ! empty( $args['offset'] ) ) {
672 if ( (int) $args['offset'] >= $total_objects ) {
673 $total_objects = 0;
674 }
675 }
676
677 $query = new WP_Term_Query( $args );
678
679 if ( is_array( $query->terms ) ) {
680 array_walk( $query->terms, array( $this, 'remap_terms' ) );
681 }
682
683 return [
684 'objects' => $query->terms,
685 'total_objects' => $total_objects,
686 ];
687 }
688
689 /**
690 * Returns indexable taxonomies for the current site
691 *
692 * @since 3.1
693 * @return mixed|void
694 */
695 public function get_indexable_taxonomies() {
696 $taxonomies = get_taxonomies( [], 'objects' );
697 $public_taxonomies = [];
698
699 foreach ( $taxonomies as $taxonomy ) {
700 if ( $taxonomy->public || $taxonomy->publicly_queryable ) {
701 $public_taxonomies[] = $taxonomy->name;
702 }
703 }
704
705 /**
706 * Filter indexable taxonomies for Terms indexable
707 *
708 * @hook ep_indexable_taxonomies
709 * @param {array} $public_taxonomies Taxonomies
710 * @since 3.4
711 * @return {array} New taxonomies array
712 */
713 return apply_filters( 'ep_indexable_taxonomies', $public_taxonomies );
714 }
715
716 /**
717 * Rebuild our term object to match the fields we need.
718 *
719 * In particular, result of WP_Term_Query does not
720 * include an "id" field, which our index command
721 * expects.
722 *
723 * @param object $value Term object
724 * @since 3.1
725 * @return void Returns by reference
726 */
727 public function remap_terms( &$value ) {
728 $value = (object) array(
729 'ID' => $value->term_id,
730 'term_id' => $value->term_id,
731 'name' => $value->name,
732 'slug' => $value->slug,
733 'term_group' => $value->term_group,
734 'term_taxonomy_id' => $value->term_taxonomy_id,
735 'taxonomy' => $value->taxonomy,
736 'description' => $value->description,
737 'parent' => $value->parent,
738 'count' => $value->count,
739 );
740 }
741
742 /**
743 * Prepare meta to send to ES
744 *
745 * @param int $term_id Term ID
746 * @since 3.1
747 * @return array
748 */
749 public function prepare_meta( $term_id ) {
750 $meta = (array) get_term_meta( $term_id );
751
752 if ( empty( $meta ) ) {
753 return [];
754 }
755
756 $prepared_meta = [];
757
758 /**
759 * Filter index-able private meta
760 *
761 * Allows for specifying private meta keys that may be indexed in the same manner as public meta keys.
762 *
763 * @since 3.4
764 * @hook ep_prepare_term_meta_allowed_protected_keys
765 * @param {array} $allowed_protected_keys Array of index-able private meta keys.
766 * @param {int} $term_id Term ID.
767 * @return {array} New meta keys
768 */
769 $allowed_protected_keys = apply_filters( 'ep_prepare_term_meta_allowed_protected_keys', [], $term_id );
770
771 /**
772 * Filter non-indexed public meta
773 *
774 * Allows for specifying public meta keys that should be excluded from the ElasticPress index.
775 *
776 * @since 3.4
777 * @hook ep_prepare_term_meta_excluded_public_keys
778 * @param {array} $public_keys Array of public meta keys to exclude from index.
779 * @param {int} $term_id Term ID.
780 * @return {array} New keys
781 */
782 $excluded_public_keys = apply_filters(
783 'ep_prepare_term_meta_excluded_public_keys',
784 [
785 'session_tokens',
786 ],
787 $term_id
788 );
789
790 foreach ( $meta as $key => $value ) {
791
792 $allow_index = false;
793
794 if ( is_protected_meta( $key ) ) {
795
796 if ( true === $allowed_protected_keys || in_array( $key, $allowed_protected_keys, true ) ) {
797 $allow_index = true;
798 }
799 } else {
800
801 if ( true !== $excluded_public_keys && ! in_array( $key, $excluded_public_keys, true ) ) {
802 $allow_index = true;
803 }
804 }
805
806 /**
807 * Filter kill switch for any term meta
808 *
809 * @since 3.4
810 * @hook ep_prepare_term_meta_whitelist_key
811 * @param {boolean} $index_key Whether to index key or not
812 * @param {string} $key Key name
813 * @param {int} $term_id Term ID.
814 * @return {boolean} New index value
815 */
816 if ( true === $allow_index || apply_filters( 'ep_prepare_term_meta_whitelist_key', false, $key, $term_id ) ) {
817 $prepared_meta[ $key ] = maybe_unserialize( $value );
818 }
819 }
820
821 return $prepared_meta;
822 }
823
824 /**
825 * Prepare term hierarchy to send to ES
826 *
827 * @param int $term_id Term ID.
828 * @param string $taxonomy Term taxonomy.
829 * @since 3.1
830 * @return array
831 */
832 public function prepare_term_hierarchy( $term_id, $taxonomy ) {
833 $hierarchy = [];
834 $children = get_term_children( $term_id, $taxonomy );
835 $ancestors = get_ancestors( $term_id, $taxonomy, 'taxonomy' );
836
837 if ( ! empty( $children ) && ! is_wp_error( $children ) ) {
838 $hierarchy['children']['terms'] = $children;
839 $children_count = 0;
840
841 foreach ( $children as $child_term_id ) {
842 $child_term = get_term( $child_term_id );
843 $children_count += (int) $child_term->count;
844 }
845
846 $hierarchy['children']['count'] = $children_count;
847 } else {
848 $hierarchy['children']['terms'] = 0;
849 $hierarchy['children']['count'] = 0;
850 }
851
852 if ( ! empty( $ancestors ) ) {
853 $hierarchy['ancestors']['terms'] = $ancestors;
854 } else {
855 $hierarchy['ancestors']['terms'] = 0;
856 }
857
858 return $hierarchy;
859 }
860
861 /**
862 * Prepare object IDs to send to ES
863 *
864 * @param int $term_id Term ID.
865 * @param string $taxonomy Term taxonomy.
866 * @since 3.1
867 * @return array
868 */
869 public function prepare_object_ids( $term_id, $taxonomy ) {
870 $ids = [];
871 $object_ids = get_objects_in_term( [ $term_id ], [ $taxonomy ] );
872
873 if ( ! empty( $object_ids ) && ! is_wp_error( $object_ids ) ) {
874 $ids['value'] = array_map( 'absint', array_values( $object_ids ) );
875 } else {
876 $ids['value'] = 0;
877 }
878
879 return $ids;
880 }
881
882 /**
883 * Parse an 'order' query variable and cast it to ASC or DESC as necessary.
884 *
885 * @access protected
886 *
887 * @param string $order The 'order' query variable.
888 * @since 3.1
889 * @return string The sanitized 'order' query variable.
890 */
891 protected function parse_order( $order ) {
892 if ( ! is_string( $order ) || empty( $order ) ) {
893 return 'desc';
894 }
895
896 if ( 'ASC' === strtoupper( $order ) ) {
897 return 'asc';
898 } else {
899 return 'desc';
900 }
901 }
902
903 /**
904 * Convert the alias to a properly-prefixed sort value.
905 *
906 * @access protected
907 *
908 * @param string $orderby Alias or path for the field to order by.
909 * @param string $order Order direction
910 * @param array $args Query args
911 * @since 3.1
912 * @return array
913 */
914 protected function parse_orderby( $orderby, $order, $args ) {
915 $sort = [];
916
917 if ( empty( $orderby ) ) {
918 return $sort;
919 }
920
921 $from_to = [
922 'slug' => 'slug.raw',
923 'id' => 'term_id',
924 'description' => 'description.sortable',
925 ];
926
927 if ( in_array( $orderby, [ 'meta_value', 'meta_value_num' ], true ) ) {
928 if ( empty( $args['meta_key'] ) ) {
929 return $sort;
930 } else {
931 $from_to['meta_value'] = 'meta.' . $args['meta_key'] . '.value';
932 $from_to['meta_value_num'] = 'meta.' . $args['meta_key'] . '.long';
933 }
934 }
935
936 if ( 'name' === $orderby ) {
937 $es_version = Elasticsearch::factory()->get_elasticsearch_version();
938 $from_to['name'] = version_compare( (string) $es_version, '7.0', '<' ) ? 'name.raw' : 'name.sortable';
939 }
940
941 $orderby = $from_to[ $orderby ] ?? $orderby;
942
943 $sort[] = array(
944 $orderby => array(
945 'order' => $order,
946 ),
947 );
948
949 return $sort;
950 }
951
952 }
953