各位观众,欢迎来到今天的 WordPress 源码解剖现场! 今天我们要扒的是 WordPress 里一个“老实巴交”但又至关重要的类:WP_Term_Query
。 别看它名字长,其实就是个“分类术语查询器”。 想象一下,你需要从数据库里捞出一堆分类目录、标签、或者自定义分类法的术语(term),它就是那个帮你整理参数、发送请求、然后把结果打包送回来的“快递员”。
咱们今天就来拆解一下这个“快递员”,看看它到底是怎么工作的。
开场白: 为什么要解剖 WP_Term_Query
?
你可能会问,WordPress 提供了 get_terms()
函数,直接用它不就好了? 为什么要费劲巴拉地研究 WP_Term_Query
呢? 问得好!
get_terms()
函数底层就是用的 WP_Term_Query
。 get_terms()
就像是“傻瓜相机”,给你预设好了一些参数,方便快速拍照。 但如果你想玩转光圈、快门、ISO,拍出更有创意的照片,那就需要了解相机的底层原理。 WP_Term_Query
就是那个让你了解底层原理的“工具书”。
理解 WP_Term_Query
,你就能:
- 更灵活地控制术语查询,实现更复杂的业务逻辑。
- 更好地理解
get_terms()
函数的底层工作机制。 - 在某些特殊情况下,直接使用
WP_Term_Query
,避免不必要的性能损耗。
第一部分: 类的基本结构
首先,我们来看看 WP_Term_Query
类的基本骨架。 这个类定义在 wp-includes/class-wp-term-query.php
文件里。 打开文件,你会看到类似下面的结构:
<?php
/**
* Core class used to implement the term query.
*
* @since 4.6.0
*/
class WP_Term_Query {
/**
* SQL query string.
*
* @since 4.6.0
* @access public
* @var string
*/
public $request;
/**
* List of term IDs.
*
* @since 4.6.0
* @access public
* @var array
*/
public $terms;
/**
* The term query arguments.
*
* @since 4.6.0
* @access public
* @var array
*/
public $query_vars = array();
/**
* The taxonomy being queried.
*
* @since 4.6.0
* @access public
* @var array
*/
public $taxonomies = array();
/**
* A flat array of taxonomy terms.
*
* @since 4.6.0
* @access public
* @var array
*/
public $_taxonomies = array();
/**
* Cache key for the current query.
*
* @since 4.6.0
* @access public
* @var string
*/
public $cache_key;
/**
* Whether to prime cache for found terms.
*
* @since 4.6.0
* @access public
* @var bool
*/
public $cache_prime;
/**
* Constructor.
*
* @since 4.6.0
* @access public
*
* @param string|array $query {
* Array or string of term query arguments. See {@see WP_Term_Query::get_terms()}
* for information on accepted arguments.
*
* @type array 'taxonomy' Taxonomy or taxonomies to query.
* @type string 'search' Search term.
* @type string 'orderby' How to order matching terms. Accepts 'name', 'slug',
* 'term_group', 'term_id', 'id', 'description', 'count',
* 'none', or a custom field key. Default 'name'.
* @type string 'order' Whether to order terms in ascending or descending order.
* Accepts 'ASC', 'DESC'. Default 'ASC'.
* @type bool 'hide_empty' Whether to hide terms not assigned to any posts.
* Default false.
* @type array 'include' Array of term IDs to include. Default empty array.
* @type array 'exclude' Array of term IDs to exclude. Default empty array.
* @type array 'exclude_tree' Array of term IDs to exclude along with all of their
* descendants. Default empty array.
* @type int 'number' Maximum number of terms to return. Default is to return
* all terms.
* @type int 'offset' The number of terms to skip. Default 0.
* @type string 'fields' Which fields to return. Accepts 'all', 'ids', 'names',
* 'id=>name', 'id=>parent'. Default 'all'.
* @type string|array 'slug' Slug or array of slugs to filter by.
* @type bool 'hierarchical' Whether to include terms which are hierarchical
* descendants of the terms included in the 'include'
* argument. Default true.
* @type string 'name__like' Retrieve terms where the name is LIKE the input string.
* @type string 'description__like' Retrieve terms where the description is LIKE the input
* string.
* @type bool 'pad_counts' Whether to pad the count of each term with the number
* of its descendants. Default false.
* @type string 'get' What to return. Accepts ''|'all' for all (default),
* 'id=>parent' for term ID => parent term ID pairs,
* 'id=>name' for term ID => term name pairs,
* 'count' for the total term count.
* @type string 'name' Retrieve terms with this name.
* @type int 'child_of' Term ID to retrieve child terms of. Default 0.
* @type int 'parent' Term ID to retrieve direct-child terms of. Default empty.
* @type string|array 'childless' True to limit results to terms that have no children.
* False to disable the children filter. Default false.
* @type string 'cache_domain' Unique cache domain to preemptively search and store
* terms in. Default 'core'.
* @type bool 'update_term_meta_cache' Whether to prime term meta cache for any found terms.
* Default true.
* @type string 'meta_key' Meta key to filter by.
* @type string 'meta_value' Meta value to filter by.
* @type string 'meta_compare' Comparison operator to test the 'meta_value'. Accepts
* '=', '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE',
* 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'EXISTS'
* (alias of 'TRUE'), and 'NOT EXISTS' (alias of 'FALSE').
* Default '='.
* @type array 'meta_query' An associative array of meta query clauses, or array of
* meta query clauses. See WP_Meta_Query::get_sql() for
* more information.
* @type string 'taxonomy' Taxonomy or taxonomies to query.
* }
*/
public function __construct( $query = '' ) {
if ( ! empty( $query ) ) {
$this->query( $query );
}
}
/**
* Parses arguments passed to the term query with default query parameters.
*
* @since 4.6.0
* @access public
*
* @param string|array $query Array of query variables.
* @return array Array of parsed query variables.
*/
public function parse_query( $query = '' ) {
$this->query_vars = wp_parse_args( $query, $this->query_vars );
$this->query_vars = sanitize_term_field( 'query_vars', $this->query_vars, 0, 'term', 'query' );
/**
* Filters the term query arguments.
*
* @since 4.6.0
*
* @param array $query_vars The array of term query variables.
* @param WP_Term_Query $this The WP_Term_Query instance.
*/
$this->query_vars = apply_filters( 'terms_clauses', $this->query_vars, $this );
return $this->query_vars;
}
/**
* Sets up the WordPress query for retrieving terms.
*
* @since 4.6.0
* @access public
*
* @param string|array $query Array of query variables.
* @return array|int List of terms/term IDs, or number of terms when 'count' is passed as a query var.
*/
public function query( $query ) {
$this->query_vars = $this->parse_query( $query );
/**
* Fires before the term query.
*
* @since 4.6.0
*
* @param array $query_vars The array of term query variables.
* @param WP_Term_Query $this The WP_Term_Query instance.
*/
do_action( 'pre_get_terms', $this->query_vars, $this );
$this->terms = $this->get_terms( $this->query_vars );
return $this->terms;
}
/**
* Generates SQL clauses to be appended to the main SQL query.
*
* @since 4.6.0
* @access protected
*
* @param array $args Query arguments.
* @return array An associative array of SQL clauses.
*/
protected function get_sql_clauses( $args = array() ) {
global $wpdb;
$args = wp_parse_args( $args, array(
'orderby' => 'name',
'order' => 'ASC',
'fields' => 'all',
) );
$orderby = $args['orderby'];
$order = $args['order'];
$fields = $args['fields'];
$where = '';
$join = '';
$sort = '';
$groupby = '';
$limits = '';
// Taxonomy query.
$this->transform_query( $args, 'taxonomy', 'term_taxonomy', 'taxonomy', $this->taxonomies );
$clauses = $this->get_tax_sql( $args );
if ( ! empty( $clauses['where'] ) ) {
$where .= ' AND ' . $clauses['where'];
}
if ( ! empty( $clauses['join'] ) ) {
$join .= $clauses['join'];
}
// Meta query.
$meta_query = new WP_Meta_Query();
$meta_query->parse_query_vars( $args );
$meta_sql = $meta_query->get_sql( 'term', $wpdb->terms, 'term_id' );
if ( ! empty( $meta_sql['where'] ) ) {
$where .= ' AND ' . $meta_sql['where'];
}
if ( ! empty( $meta_sql['join'] ) ) {
$join .= $meta_sql['join'];
}
$search = '';
if ( isset( $args['search'] ) && '' !== $args['search'] ) {
$term_search = esc_sql( $wpdb->esc_like( $args['search'] ) );
$search = "AND ( t.name LIKE '%{$term_search}%' )";
}
/**
* Filters the terms search SQL clause.
*
* @since 5.1.0
*
* @param string $search SQL search clause.
* @param WP_Term_Query $this The WP_Term_Query instance.
*/
$search = apply_filters( 'get_terms_search_sql', $search, $this );
$where .= $search;
// include / exclude.
if ( ! empty( $args['include'] ) ) {
$ids = implode( ',', array_map( 'intval', $args['include'] ) );
$where .= " AND t.term_id IN ($ids)";
} elseif ( ! empty( $args['exclude'] ) ) {
$ids = implode( ',', array_map( 'intval', $args['exclude'] ) );
$where .= " AND t.term_id NOT IN ($ids)";
}
if ( ! empty( $args['slug'] ) ) {
if ( is_array( $args['slug'] ) ) {
$slug = "'" . implode( "', '", array_map( 'esc_sql', $args['slug'] ) ) . "'";
} else {
$slug = "'" . esc_sql( $args['slug'] ) . "'";
}
$where .= " AND t.slug IN ($slug)";
}
if ( isset( $args['name'] ) ) {
$name = esc_sql( $args['name'] );
$where .= " AND t.name = '$name'";
}
if ( isset( $args['name__like'] ) ) {
$name_like = esc_sql( $wpdb->esc_like( $args['name__like'] ) );
$where .= " AND t.name LIKE '%$name_like%'";
}
if ( isset( $args['description__like'] ) ) {
$description_like = esc_sql( $wpdb->esc_like( $args['description__like'] ) );
$where .= " AND tt.description LIKE '%$description_like%'";
}
if ( ! empty( $args['term_id'] ) ) {
$term_id = intval( $args['term_id'] );
$where .= " AND t.term_id = '$term_id'";
}
// Hide empty terms.
if ( isset( $args['hide_empty'] ) && $args['hide_empty'] ) {
$where .= ' AND tt.count > 0';
}
// 'child_of' makes the query slow, so use it with care.
if ( ! empty( $args['child_of'] ) ) {
$child_of = intval( $args['child_of'] );
if ( isset( $args['hierarchical'] ) && ! $args['hierarchical'] ) {
$where .= " AND tt.parent = $child_of";
} else {
$term_children = get_term_children( $child_of, $this->taxonomies[0] );
if ( empty( $term_children ) ) {
$where .= ' AND 0 = 1';
} else {
$ids = implode( ',', array_map( 'intval', $term_children ) );
$where .= " AND t.term_id IN ($ids)";
}
}
}
if ( isset( $args['parent'] ) ) {
if ( is_numeric( $args['parent'] ) ) {
$parent = intval( $args['parent'] );
$where .= " AND tt.parent = '$parent'";
} else {
if ( 'any' === $args['parent'] ) {
$where .= ' AND tt.parent > 0';
} else {
$where .= ' AND tt.parent = 0';
}
}
}
if ( isset( $args['childless'] ) ) {
if ( filter_var( $args['childless'], FILTER_VALIDATE_BOOLEAN ) ) {
$where .= ' AND tt.term_id NOT IN ( SELECT tt.parent FROM ' . $wpdb->term_taxonomy . ' tt WHERE tt.parent != 0 )';
}
}
/*
* 'get' => 'all' is the only supported value.
*
* Other values of 'get' supported by get_terms() are deprecated
* as of 4.5.0, as they are wasteful.
*/
if ( isset( $args['get'] ) && 'count' === $args['get'] ) {
$fields = 'count';
}
switch ( $orderby ) {
case 'none':
break;
case 'term_id':
case 'id':
$sort = 'ORDER BY t.term_id';
break;
case 'name':
$sort = 'ORDER BY t.name';
break;
case 'slug':
$sort = 'ORDER BY t.slug';
break;
case 'term_group':
$sort = 'ORDER BY t.term_group';
break;
case 'description':
$sort = 'ORDER BY tt.description';
break;
case 'count':
$sort = 'ORDER BY tt.count';
break;
case 'parent':
$sort = 'ORDER BY tt.parent';
break;
case 'include':
$sort = 'ORDER BY FIELD(t.term_id, ' . implode( ',', array_map( 'intval', $args['include'] ) ) . ')';
break;
default:
$sort = 'ORDER BY t.name';
break;
}
if ( ! empty( $sort ) ) {
$sort .= ' ' . $order;
}
if ( ! empty( $args['number'] ) && ! empty( $args['offset'] ) ) {
$limits = $wpdb->prepare( 'LIMIT %d, %d', $args['offset'], $args['number'] );
} elseif ( ! empty( $args['number'] ) ) {
$limits = $wpdb->prepare( 'LIMIT %d', $args['number'] );
} elseif ( ! empty( $args['offset'] ) ) {
$limits = $wpdb->prepare( 'LIMIT %d, %d', $args['offset'], PHP_INT_MAX );
}
return compact( 'where', 'join', 'sort', 'fields', 'groupby', 'limits' );
}
/**
* Get terms.
*
* @since 4.6.0
* @access public
*
* @param array $query_vars Term query arguments.
* @return array|int List of terms/term IDs, or number of terms when 'count' is passed as a query var.
*/
public function get_terms( $query_vars = array() ) {
global $wpdb;
$defaults = array(
'search' => '',
'cache_domain' => 'core',
'update_term_meta_cache' => true,
);
$query_vars = wp_parse_args( $query_vars, $defaults );
/**
* Fires just before calling the terms query.
*
* @since 4.6.0
*
* @param WP_Term_Query $this The WP_Term_Query instance.
*/
do_action( 'pre_get_terms', $this );
/**
* Filters the terms query before querying the database.
*
* @since 4.6.0
*
* @param array $clauses An array of SQL clauses.
* @param WP_Term_Query $this The WP_Term_Query instance.
*/
$clauses = apply_filters( 'terms_clauses', $this->get_sql_clauses( $query_vars ), $this );
$fields = isset( $clauses['fields'] ) ? $clauses['fields'] : '';
// Fields.
if ( 'count' === $fields ) {
$fields = 'COUNT(*)';
} else {
$fields = 't.*, tt.*';
}
// Query.
$found_terms = array();
$this->request = "SELECT $fields FROM {$wpdb->terms} AS t INNER JOIN {$wpdb->term_taxonomy} AS tt ON t.term_id = tt.term_id {$clauses['join']} WHERE 1=1 {$clauses['where']} {$clauses['groupby']} {$clauses['sort']} {$clauses['limits']}";
if ( 'count' === $fields ) {
$this->request = "SELECT $fields FROM {$wpdb->term_taxonomy} AS tt {$clauses['join']} WHERE 1=1 {$clauses['where']} {$clauses['groupby']}";
}
$this->request = apply_filters( 'terms_request', $this->request, $query_vars, $this );
if ( 'count' === $fields ) {
$count = $wpdb->get_var( $this->request );
return absint( $count );
}
$cache_key = $this->cache_key();
$cache = wp_cache_get( $cache_key, 'terms' );
if ( false !== $cache ) {
$this->terms = $cache;
/**
* Fires after terms have been retrieved from the cache.
*
* @since 4.6.0
*
* @param array $this->terms The list of terms.
* @param WP_Term_Query $this The WP_Term_Query instance.
*/
do_action( 'get_terms_from_cache', $this->terms, $this );
return $this->terms;
}
$terms = $wpdb->get_results( $this->request );
// Prime term meta cache.
if ( $query_vars['update_term_meta_cache'] ) {
update_term_meta_cache( wp_list_pluck( $terms, 'term_id' ) );
}
// Convert to WP_Term objects.
$term_results = array();
if ( ! empty( $terms ) ) {
foreach ( $terms as $term ) {
$term_results[] = get_term( $term );
}
}
$this->terms = $term_results;
wp_cache_set( $cache_key, $this->terms, 'terms', DAY_IN_SECONDS );
/**
* Fires after terms have been retrieved from the database.
*
* @since 4.6.0
*
* @param array $this->terms The list of terms.
* @param WP_Term_Query $this The WP_Term_Query instance.
*/
do_action( 'get_terms', $this->terms, $this );
return $this->terms;
}
/**
* Generates a cache key.
*
* @since 4.6.0
* @access public
*
* @return string Unique cache key.
*/
public function cache_key() {
global $wpdb;
$key = md5( serialize( array_merge( $this->query_vars, array(
'taxonomies' => $this->taxonomies,
'db_version' => get_option( 'db_version' ),
'blog_id' => get_current_blog_id(),
) ) ) );
$this->cache_key = sanitize_key( $key );
return $this->cache_key;
}
/**
* Used internally to transform query variables to format expected in
* SQL.
*
* @since 4.6.0
* @access protected
*
* @param array $query Query variables.
* @param string $query_key The query key.
* @param string $db_key The database key.
* @param string $key The key.
* @param array $query_var The query variable.
*/
protected function transform_query( &$query, $query_key, $db_key, $key, &$query_var ) {
if ( ! isset( $query[ $query_key ] ) ) {
return;
}
$this->set_query_var( $query[ $query_key ], $query_var );
if ( ! empty( $query_var ) ) {
$query[ $db_key ] = $query_var;
}
unset( $query[ $query_key ] );
}
/**
* Sets the value of a query variable.
*
* @since 4.6.0
* @access protected
*
* @param array|string $value The query variable value.
* @param array $var The query variable.
*/
protected function set_query_var( $value, &$var ) {
if ( ! is_array( $value ) ) {
$value = preg_split( '/[,s]+/', $value );
}
$var = array_filter( array_unique( array_map( 'sanitize_key', $value ) ) );
}
/**
* Get SQL for taxonomies.
*
* @since 4.6.0
* @access protected
*
* @param array $args Query arguments.
* @return array SQL clauses relating to taxonomy queries.
*/
protected function get_tax_sql( $args = array() ) {
global $wpdb;
$where = '';
$join = '';
$taxonomies = isset( $args['term_taxonomy'] ) ? $args['term_taxonomy'] : array();
if ( ! empty( $taxonomies ) ) {
$taxonomies = "'" . implode( "', '", array_map( 'esc_sql', $taxonomies ) ) . "'";
$where .= "AND tt.taxonomy IN ($taxonomies)";
}
return compact( 'where', 'join' );
}
/**
* Determines whether the query is for a taxonomy archive page.
*
* @since 4.6.0
* @access public
*
* @param string|array $taxonomy Optional. Taxonomy name or array of taxonomy names to check against.
* Default empty.
* @return bool Whether the query is for an existing taxonomy archive page.
*/
public function is_taxonomy( $taxonomy = '' ) {
if ( ! is_array( $taxonomy ) ) {
$taxonomy = array( $taxonomy );
}
if ( empty( $taxonomy ) || empty( $this->taxonomies ) ) {
return false;
}
$intersect = array_intersect( $taxonomy, $this->taxonomies );
return ! empty( $intersect );
}
}
我们来简单解读一下:
$request
: 存储最终的 SQL 查询语句。 这是个很重要的属性,你可以通过它看到WP_Term_Query
最终生成的 SQL,方便调试。$terms
: 存储查询结果,也就是符合条件的术语对象数组。$query_vars
: 存储查询参数,比如taxonomy
、orderby
、order
等等。 这些参数决定了你要查询哪些术语,以及如何排序。$taxonomies
: 存储要查询的分类法名称。__construct( $query = '' )
: 构造函数,接收一个查询参数数组或者字符串,并调用query()
方法来执行查询。parse_query( $query = '' )
: 解析查询参数,将传入的参数与默认参数合并,并进行安全过滤。query( $query )
: 执行查询的核心方法。 它会先调用parse_query()
解析参数,然后调用get_terms()
获取查询结果。get_terms( $query_vars = array() )
: 真正从数据库中获取术语的方法。 它会根据查询参数生成 SQL 语句,然后执行查询,并将结果返回。get_sql_clauses( $args = array() )
: 生成 SQL 语句的各个部分(WHERE, JOIN, ORDER BY, LIMIT)。 这是个很关键的方法,它决定了最终的 SQL 语句长什么样。cache_key()
: 生成缓存键,用于缓存查询结果。
第二部分: 查询参数详解
WP_Term_Query
提供了非常丰富的查询参数,可以满足各种各样的查询需求。 这些参数都可以在 get_terms()
函数中使用,也可以直接传递给 WP_Term_Query
的构造函数。
我们来逐一看看这些常用的参数:
参数名 | 类型 | 描述 | 默认值 |
---|---|---|---|
taxonomy |
string/array | 要查询的分类法名称。 可以是单个分类法名称,也可以是多个分类法名称的数组。 | 无 |
search |
string | 搜索关键词。 可以根据术语的名称进行模糊搜索。 | ” |
orderby |
string | 排序方式。 常用的值有 name (按名称排序)、slug (按别名排序)、term_id (按 ID 排序)、count (按文章数排序)等。 |
'name' |
order |
string | 排序顺序。 ASC (升序)或 DESC (降序)。 |
'ASC' |
hide_empty |
bool | 是否隐藏没有文章关联的术语。 | false |
include |
array | 要包含的术语 ID 数组。 只有这些 ID 的术语才会被返回。 | array() |
exclude |
array | 要排除的术语 ID 数组。 这些 ID 的术语不会被返回。 | array() |
exclude_tree |
array | 要排除的术语 ID 数组,以及它们的子术语。 | array() |
number |
int | 返回的术语数量。 如果不设置,则返回所有符合条件的术语。 | 无 |
offset |
int | 偏移量。 从第几个术语开始返回。 | 0 |
fields |
string | 返回的字段。 常用的值有 all (返回所有字段,默认值)、ids (只返回 ID 数组)、names (只返回名称数组)、id=>name (返回 ID => 名称的关联数组)。 |
'all' |
slug |
string/array | 要查询的术语别名。 可以是单个别名,也可以是多个别名的数组。 | 无 |
hierarchical |
bool | 是否返回层级关系的术语。 只在 taxonomy 为层级分类法(如分类目录)时有效。 |
true |
name__like |
string | 模糊匹配术语名称。 | 无 |
description__like |
string | 模糊匹配术语描述。 | 无 |
pad_counts |
bool | 是否填充文章数。 对于层级分类法,如果设置为 true ,则每个术语的文章数会包含其子术语的文章数。 |
false |
parent |
int | 只返回指定父级术语的子术语。 | 无 |
child_of |
int | 返回指定术语的所有子术语。 | 0 |
meta_key |
string | 自定义字段的键名。 可以根据自定义字段的值进行过滤。 | 无 |
meta_value |
string | 自定义字段的值。 需要和 meta_key 配合使用。 |
无 |
meta_compare |
string | 自定义字段的比较操作符。 常用的值有 = 、!= 、> 、< 、LIKE 等。 |
'=' |
meta_query |
array | 更复杂的自定义字段查询。 可以组合多个 meta_key 、meta_value 和 meta_compare 条件。 具体用法可以参考 WP_Meta_Query 类。 |
array() |
update_term_meta_cache |
bool | 是否更新术语的元数据缓存。 如果设置为 true ,则会一次性加载所有术语的元数据,可以提高性能。 |
true |
childless |
bool | 是否只返回没有子分类的分类,或者返回所有分类。true:只返回没有子分类的分类,false:返回所有分类,默认值false | false |
第三部分: 代码示例
光说不练假把式,咱们来几个实际的例子。
例子 1: 获取所有 "category" 分类法的术语,按名称升序排列,隐藏空分类。
$args = array(
'taxonomy' => 'category',
'orderby' => 'name',
'order' => 'ASC',
'hide_empty' => true,