Querying Terms
get_terms() and WP_Term_Query: filtering by taxonomy, controlling hide_empty, ordering, and counting terms.
get_terms( array|string $args = array(), array|string $deprecated = '' ): WP_Term[]|int[]|string[]|string|WP_Errorget_terms() is the front door to WP_Term_Query. Since WordPress 4.5 the taxonomy is passed inside the arguments array as taxonomy; the old get_terms( $taxonomy, $args ) form still works but is legacy. It returns WP_Term objects by default, or a WP_Error when a taxonomy does not exist.
hide_empty
hide_empty defaults to true, and "empty" means wp_term_taxonomy.count is zero. That column is maintained by _update_post_term_count(), which counts only posts with status publish — so a term used exclusively by drafts or private posts is invisible until you pass 'hide_empty' => false. The update_post_term_count_statuses filter (5.7) changes which statuses count.
Ordering and shaping
orderby accepts term fields (name, slug, term_id, parent, description, term_group), plus count, include, slug__in, meta_value, meta_value_num and none; the default is name ascending. fields reshapes the return: ids, slugs, names, tt_ids, count, id=>name, id=>slug, id=>parent.
Counting
'fields' => 'count' returns the number of matching terms as a numeric string. wp_count_terms() does the same and, unlike get_terms(), defaults hide_empty to false.
Example
<?php
// A genre filter dropdown: only genres with published books,
// most used first, capped at 20.
$genres = get_terms( array(
'taxonomy' => 'acme_genre',
'hide_empty' => true, // the default; drops terms whose count is 0
'orderby' => 'count',
'order' => 'DESC',
'number' => 20, // '' or 0 means no limit
'fields' => 'all', // WP_Term[]; the default
) );
if ( is_wp_error( $genres ) ) {
return; // taxonomy is not registered
}
echo '<select name="genre">';
echo '<option value="">' . esc_html__( 'All genres', 'acme' ) . '</option>';
foreach ( $genres as $genre ) {
printf(
'<option value="%s">%s (%d)</option>',
esc_attr( $genre->slug ),
esc_html( $genre->name ),
(int) $genre->count
);
}
echo '</select>';When to use it
- Building a filter dropdown or tag cloud that only lists used terms
- Rendering a nested category menu from a single query
- Sorting terms by a custom weight stored in term meta
- Reporting how many terms a taxonomy holds, used or not
More examples
get_terms() and the WP_Term_Query longhand
Three spellings of the same query. Use get_terms() by default; use WP_Term_Query directly when you want to run several term queries from one object or inspect $query->request while debugging.
<?php
$args = array(
'taxonomy' => 'acme_genre',
'hide_empty' => false,
'orderby' => 'name',
);
// Shorthand: get_terms() instantiates WP_Term_Query, runs it, and passes
// the result through the 'get_terms' filter.
$terms = get_terms( $args );
// Longhand: the object form. Same SQL, same results, minus the filter
// that get_terms() applies at the end.
$query = new WP_Term_Query( $args );
$terms = $query->terms; // also returned by $query->get_terms()
// The object is reusable, which is the reason to reach for it:
$children = $query->query( array(
'taxonomy' => 'acme_genre',
'parent' => 12,
) );
// And the legacy signature, still supported, taxonomy first:
$terms = get_terms( 'acme_genre', array( 'hide_empty' => false ) );
// Prior to WP 4.5 this was the only form. Prefer the args array.Counting terms and counting objects
Two functions with opposite hide_empty defaults, which is a frequent source of off-by-a-lot bugs in dashboards. pad_counts is what you want when a parent term should report everything filed under its children.
<?php
// How many genres exist, including unused ones?
// wp_count_terms() defaults hide_empty to FALSE.
$total = wp_count_terms( array( 'taxonomy' => 'acme_genre' ) ); // numeric string
// The same figure through get_terms(), which defaults hide_empty to TRUE,
// so the argument has to be spelled out.
$total = get_terms( array(
'taxonomy' => 'acme_genre',
'hide_empty' => false,
'fields' => 'count',
) );
// Signature note: the args-array form of wp_count_terms() arrived in WP 5.6.
// Older code passes the taxonomy first: wp_count_terms( 'acme_genre', $args ).
// Per-term object counts: $term->count is the stored count column.
// pad_counts rolls descendants' counts up into their ancestors.
$tree = get_terms( array(
'taxonomy' => 'acme_genre',
'pad_counts' => true,
) );Hierarchy, meta and object-scoped term queries
The arguments that only make sense for hierarchical taxonomies (parent, child_of, childless) plus the two that most people miss: meta-based ordering, and object_ids for querying terms across a known set of posts in one round trip.
<?php
// Direct children of one term only.
$children = get_terms( array(
'taxonomy' => 'acme_genre',
'parent' => 12,
'hide_empty' => false,
) );
// The whole subtree beneath a term, at any depth.
$subtree = get_terms( array(
'taxonomy' => 'acme_genre',
'child_of' => 12,
) );
// Ordered by term meta. 'meta_value_num' casts for numeric sorting.
$featured = get_terms( array(
'taxonomy' => 'acme_genre',
'hide_empty' => false,
'meta_key' => 'acme_sort_weight',
'orderby' => 'meta_value_num',
'order' => 'ASC',
) );
// Terms shared by a set of posts, with the object_id on each result.
$shared = get_terms( array(
'taxonomy' => 'acme_genre',
'object_ids' => array( 41, 42, 43 ), // since WP 4.7
'fields' => 'all_with_object_id',
) );
Discussion