Querying Terms

get_terms() and WP_Term_Query: filtering by taxonomy, controlling hide_empty, ordering, and counting terms.

Syntaxget_terms( array|string $args = array(), array|string $deprecated = '' ): WP_Term[]|int[]|string[]|string|WP_Error

get_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

Example · php
<?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.

Example · php
<?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.

Example · php
<?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.

Example · php
<?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

  • Be the first to comment on this lesson.