WordPress’s WP_Term_Query class is the taxonomy equivalent of WP_Query for posts — it provides a structured, filterable, cached API for querying taxonomy terms with fine-grained control over ordering, meta conditions, hierarchy depth, and what fields are returned. The older get_terms() function is a thin wrapper around WP_Term_Query and is appropriate for simple term retrieval. For complex queries — filtering by term meta, excluding certain terms, ordering by a custom meta value, getting only direct children of a term, or retrieving term IDs for a WHERE IN sub-query — working directly with WP_Term_Query gives you access to the full argument set and the parsed query object for debugging.
Problem: A shop page needs to display product categories sorted by a custom sort_order term meta value, with empty categories hidden, limited to the top-level (not subcategories), and only returning the term ID, name, and slug — not full WP_Term objects — for efficiency.
Solution: Use WP_Term_Query with meta_key for ordering, hide_empty, parent for top-level only, and fields to limit returned data.
<?php
// ── Basic WP_Term_Query ────────────────────────────────────────────────
$term_query = new WP_Term_Query( [
'taxonomy' => 'product_cat',
'hide_empty' => true, // exclude terms with no published posts
'parent' => 0, // top-level only (direct children of root)
'orderby' => 'meta_value_num',// order by numeric meta value
'meta_key' => 'sort_order', // the meta key to order by
'order' => 'ASC',
'fields' => 'all', // 'all' | 'ids' | 'names' | 'id=>name' | 'id=>slug'
] );
$terms = $term_query->get_terms(); // array of WP_Term objects (with 'all')
foreach ( $terms as $term ) {
printf( '<a href="%s">%s (%d)</a>',
esc_url( get_term_link( $term ) ),
esc_html( $term->name ),
(int) $term->count
);
}
// ── Get only IDs (efficient for use in queries) ────────────────────────
$cat_ids = ( new WP_Term_Query( [
'taxonomy' => 'product_cat',
'hide_empty' => false,
'fields' => 'ids', // returns [ 3, 7, 12, 15 ]
] ) )->get_terms();
// ── Filter by term meta ────────────────────────────────────────────────
$featured_terms = new WP_Term_Query( [
'taxonomy' => 'product_cat',
'hide_empty' => true,
'meta_query' => [
[
'key' => 'featured',
'value' => '1',
'compare' => '=',
],
],
] );
// ── Get all children of a specific term ───────────────────────────────
$electronics_id = 7;
$children = new WP_Term_Query( [
'taxonomy' => 'product_cat',
'parent' => $electronics_id, // direct children only
'hide_empty' => false,
'orderby' => 'name',
'order' => 'ASC',
] );
// ── Exclude specific terms ─────────────────────────────────────────────
$all_except_uncategorized = new WP_Term_Query( [
'taxonomy' => 'category',
'exclude' => [ 1 ], // term IDs to skip
'fields' => 'id=>name', // [ 3 => 'WordPress', 5 => 'PHP', ... ]
] );
// ── get_terms() — wrapper, same arguments ─────────────────────────────
// Equivalent to: (new WP_Term_Query(...))->get_terms()
$terms = get_terms( [
'taxonomy' => 'category',
'hide_empty' => true,
'number' => 10,
'orderby' => 'count',
'order' => 'DESC',
] );
NOTE: The 'fields' argument dramatically affects performance — 'ids' returns a plain array of integers with no object instantiation overhead, while 'all' (the default) constructs full WP_Term objects with all properties. Use 'ids' whenever you only need term IDs for further queries, and 'id=>name' or 'id=>slug' for dropdown population. The 'number' argument defaults to 0 (no limit) — always set an explicit limit in production queries to avoid retrieving thousands of terms in large sites. The 'childless' argument (set to true) is useful for returning only leaf-node terms — terms that have no children — which is helpful for faceted filtering UIs.