WordPress 4.7
[autoinstalls/wordpress.git] / wp-includes / class-wp-term-query.php
1 <?php
2
3 /**
4  * Taxonomy API: WP_Term_Query class.
5  *
6  * @package WordPress
7  * @subpackage Taxonomy
8  * @since 4.6.0
9  */
10
11 /**
12  * Class used for querying terms.
13  *
14  * @since 4.6.0
15  *
16  * @see WP_Term_Query::__construct() for accepted arguments.
17  */
18 class WP_Term_Query {
19
20         /**
21          * SQL string used to perform database query.
22          *
23          * @since 4.6.0
24          * @access public
25          * @var string
26          */
27         public $request;
28
29         /**
30          * Metadata query container.
31          *
32          * @since 4.6.0
33          * @access public
34          * @var object WP_Meta_Query
35          */
36         public $meta_query = false;
37
38         /**
39          * Metadata query clauses.
40          *
41          * @since 4.6.0
42          * @access protected
43          * @var array
44          */
45         protected $meta_query_clauses;
46
47         /**
48          * SQL query clauses.
49          *
50          * @since 4.6.0
51          * @access protected
52          * @var array
53          */
54         protected $sql_clauses = array(
55                 'select'  => '',
56                 'from'    => '',
57                 'where'   => array(),
58                 'orderby' => '',
59                 'limits'  => '',
60         );
61
62         /**
63          * Query vars set by the user.
64          *
65          * @since 4.6.0
66          * @access public
67          * @var array
68          */
69         public $query_vars;
70
71         /**
72          * Default values for query vars.
73          *
74          * @since 4.6.0
75          * @access public
76          * @var array
77          */
78         public $query_var_defaults;
79
80         /**
81          * List of terms located by the query.
82          *
83          * @since 4.6.0
84          * @access public
85          * @var array
86          */
87         public $terms;
88
89         /**
90          * Constructor.
91          *
92          * Sets up the term query, based on the query vars passed.
93          *
94          * @since 4.6.0
95          * @since 4.6.0 Introduced 'term_taxonomy_id' parameter.
96          * @since 4.7.0 Introduced 'object_ids' parameter.
97          * @access public
98          *
99          * @param string|array $query {
100          *     Optional. Array or query string of term query parameters. Default empty.
101          *
102          *     @type string|array $taxonomy               Taxonomy name, or array of taxonomies, to which results should
103          *                                                be limited.
104          *     @type int|array    $object_ids             Optional. Object ID, or array of object IDs. Results will be
105          *                                                limited to terms associated with these objects.
106          *     @type string       $orderby                Field(s) to order terms by. Accepts term fields ('name',
107          *                                                'slug', 'term_group', 'term_id', 'id', 'description'),
108          *                                                'count' for term taxonomy count, 'include' to match the
109          *                                                'order' of the $include param, 'meta_value', 'meta_value_num',
110          *                                                the value of `$meta_key`, the array keys of `$meta_query`, or
111          *                                                'none' to omit the ORDER BY clause. Defaults to 'name'.
112          *     @type string       $order                  Whether to order terms in ascending or descending order.
113          *                                                Accepts 'ASC' (ascending) or 'DESC' (descending).
114          *                                                Default 'ASC'.
115          *     @type bool|int     $hide_empty             Whether to hide terms not assigned to any posts. Accepts
116          *                                                1|true or 0|false. Default 1|true.
117          *     @type array|string $include                Array or comma/space-separated string of term ids to include.
118          *                                                Default empty array.
119          *     @type array|string $exclude                Array or comma/space-separated string of term ids to exclude.
120          *                                                If $include is non-empty, $exclude is ignored.
121          *                                                Default empty array.
122          *     @type array|string $exclude_tree           Array or comma/space-separated string of term ids to exclude
123          *                                                along with all of their descendant terms. If $include is
124          *                                                non-empty, $exclude_tree is ignored. Default empty array.
125          *     @type int|string   $number                 Maximum number of terms to return. Accepts ''|0 (all) or any
126          *                                                positive number. Default ''|0 (all).
127          *     @type int          $offset                 The number by which to offset the terms query. Default empty.
128          *     @type string       $fields                 Term fields to query for. Accepts 'all' (returns an array of
129          *                                                complete term objects), 'all_with_object_id' (returns an
130          *                                                array of term objects with the 'object_id' param; only works
131          *                                                when the `$fields` parameter is 'object_ids' ), 'ids'
132          *                                                (returns an array of ids), 'tt_ids' (returns an array of
133          *                                                term taxonomy ids), 'id=>parent' (returns an associative
134          *                                                array with ids as keys, parent term IDs as values), 'names'
135          *                                                (returns an array of term names), 'count' (returns the number
136          *                                                of matching terms), 'id=>name' (returns an associative array
137          *                                                with ids as keys, term names as values), or 'id=>slug'
138          *                                                (returns an associative array with ids as keys, term slugs
139          *                                                as values). Default 'all'.
140          *     @type bool         $count                  Whether to return a term count (true) or array of term objects
141          *                                                (false). Will take precedence over `$fields` if true.
142          *                                                Default false.
143          *     @type string|array $name                   Optional. Name or array of names to return term(s) for.
144          *                                                Default empty.
145          *     @type string|array $slug                   Optional. Slug or array of slugs to return term(s) for.
146          *                                                Default empty.
147          *     @type int|array    $term_taxonomy_id       Optional. Term taxonomy ID, or array of term taxonomy IDs,
148          *                                                to match when querying terms.
149          *     @type bool         $hierarchical           Whether to include terms that have non-empty descendants (even
150          *                                                if $hide_empty is set to true). Default true.
151          *     @type string       $search                 Search criteria to match terms. Will be SQL-formatted with
152          *                                                wildcards before and after. Default empty.
153          *     @type string       $name__like             Retrieve terms with criteria by which a term is LIKE
154          *                                                `$name__like`. Default empty.
155          *     @type string       $description__like      Retrieve terms where the description is LIKE
156          *                                                `$description__like`. Default empty.
157          *     @type bool         $pad_counts             Whether to pad the quantity of a term's children in the
158          *                                                quantity of each term's "count" object variable.
159          *                                                Default false.
160          *     @type string       $get                    Whether to return terms regardless of ancestry or whether the
161          *                                                terms are empty. Accepts 'all' or empty (disabled).
162          *                                                Default empty.
163          *     @type int          $child_of               Term ID to retrieve child terms of. If multiple taxonomies
164          *                                                are passed, $child_of is ignored. Default 0.
165          *     @type int|string   $parent                 Parent term ID to retrieve direct-child terms of.
166          *                                                Default empty.
167          *     @type bool         $childless              True to limit results to terms that have no children.
168          *                                                This parameter has no effect on non-hierarchical taxonomies.
169          *                                                Default false.
170          *     @type string       $cache_domain           Unique cache key to be produced when this query is stored in
171          *                                                an object cache. Default is 'core'.
172          *     @type bool         $update_term_meta_cache Whether to prime meta caches for matched terms. Default true.
173          *     @type array        $meta_query             Optional. Meta query clauses to limit retrieved terms by.
174          *                                                See `WP_Meta_Query`. Default empty.
175          *     @type string       $meta_key               Limit terms to those matching a specific metadata key.
176          *                                                Can be used in conjunction with `$meta_value`.
177          *     @type string       $meta_value             Limit terms to those matching a specific metadata value.
178          *                                                Usually used in conjunction with `$meta_key`.
179          * }
180          */
181         public function __construct( $query = '' ) {
182                 $this->query_var_defaults = array(
183                         'taxonomy'               => null,
184                         'object_ids'             => null,
185                         'orderby'                => 'name',
186                         'order'                  => 'ASC',
187                         'hide_empty'             => true,
188                         'include'                => array(),
189                         'exclude'                => array(),
190                         'exclude_tree'           => array(),
191                         'number'                 => '',
192                         'offset'                 => '',
193                         'fields'                 => 'all',
194                         'count'                  => false,
195                         'name'                   => '',
196                         'slug'                   => '',
197                         'term_taxonomy_id'       => '',
198                         'hierarchical'           => true,
199                         'search'                 => '',
200                         'name__like'             => '',
201                         'description__like'      => '',
202                         'pad_counts'             => false,
203                         'get'                    => '',
204                         'child_of'               => 0,
205                         'parent'                 => '',
206                         'childless'              => false,
207                         'cache_domain'           => 'core',
208                         'update_term_meta_cache' => true,
209                         'meta_query'             => '',
210                         'meta_key'               => '',
211                         'meta_value'             => '',
212                         'meta_type'              => '',
213                         'meta_compare'           => '',
214                 );
215
216                 if ( ! empty( $query ) ) {
217                         $this->query( $query );
218                 }
219         }
220
221         /**
222          * Parse arguments passed to the term query with default query parameters.
223          *
224          * @since 4.6.0
225          * @access public
226          *
227          * @param string|array $query WP_Term_Query arguments. See WP_Term_Query::__construct()
228          */
229         public function parse_query( $query = '' ) {
230                 if ( empty( $query ) ) {
231                         $query = $this->query_vars;
232                 }
233
234                 $taxonomies = isset( $query['taxonomy'] ) ? (array) $query['taxonomy'] : null;
235
236                 /**
237                  * Filters the terms query default arguments.
238                  *
239                  * Use {@see 'get_terms_args'} to filter the passed arguments.
240                  *
241                  * @since 4.4.0
242                  *
243                  * @param array $defaults   An array of default get_terms() arguments.
244                  * @param array $taxonomies An array of taxonomies.
245                  */
246                 $this->query_var_defaults = apply_filters( 'get_terms_defaults', $this->query_var_defaults, $taxonomies );
247
248                 $query = wp_parse_args( $query, $this->query_var_defaults );
249
250                 $query['number'] = absint( $query['number'] );
251                 $query['offset'] = absint( $query['offset'] );
252
253                 // 'parent' overrides 'child_of'.
254                 if ( 0 < intval( $query['parent'] ) ) {
255                         $query['child_of'] = false;
256                 }
257
258                 if ( 'all' == $query['get'] ) {
259                         $query['childless'] = false;
260                         $query['child_of'] = 0;
261                         $query['hide_empty'] = 0;
262                         $query['hierarchical'] = false;
263                         $query['pad_counts'] = false;
264                 }
265
266                 $query['taxonomy'] = $taxonomies;
267
268                 $this->query_vars = $query;
269
270                 /**
271                  * Fires after term query vars have been parsed.
272                  *
273                  * @since 4.6.0
274                  *
275                  * @param WP_Term_Query $this Current instance of WP_Term_Query.
276                  */
277                 do_action( 'parse_term_query', $this );
278         }
279
280         /**
281          * Sets up the query for retrieving terms.
282          *
283          * @since 4.6.0
284          * @access public
285          *
286          * @param string|array $query Array or URL query string of parameters.
287          * @return array|int List of terms, or number of terms when 'count' is passed as a query var.
288          */
289         public function query( $query ) {
290                 $this->query_vars = wp_parse_args( $query );
291                 return $this->get_terms();
292         }
293
294         /**
295          * Get terms, based on query_vars.
296          *
297          * @param 4.6.0
298          * @access public
299          *
300          * @global wpdb $wpdb WordPress database abstraction object.
301          *
302          * @return array
303          */
304         public function get_terms() {
305                 global $wpdb;
306
307                 $this->parse_query( $this->query_vars );
308                 $args = $this->query_vars;
309
310                 // Set up meta_query so it's available to 'pre_get_terms'.
311                 $this->meta_query = new WP_Meta_Query();
312                 $this->meta_query->parse_query_vars( $args );
313
314                 /**
315                  * Fires before terms are retrieved.
316                  *
317                  * @since 4.6.0
318                  *
319                  * @param WP_Term_Query $this Current instance of WP_Term_Query.
320                  */
321                 do_action( 'pre_get_terms', $this );
322
323                 $taxonomies = $args['taxonomy'];
324
325                 // Save queries by not crawling the tree in the case of multiple taxes or a flat tax.
326                 $has_hierarchical_tax = false;
327                 if ( $taxonomies ) {
328                         foreach ( $taxonomies as $_tax ) {
329                                 if ( is_taxonomy_hierarchical( $_tax ) ) {
330                                         $has_hierarchical_tax = true;
331                                 }
332                         }
333                 }
334
335                 if ( ! $has_hierarchical_tax ) {
336                         $args['hierarchical'] = false;
337                         $args['pad_counts'] = false;
338                 }
339
340                 // 'parent' overrides 'child_of'.
341                 if ( 0 < intval( $args['parent'] ) ) {
342                         $args['child_of'] = false;
343                 }
344
345                 if ( 'all' == $args['get'] ) {
346                         $args['childless'] = false;
347                         $args['child_of'] = 0;
348                         $args['hide_empty'] = 0;
349                         $args['hierarchical'] = false;
350                         $args['pad_counts'] = false;
351                 }
352
353                 /**
354                  * Filters the terms query arguments.
355                  *
356                  * @since 3.1.0
357                  *
358                  * @param array $args       An array of get_terms() arguments.
359                  * @param array $taxonomies An array of taxonomies.
360                  */
361                 $args = apply_filters( 'get_terms_args', $args, $taxonomies );
362
363                 // Avoid the query if the queried parent/child_of term has no descendants.
364                 $child_of = $args['child_of'];
365                 $parent   = $args['parent'];
366
367                 if ( $child_of ) {
368                         $_parent = $child_of;
369                 } elseif ( $parent ) {
370                         $_parent = $parent;
371                 } else {
372                         $_parent = false;
373                 }
374
375                 if ( $_parent ) {
376                         $in_hierarchy = false;
377                         foreach ( $taxonomies as $_tax ) {
378                                 $hierarchy = _get_term_hierarchy( $_tax );
379
380                                 if ( isset( $hierarchy[ $_parent ] ) ) {
381                                         $in_hierarchy = true;
382                                 }
383                         }
384
385                         if ( ! $in_hierarchy ) {
386                                 return array();
387                         }
388                 }
389
390                 // 'term_order' is a legal sort order only when joining the relationship table.
391                 $_orderby = $this->query_vars['orderby'];
392                 if ( 'term_order' === $_orderby && empty( $this->query_vars['object_ids'] ) ) {
393                         $_orderby = 'term_id';
394                 }
395                 $orderby = $this->parse_orderby( $_orderby );
396
397                 if ( $orderby ) {
398                         $orderby = "ORDER BY $orderby";
399                 }
400
401                 $order = $this->parse_order( $this->query_vars['order'] );
402
403                 if ( $taxonomies ) {
404                         $this->sql_clauses['where']['taxonomy'] = "tt.taxonomy IN ('" . implode( "', '", array_map( 'esc_sql', $taxonomies ) ) . "')";
405                 }
406
407                 $exclude      = $args['exclude'];
408                 $exclude_tree = $args['exclude_tree'];
409                 $include      = $args['include'];
410
411                 $inclusions = '';
412                 if ( ! empty( $include ) ) {
413                         $exclude = '';
414                         $exclude_tree = '';
415                         $inclusions = implode( ',', wp_parse_id_list( $include ) );
416                 }
417
418                 if ( ! empty( $inclusions ) ) {
419                         $this->sql_clauses['where']['inclusions'] = 't.term_id IN ( ' . $inclusions . ' )';
420                 }
421
422                 $exclusions = array();
423                 if ( ! empty( $exclude_tree ) ) {
424                         $exclude_tree = wp_parse_id_list( $exclude_tree );
425                         $excluded_children = $exclude_tree;
426                         foreach ( $exclude_tree as $extrunk ) {
427                                 $excluded_children = array_merge(
428                                         $excluded_children,
429                                         (array) get_terms( $taxonomies[0], array(
430                                                 'child_of' => intval( $extrunk ),
431                                                 'fields' => 'ids',
432                                                 'hide_empty' => 0
433                                         ) )
434                                 );
435                         }
436                         $exclusions = array_merge( $excluded_children, $exclusions );
437                 }
438
439                 if ( ! empty( $exclude ) ) {
440                         $exclusions = array_merge( wp_parse_id_list( $exclude ), $exclusions );
441                 }
442
443                 // 'childless' terms are those without an entry in the flattened term hierarchy.
444                 $childless = (bool) $args['childless'];
445                 if ( $childless ) {
446                         foreach ( $taxonomies as $_tax ) {
447                                 $term_hierarchy = _get_term_hierarchy( $_tax );
448                                 $exclusions = array_merge( array_keys( $term_hierarchy ), $exclusions );
449                         }
450                 }
451
452                 if ( ! empty( $exclusions ) ) {
453                         $exclusions = 't.term_id NOT IN (' . implode( ',', array_map( 'intval', $exclusions ) ) . ')';
454                 } else {
455                         $exclusions = '';
456                 }
457
458                 /**
459                  * Filters the terms to exclude from the terms query.
460                  *
461                  * @since 2.3.0
462                  *
463                  * @param string $exclusions `NOT IN` clause of the terms query.
464                  * @param array  $args       An array of terms query arguments.
465                  * @param array  $taxonomies An array of taxonomies.
466                  */
467                 $exclusions = apply_filters( 'list_terms_exclusions', $exclusions, $args, $taxonomies );
468
469                 if ( ! empty( $exclusions ) ) {
470                         // Must do string manipulation here for backward compatibility with filter.
471                         $this->sql_clauses['where']['exclusions'] = preg_replace( '/^\s*AND\s*/', '', $exclusions );
472                 }
473
474                 if ( ! empty( $args['name'] ) ) {
475                         $names = (array) $args['name'];
476                         foreach ( $names as &$_name ) {
477                                 // `sanitize_term_field()` returns slashed data.
478                                 $_name = stripslashes( sanitize_term_field( 'name', $_name, 0, reset( $taxonomies ), 'db' ) );
479                         }
480
481                         $this->sql_clauses['where']['name'] = "t.name IN ('" . implode( "', '", array_map( 'esc_sql', $names ) ) . "')";
482                 }
483
484                 if ( ! empty( $args['slug'] ) ) {
485                         if ( is_array( $args['slug'] ) ) {
486                                 $slug = array_map( 'sanitize_title', $args['slug'] );
487                                 $this->sql_clauses['where']['slug'] = "t.slug IN ('" . implode( "', '", $slug ) . "')";
488                         } else {
489                                 $slug = sanitize_title( $args['slug'] );
490                                 $this->sql_clauses['where']['slug'] = "t.slug = '$slug'";
491                         }
492                 }
493
494                 if ( ! empty( $args['term_taxonomy_id'] ) ) {
495                         if ( is_array( $args['term_taxonomy_id'] ) ) {
496                                 $tt_ids = implode( ',', array_map( 'intval', $args['term_taxonomy_id'] ) );
497                                 $this->sql_clauses['where']['term_taxonomy_id'] = "tt.term_taxonomy_id IN ({$tt_ids})";
498                         } else {
499                                 $this->sql_clauses['where']['term_taxonomy_id'] = $wpdb->prepare( "tt.term_taxonomy_id = %d", $args['term_taxonomy_id'] );
500                         }
501                 }
502
503                 if ( ! empty( $args['name__like'] ) ) {
504                         $this->sql_clauses['where']['name__like'] = $wpdb->prepare( "t.name LIKE %s", '%' . $wpdb->esc_like( $args['name__like'] ) . '%' );
505                 }
506
507                 if ( ! empty( $args['description__like'] ) ) {
508                         $this->sql_clauses['where']['description__like'] = $wpdb->prepare( "tt.description LIKE %s", '%' . $wpdb->esc_like( $args['description__like'] ) . '%' );
509                 }
510
511                 if ( ! empty( $args['object_ids'] ) ) {
512                         $object_ids = $args['object_ids'];
513                         if ( ! is_array( $object_ids ) ) {
514                                 $object_ids = array( $object_ids );
515                         }
516
517                         $object_ids = implode( ', ', array_map( 'intval', $object_ids ) );
518                         $this->sql_clauses['where']['object_ids'] = "tr.object_id IN ($object_ids)";
519                 }
520
521                 /*
522                  * When querying for object relationships, the 'count > 0' check
523                  * added by 'hide_empty' is superfluous.
524                  */
525                 if ( ! empty( $args['object_ids'] ) ) {
526                         $args['hide_empty'] = false;
527                 }
528
529                 if ( '' !== $parent ) {
530                         $parent = (int) $parent;
531                         $this->sql_clauses['where']['parent'] = "tt.parent = '$parent'";
532                 }
533
534                 $hierarchical = $args['hierarchical'];
535                 if ( 'count' == $args['fields'] ) {
536                         $hierarchical = false;
537                 }
538                 if ( $args['hide_empty'] && !$hierarchical ) {
539                         $this->sql_clauses['where']['count'] = 'tt.count > 0';
540                 }
541
542                 $number = $args['number'];
543                 $offset = $args['offset'];
544
545                 // Don't limit the query results when we have to descend the family tree.
546                 if ( $number && ! $hierarchical && ! $child_of && '' === $parent ) {
547                         if ( $offset ) {
548                                 $limits = 'LIMIT ' . $offset . ',' . $number;
549                         } else {
550                                 $limits = 'LIMIT ' . $number;
551                         }
552                 } else {
553                         $limits = '';
554                 }
555
556
557                 if ( ! empty( $args['search'] ) ) {
558                         $this->sql_clauses['where']['search'] = $this->get_search_sql( $args['search'] );
559                 }
560
561                 // Meta query support.
562                 $join = '';
563                 $distinct = '';
564
565                 // Reparse meta_query query_vars, in case they were modified in a 'pre_get_terms' callback.
566                 $this->meta_query->parse_query_vars( $this->query_vars );
567                 $mq_sql = $this->meta_query->get_sql( 'term', 't', 'term_id' );
568                 $meta_clauses = $this->meta_query->get_clauses();
569
570                 if ( ! empty( $meta_clauses ) ) {
571                         $join .= $mq_sql['join'];
572                         $this->sql_clauses['where']['meta_query'] = preg_replace( '/^\s*AND\s*/', '', $mq_sql['where'] );
573                         $distinct .= "DISTINCT";
574
575                 }
576
577                 $selects = array();
578                 switch ( $args['fields'] ) {
579                         case 'all':
580                         case 'all_with_object_id' :
581                         case 'tt_ids' :
582                         case 'slugs' :
583                                 $selects = array( 't.*', 'tt.*' );
584                                 if ( 'all_with_object_id' === $args['fields'] && ! empty( $args['object_ids'] ) ) {
585                                         $selects[] = 'tr.object_id';
586                                 }
587                                 break;
588                         case 'ids':
589                         case 'id=>parent':
590                                 $selects = array( 't.term_id', 'tt.parent', 'tt.count', 'tt.taxonomy' );
591                                 break;
592                         case 'names':
593                                 $selects = array( 't.term_id', 'tt.parent', 'tt.count', 't.name', 'tt.taxonomy' );
594                                 break;
595                         case 'count':
596                                 $orderby = '';
597                                 $order = '';
598                                 $selects = array( 'COUNT(*)' );
599                                 break;
600                         case 'id=>name':
601                                 $selects = array( 't.term_id', 't.name', 'tt.count', 'tt.taxonomy' );
602                                 break;
603                         case 'id=>slug':
604                                 $selects = array( 't.term_id', 't.slug', 'tt.count', 'tt.taxonomy' );
605                                 break;
606                 }
607
608                 $_fields = $args['fields'];
609
610                 /**
611                  * Filters the fields to select in the terms query.
612                  *
613                  * Field lists modified using this filter will only modify the term fields returned
614                  * by the function when the `$fields` parameter set to 'count' or 'all'. In all other
615                  * cases, the term fields in the results array will be determined by the `$fields`
616                  * parameter alone.
617                  *
618                  * Use of this filter can result in unpredictable behavior, and is not recommended.
619                  *
620                  * @since 2.8.0
621                  *
622                  * @param array $selects    An array of fields to select for the terms query.
623                  * @param array $args       An array of term query arguments.
624                  * @param array $taxonomies An array of taxonomies.
625                  */
626                 $fields = implode( ', ', apply_filters( 'get_terms_fields', $selects, $args, $taxonomies ) );
627
628                 $join .= " INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id";
629
630                 if ( ! empty( $this->query_vars['object_ids'] ) ) {
631                         $join .= " INNER JOIN {$wpdb->term_relationships} AS tr ON tr.term_taxonomy_id = tt.term_taxonomy_id";
632                 }
633
634                 $where = implode( ' AND ', $this->sql_clauses['where'] );
635
636                 /**
637                  * Filters the terms query SQL clauses.
638                  *
639                  * @since 3.1.0
640                  *
641                  * @param array $pieces     Terms query SQL clauses.
642                  * @param array $taxonomies An array of taxonomies.
643                  * @param array $args       An array of terms query arguments.
644                  */
645                 $clauses = apply_filters( 'terms_clauses', compact( 'fields', 'join', 'where', 'distinct', 'orderby', 'order', 'limits' ), $taxonomies, $args );
646
647                 $fields = isset( $clauses[ 'fields' ] ) ? $clauses[ 'fields' ] : '';
648                 $join = isset( $clauses[ 'join' ] ) ? $clauses[ 'join' ] : '';
649                 $where = isset( $clauses[ 'where' ] ) ? $clauses[ 'where' ] : '';
650                 $distinct = isset( $clauses[ 'distinct' ] ) ? $clauses[ 'distinct' ] : '';
651                 $orderby = isset( $clauses[ 'orderby' ] ) ? $clauses[ 'orderby' ] : '';
652                 $order = isset( $clauses[ 'order' ] ) ? $clauses[ 'order' ] : '';
653                 $limits = isset( $clauses[ 'limits' ] ) ? $clauses[ 'limits' ] : '';
654
655                 if ( $where ) {
656                         $where = "WHERE $where";
657                 }
658
659                 $this->sql_clauses['select']  = "SELECT $distinct $fields";
660                 $this->sql_clauses['from']    = "FROM $wpdb->terms AS t $join";
661                 $this->sql_clauses['orderby'] = $orderby ? "$orderby $order" : '';
662                 $this->sql_clauses['limits']  = $limits;
663
664                 $this->request = "{$this->sql_clauses['select']} {$this->sql_clauses['from']} {$where} {$this->sql_clauses['orderby']} {$this->sql_clauses['limits']}";
665
666                 // $args can be anything. Only use the args defined in defaults to compute the key.
667                 $key = md5( serialize( wp_array_slice_assoc( $args, array_keys( $this->query_var_defaults ) ) ) . serialize( $taxonomies ) . $this->request );
668                 $last_changed = wp_cache_get_last_changed( 'terms' );
669                 $cache_key = "get_terms:$key:$last_changed";
670                 $cache = wp_cache_get( $cache_key, 'terms' );
671                 if ( false !== $cache ) {
672                         if ( 'all' === $_fields ) {
673                                 $cache = array_map( 'get_term', $cache );
674                         }
675
676                         $this->terms = $cache;
677                         return $this->terms;
678                 }
679
680                 if ( 'count' == $_fields ) {
681                         $count = $wpdb->get_var( $this->request );
682                         wp_cache_set( $cache_key, $count, 'terms' );
683                         return $count;
684                 }
685
686                 $terms = $wpdb->get_results( $this->request );
687                 if ( 'all' == $_fields || 'all_with_object_id' === $_fields ) {
688                         update_term_cache( $terms );
689                 }
690
691                 // Prime termmeta cache.
692                 if ( $args['update_term_meta_cache'] ) {
693                         $term_ids = wp_list_pluck( $terms, 'term_id' );
694                         update_termmeta_cache( $term_ids );
695                 }
696
697                 if ( empty( $terms ) ) {
698                         wp_cache_add( $cache_key, array(), 'terms', DAY_IN_SECONDS );
699                         return array();
700                 }
701
702                 if ( $child_of ) {
703                         foreach ( $taxonomies as $_tax ) {
704                                 $children = _get_term_hierarchy( $_tax );
705                                 if ( ! empty( $children ) ) {
706                                         $terms = _get_term_children( $child_of, $terms, $_tax );
707                                 }
708                         }
709                 }
710
711                 // Update term counts to include children.
712                 if ( $args['pad_counts'] && 'all' == $_fields ) {
713                         foreach ( $taxonomies as $_tax ) {
714                                 _pad_term_counts( $terms, $_tax );
715                         }
716                 }
717
718                 // Make sure we show empty categories that have children.
719                 if ( $hierarchical && $args['hide_empty'] && is_array( $terms ) ) {
720                         foreach ( $terms as $k => $term ) {
721                                 if ( ! $term->count ) {
722                                         $children = get_term_children( $term->term_id, $term->taxonomy );
723                                         if ( is_array( $children ) ) {
724                                                 foreach ( $children as $child_id ) {
725                                                         $child = get_term( $child_id, $term->taxonomy );
726                                                         if ( $child->count ) {
727                                                                 continue 2;
728                                                         }
729                                                 }
730                                         }
731
732                                         // It really is empty.
733                                         unset( $terms[ $k ] );
734                                 }
735                         }
736                 }
737
738                 /*
739                  * When querying for terms connected to objects, we may get
740                  * duplicate results. The duplicates should be preserved if
741                  * `$fields` is 'all_with_object_id', but should otherwise be
742                  * removed.
743                  */
744                 if ( ! empty( $args['object_ids'] ) && 'all_with_object_id' != $_fields ) {
745                         $_tt_ids = $_terms = array();
746                         foreach ( $terms as $term ) {
747                                 if ( isset( $_tt_ids[ $term->term_id ] ) ) {
748                                         continue;
749                                 }
750
751                                 $_tt_ids[ $term->term_id ] = 1;
752                                 $_terms[] = $term;
753                         }
754
755                         $terms = $_terms;
756                 }
757
758                 $_terms = array();
759                 if ( 'id=>parent' == $_fields ) {
760                         foreach ( $terms as $term ) {
761                                 $_terms[ $term->term_id ] = $term->parent;
762                         }
763                 } elseif ( 'ids' == $_fields ) {
764                         foreach ( $terms as $term ) {
765                                 $_terms[] = (int) $term->term_id;
766                         }
767                 } elseif ( 'tt_ids' == $_fields ) {
768                         foreach ( $terms as $term ) {
769                                 $_terms[] = (int) $term->term_taxonomy_id;
770                         }
771                 } elseif ( 'names' == $_fields ) {
772                         foreach ( $terms as $term ) {
773                                 $_terms[] = $term->name;
774                         }
775                 } elseif ( 'slugs' == $_fields ) {
776                         foreach ( $terms as $term ) {
777                                 $_terms[] = $term->slug;
778                         }
779                 } elseif ( 'id=>name' == $_fields ) {
780                         foreach ( $terms as $term ) {
781                                 $_terms[ $term->term_id ] = $term->name;
782                         }
783                 } elseif ( 'id=>slug' == $_fields ) {
784                         foreach ( $terms as $term ) {
785                                 $_terms[ $term->term_id ] = $term->slug;
786                         }
787                 }
788
789                 if ( ! empty( $_terms ) ) {
790                         $terms = $_terms;
791                 }
792
793                 // Hierarchical queries are not limited, so 'offset' and 'number' must be handled now.
794                 if ( $hierarchical && $number && is_array( $terms ) ) {
795                         if ( $offset >= count( $terms ) ) {
796                                 $terms = array();
797                         } else {
798                                 $terms = array_slice( $terms, $offset, $number, true );
799                         }
800                 }
801
802                 wp_cache_add( $cache_key, $terms, 'terms', DAY_IN_SECONDS );
803
804                 if ( 'all' === $_fields || 'all_with_object_id' === $_fields ) {
805                         $terms = array_map( 'get_term', $terms );
806                 }
807
808                 $this->terms = $terms;
809                 return $this->terms;
810         }
811
812         /**
813          * Parse and sanitize 'orderby' keys passed to the term query.
814          *
815          * @since 4.6.0
816          * @access protected
817          *
818          * @global wpdb $wpdb WordPress database abstraction object.
819          *
820          * @param string $orderby_raw Alias for the field to order by.
821          * @return string|false Value to used in the ORDER clause. False otherwise.
822          */
823         protected function parse_orderby( $orderby_raw ) {
824                 $_orderby = strtolower( $orderby_raw );
825                 $maybe_orderby_meta = false;
826
827                 if ( in_array( $_orderby, array( 'term_id', 'name', 'slug', 'term_group' ), true ) ) {
828                         $orderby = "t.$_orderby";
829                 } elseif ( in_array( $_orderby, array( 'count', 'parent', 'taxonomy', 'term_taxonomy_id', 'description' ), true ) ) {
830                         $orderby = "tt.$_orderby";
831                 } elseif ( 'term_order' === $_orderby ) {
832                         $orderby = 'tr.term_order';
833                 } elseif ( 'include' == $_orderby && ! empty( $this->query_vars['include'] ) ) {
834                         $include = implode( ',', wp_parse_id_list( $this->query_vars['include'] ) );
835                         $orderby = "FIELD( t.term_id, $include )";
836                 } elseif ( 'none' == $_orderby ) {
837                         $orderby = '';
838                 } elseif ( empty( $_orderby ) || 'id' == $_orderby || 'term_id' === $_orderby ) {
839                         $orderby = 't.term_id';
840                 } else {
841                         $orderby = 't.name';
842
843                         // This may be a value of orderby related to meta.
844                         $maybe_orderby_meta = true;
845                 }
846
847                 /**
848                  * Filters the ORDERBY clause of the terms query.
849                  *
850                  * @since 2.8.0
851                  *
852                  * @param string $orderby    `ORDERBY` clause of the terms query.
853                  * @param array  $args       An array of terms query arguments.
854                  * @param array  $taxonomies An array of taxonomies.
855                  */
856                 $orderby = apply_filters( 'get_terms_orderby', $orderby, $this->query_vars, $this->query_vars['taxonomy'] );
857
858                 // Run after the 'get_terms_orderby' filter for backward compatibility.
859                 if ( $maybe_orderby_meta ) {
860                         $maybe_orderby_meta = $this->parse_orderby_meta( $_orderby );
861                         if ( $maybe_orderby_meta ) {
862                                 $orderby = $maybe_orderby_meta;
863                         }
864                 }
865
866                 return $orderby;
867         }
868
869         /**
870          * Generate the ORDER BY clause for an 'orderby' param that is potentially related to a meta query.
871          *
872          * @since 4.6.0
873          * @access public
874          *
875          * @param string $orderby_raw Raw 'orderby' value passed to WP_Term_Query.
876          * @return string
877          */
878         protected function parse_orderby_meta( $orderby_raw ) {
879                 $orderby = '';
880
881                 // Tell the meta query to generate its SQL, so we have access to table aliases.
882                 $this->meta_query->get_sql( 'term', 't', 'term_id' );
883                 $meta_clauses = $this->meta_query->get_clauses();
884                 if ( ! $meta_clauses || ! $orderby_raw ) {
885                         return $orderby;
886                 }
887
888                 $allowed_keys = array();
889                 $primary_meta_key = null;
890                 $primary_meta_query = reset( $meta_clauses );
891                 if ( ! empty( $primary_meta_query['key'] ) ) {
892                         $primary_meta_key = $primary_meta_query['key'];
893                         $allowed_keys[] = $primary_meta_key;
894                 }
895                 $allowed_keys[] = 'meta_value';
896                 $allowed_keys[] = 'meta_value_num';
897                 $allowed_keys   = array_merge( $allowed_keys, array_keys( $meta_clauses ) );
898
899                 if ( ! in_array( $orderby_raw, $allowed_keys, true ) ) {
900                         return $orderby;
901                 }
902
903                 switch( $orderby_raw ) {
904                         case $primary_meta_key:
905                         case 'meta_value':
906                                 if ( ! empty( $primary_meta_query['type'] ) ) {
907                                         $orderby = "CAST({$primary_meta_query['alias']}.meta_value AS {$primary_meta_query['cast']})";
908                                 } else {
909                                         $orderby = "{$primary_meta_query['alias']}.meta_value";
910                                 }
911                                 break;
912
913                         case 'meta_value_num':
914                                 $orderby = "{$primary_meta_query['alias']}.meta_value+0";
915                                 break;
916
917                         default:
918                                 if ( array_key_exists( $orderby_raw, $meta_clauses ) ) {
919                                         // $orderby corresponds to a meta_query clause.
920                                         $meta_clause = $meta_clauses[ $orderby_raw ];
921                                         $orderby = "CAST({$meta_clause['alias']}.meta_value AS {$meta_clause['cast']})";
922                                 }
923                                 break;
924                 }
925
926                 return $orderby;
927         }
928
929         /**
930          * Parse an 'order' query variable and cast it to ASC or DESC as necessary.
931          *
932          * @since 4.6.0
933          * @access protected
934          *
935          * @param string $order The 'order' query variable.
936          * @return string The sanitized 'order' query variable.
937          */
938         protected function parse_order( $order ) {
939                 if ( ! is_string( $order ) || empty( $order ) ) {
940                         return 'DESC';
941                 }
942
943                 if ( 'ASC' === strtoupper( $order ) ) {
944                         return 'ASC';
945                 } else {
946                         return 'DESC';
947                 }
948         }
949
950         /**
951          * Used internally to generate a SQL string related to the 'search' parameter.
952          *
953          * @since 4.6.0
954          * @access protected
955          *
956          * @global wpdb $wpdb WordPress database abstraction object.
957          *
958          * @param string $string
959          * @return string
960          */
961         protected function get_search_sql( $string ) {
962                 global $wpdb;
963
964                 $like = '%' . $wpdb->esc_like( $string ) . '%';
965
966                 return $wpdb->prepare( '((t.name LIKE %s) OR (t.slug LIKE %s))', $like, $like );
967         }
968 }