大家好,欢迎来到今天的 WordPress 源码探秘讲座,我是你们的导游,就叫我老码吧!今天咱们要一起扒一扒 WordPress 里的 WP_Term_Query
类,看看它是怎么把分类术语给揪出来的。准备好了吗?咱们这就开始!
WP_Term_Query
:术语猎手
WP_Term_Query
类,顾名思义,就是用来查询分类术语的。它就像一个经验老道的猎手,能根据你提供的各种条件,在 WordPress 的分类术语数据库里精准地找到你想要的猎物(也就是术语)。
先睹为快:WP_Term_Query
的基本用法
在深入源码之前,咱们先来熟悉一下 WP_Term_Query
的基本用法,这样能更好地理解它背后的原理。
$args = array(
'taxonomy' => 'category', // 指定分类法,例如 category、post_tag 等
'hide_empty' => false, // 是否隐藏空分类,默认为 true
'number' => 5, // 返回术语的数量,默认为返回所有术语
'orderby' => 'name', // 排序方式,例如 name、count、term_id 等
'order' => 'ASC', // 排序顺序,ASC(升序)或 DESC(降序)
);
$term_query = new WP_Term_Query( $args );
if ( ! empty( $term_query->terms ) ) {
foreach ( $term_query->terms as $term ) {
echo '<p>' . $term->name . '</p>';
}
}
这段代码的意思是:我要查询 category
分类法下的 5 个术语,按照名称升序排列,并且包括空分类。查询结果会存储在 $term_query->terms
数组里,然后我们就可以遍历这个数组,输出每个术语的名称。
进入源码:WP_Term_Query
的内部结构
现在,咱们来深入 WP_Term_Query
类的源码,看看它是如何实现这些功能的。WP_Term_Query
类的核心方法是 get_terms()
,它负责执行实际的数据库查询。
class WP_Term_Query {
public $query;
public $query_vars;
public $terms;
public $term_count = 0;
public $taxonomy;
public $queried_terms;
public function __construct( $query = '' ) {
$this->query = $this->parse_query( $query );
$this->query_vars = wp_parse_args( $this->query );
$this->get_terms();
}
public function parse_query( $query ) {
$defaults = array(
'taxonomy' => '',
'object_ids' => null,
'search' => '',
'slug' => '',
'term_id' => '',
'name' => '',
'hide_empty' => true,
'cache_domain' => 'core',
'update_term_meta_cache' => true,
'update_term_cache' => true,
'orderby' => 'name',
'order' => 'ASC',
'number' => '',
'offset' => '',
'fields' => 'all',
'count' => false,
'hierarchical' => true,
'name__like' => '',
'description__like' => '',
'pad_counts' => false,
'get' => '',
'child_of' => 0,
'parent' => '',
'childless' => false,
'exclude' => '',
'exclude_tree' => '',
'include' => '',
'term_taxonomy_id' => '',
'tax_query' => null,
'meta_query' => null,
'lang' => '',
'suppress_filter' => false,
);
$query = wp_parse_args( $query, $defaults );
/**
* Filters the taxonomy query arguments.
*
* @since 4.6.0
*
* @param array $query An array of taxonomy query arguments.
*/
$query = apply_filters( 'terms_clauses', $query );
return $query;
}
public function get_terms() {
global $wpdb;
$fields = isset( $this->query_vars['fields'] ) ? $this->query_vars['fields'] : 'all';
$args = wp_parse_args( $this->query_vars, array(
'get' => 'all',
'number' => '',
'offset' => '',
) );
$number = absint( $args['number'] );
$offset = absint( $args['offset'] );
$defaults = array(
'search' => '',
'cache_domain' => 'core',
'update_term_meta_cache' => true,
'update_term_cache' => true,
'orderby' => 'name',
'order' => 'ASC',
'fields' => 'all',
'count' => false,
'hide_empty' => true,
'hierarchical' => true,
'name__like' => '',
'description__like' => '',
'pad_counts' => false,
'child_of' => 0,
'parent' => '',
'childless' => false,
'exclude' => '',
'exclude_tree' => '',
'include' => '',
'term_taxonomy_id' => '',
'tax_query' => null,
'meta_query' => null,
'lang' => '',
'suppress_filter' => false,
);
$args = wp_parse_args( $args, $defaults );
$this->query = $args;
// Backward compatibility for meta_key and meta_value.
if ( ! empty( $args['meta_key'] ) && ! isset( $args['meta_query'] ) ) {
$args['meta_query'] = array(
array(
'key' => $args['meta_key'],
'value' => $args['meta_value'],
'compare' => isset( $args['meta_compare'] ) ? $args['meta_compare'] : '=',
),
);
}
$taxonomies = $args['taxonomy'];
if ( ! is_array( $taxonomies ) ) {
$taxonomies = array_filter( preg_split( '/[,s]+/', $taxonomies ) );
}
$taxonomies = array_intersect( $taxonomies, get_taxonomies() );
$this->taxonomies = $taxonomies;
if ( empty( $taxonomies ) ) {
return array();
}
$terms = array();
$id_fields = array( 'term_id', 'tt_id', 'id' );
$orderby = strtolower( $args['orderby'] );
if ( in_array( $orderby, $id_fields, true ) ) {
$orderby = 't.term_id';
} elseif ( 'name' === $orderby ) {
$orderby = 't.name';
} elseif ( 'slug' === $orderby ) {
$orderby = 't.slug';
} elseif ( 'term_group' === $orderby ) {
$orderby = 't.term_group';
} elseif ( 'count' === $orderby ) {
$orderby = 'tt.count';
} elseif ( isset( $wpdb->termmeta ) && 'meta_value' === $orderby ) {
// @todo Still needs to be fully implemented.
$orderby = 'tm.meta_value';
}
$order = strtoupper( $args['order'] );
if ( 'DESC' !== $order && 'ASC' !== $order ) {
$order = 'ASC';
}
$where = "WHERE tt.taxonomy IN ('" . implode( "', '", array_map( 'esc_sql', $taxonomies ) ) . "')";
if ( ! empty( $args['object_ids'] ) ) {
$object_ids = wp_parse_id_list( $args['object_ids'] );
$where .= " AND tr.object_id IN (" . implode( ',', array_map( 'intval', $object_ids ) ) . ")";
}
if ( '' !== $args['search'] ) {
$search = trim( $args['search'], '* ' );
$like = '%' . $wpdb->esc_like( $search ) . '%';
$where .= $wpdb->prepare( ' AND t.name LIKE %s', $like );
}
if ( '' !== $args['slug'] ) {
$slugs = array_map( 'sanitize_title', (array) $args['slug'] );
$where .= " AND t.slug IN ('" . implode( "', '", array_map( 'esc_sql', $slugs ) ) . "')";
}
if ( '' !== $args['name'] ) {
$names = array_map( 'trim', (array) $args['name'] );
$where .= " AND t.name IN ('" . implode( "', '", array_map( 'esc_sql', $names ) ) . "')";
}
if ( '' !== $args['name__like'] ) {
$like = '%' . $wpdb->esc_like( trim( $args['name__like'] ) ) . '%';
$where .= $wpdb->prepare( ' AND t.name LIKE %s', $like );
}
if ( '' !== $args['description__like'] ) {
$like = '%' . $wpdb->esc_like( trim( $args['description__like'] ) ) . '%';
$where .= $wpdb->prepare( ' AND tt.description LIKE %s', $like );
}
if ( is_numeric( $args['parent'] ) ) {
if ( $args['childless'] ) {
$where .= $wpdb->prepare( ' AND tt.parent = %d', 0 );
} else {
$where .= $wpdb->prepare( ' AND tt.parent = %d', $args['parent'] );
}
}
if ( '' !== $args['child_of'] ) {
$child_of = intval( $args['child_of'] );
if ( $child_of ) {
$hierarchy = _get_term_hierarchy( $taxonomies );
$term_ids = array( $child_of );
if ( isset( $hierarchy[ $child_of ] ) ) {
$term_ids = array_merge( $term_ids, $hierarchy[ $child_of ] );
}
$where .= " AND t.term_id IN (" . implode( ',', array_map( 'intval', $term_ids ) ) . ")";
}
}
if ( '' !== $args['exclude'] ) {
$exclude = wp_parse_id_list( $args['exclude'] );
$where .= " AND t.term_id NOT IN (" . implode( ',', array_map( 'intval', $exclude ) ) . ")";
}
if ( '' !== $args['exclude_tree'] ) {
$exclude_tree = wp_parse_id_list( $args['exclude_tree'] );
$hierarchy = _get_term_hierarchy( $taxonomies );
foreach ( (array) $exclude_tree as $term_id ) {
if ( isset( $hierarchy[ $term_id ] ) ) {
$exclude_tree = array_merge( $exclude_tree, $hierarchy[ $term_id ] );
}
}
$where .= " AND t.term_id NOT IN (" . implode( ',', array_map( 'intval', $exclude_tree ) ) . ")";
}
if ( '' !== $args['include'] ) {
$include = wp_parse_id_list( $args['include'] );
$where .= " AND t.term_id IN (" . implode( ',', array_map( 'intval', $include ) ) . ")";
}
if ( '' !== $args['term_id'] ) {
$term_ids = wp_parse_id_list( $args['term_id'] );
$where .= " AND t.term_id IN (" . implode( ',', array_map( 'intval', $term_ids ) ) . ")";
}
if ( '' !== $args['term_taxonomy_id'] ) {
$term_taxonomy_ids = wp_parse_id_list( $args['term_taxonomy_id'] );
$where .= " AND tt.term_taxonomy_id IN (" . implode( ',', array_map( 'intval', $term_taxonomy_ids ) ) . ")";
}
if ( $args['hide_empty'] ) {
$where .= ' AND tt.count > 0';
}
if ( isset( $args['tax_query'] ) && ! empty( $args['tax_query'] ) ) {
$tax_query = new WP_Tax_Query( $args['tax_query'] );
$where .= $tax_query->get_sql( 'tt', 'term_id', 'AND' );
}
if ( isset( $args['meta_query'] ) && ! empty( $args['meta_query'] ) ) {
$meta_query = new WP_Meta_Query( $args['meta_query'] );
$where .= $meta_query->get_sql( 'term', 't.term_id', 'AND' );
}
$join = "INNER JOIN {$wpdb->term_taxonomy} AS tt ON t.term_id = tt.term_id";
$join .= " INNER JOIN {$wpdb->terms} AS t ON t.term_id = tt.term_id";
if ( ! empty( $args['object_ids'] ) ) {
$join .= " INNER JOIN {$wpdb->term_relationships} AS tr ON tt.term_taxonomy_id = tr.term_taxonomy_id";
}
if ( isset( $args['tax_query'] ) && ! empty( $args['tax_query'] ) ) {
$join .= $tax_query->get_sql_join( 'tt', 'term_id' );
}
if ( isset( $args['meta_query'] ) && ! empty( $args['meta_query'] ) ) {
$join .= $meta_query->get_sql_join( 'term', 't.term_id' );
}
$orderby_inner = '';
if ( 'tt.parent' === $orderby ) {
$orderby_inner = 'tt.parent ' . $order;
}
$orderby = apply_filters( 'get_terms_orderby', $orderby, $args );
$order = apply_filters( 'get_terms_order', $order, $args );
$orderby_sql = "ORDER BY {$orderby} {$order}";
if ( ! empty( $orderby_inner ) ) {
$orderby_sql = "ORDER BY {$orderby_inner}, {$orderby} {$order}";
}
$groupby = '';
if ( ! empty( $args['object_ids'] ) ) {
$groupby = 'GROUP BY t.term_id';
}
$limit = '';
if ( ! empty( $number ) ) {
if ( empty( $offset ) ) {
$limit = "LIMIT {$number}";
} else {
$limit = "LIMIT {$offset}, {$number}";
}
}
$fields = trim( $fields );
if ( 'all' === $fields ) {
$fields = 't.*, tt.*';
} elseif ( 'ids' === $fields ) {
$fields = 't.term_id';
} elseif ( 'names' === $fields ) {
$fields = 't.name';
} elseif ( 'slugs' === $fields ) {
$fields = 't.slug';
} elseif ( is_array( $fields ) ) {
$fields = implode( ',', $fields );
}
$found_rows = '';
if ( ! empty( $number ) && $args['count'] ) {
$found_rows = 'SQL_CALC_FOUND_ROWS';
}
$select = "SELECT {$found_rows} {$fields}";
$sql = "{$select} FROM {$wpdb->terms} AS t {$join} {$where} {$groupby} {$orderby_sql} {$limit}";
$sql = apply_filters( 'get_terms_sql', $sql, $args, $taxonomies );
if ( ! empty( $number ) && $args['count'] ) {
$this->term_count = $wpdb->get_var( "SELECT FOUND_ROWS()" );
}
if ( 'ids' === $args['fields'] ) {
$terms = $wpdb->get_col( $sql );
$terms = array_map( 'intval', $terms );
wp_cache_add_non_object_ids( $terms, 'terms', $args['cache_domain'] );
} else {
$terms = $wpdb->get_results( $sql );
update_term_cache( $terms, $taxonomies, $args['update_term_cache'] );
if ( $args['update_term_meta_cache'] ) {
update_termmeta_cache( wp_list_pluck( $terms, 'term_id' ) );
}
}
if ( is_wp_error( $wpdb->last_error ) ) {
return new WP_Error( 'db_query_error', __( 'Database query error.' ), $wpdb->last_error );
}
if ( $args['hierarchical'] && 'all' === $args['get'] ) {
_pad_term_counts( $terms, $taxonomies, $args['hide_empty'] );
}
if ( $args['pad_counts'] ) {
_pad_term_counts( $terms, $taxonomies, $args['hide_empty'] );
}
if ( ! empty( $number ) && $args['count'] ) {
$this->query_vars['count'] = $this->term_count;
}
$this->terms = $terms;
return $terms;
}
}
get_terms()
方法分解
get_terms()
方法是 WP_Term_Query
类的灵魂,咱们来一步步地分析它的实现过程:
-
参数解析与准备:
- 首先,它会接收构造函数传入的查询参数,并进行一些预处理,例如设置默认值、处理分类法参数等。
- 它会检查
meta_key
和meta_value
参数,如果存在,则将其转换为meta_query
数组,以便后续处理。 - 对传入的
taxonomy
参数进行处理,确保其为数组,并且是已注册的分类法。
-
构建 SQL 查询语句:
- 这是最核心的部分,
get_terms()
方法会根据查询参数构建 SQL 查询语句。 - 它会根据
orderby
和order
参数设置排序方式和顺序。 - 它会根据各种条件参数(例如
object_ids
、search
、slug
、name
、parent
、child_of
、exclude
、include
等)构建WHERE
子句,用于筛选术语。 - 它会根据
tax_query
和meta_query
参数构建更复杂的筛选条件。 - 它会根据
number
和offset
参数设置LIMIT
子句,用于限制返回结果的数量。 - 它会根据
fields
参数设置SELECT
子句,用于指定要返回的字段。
- 这是最核心的部分,
-
执行 SQL 查询:
- 构建好 SQL 查询语句后,
get_terms()
方法会使用$wpdb
对象执行查询。 - 它会根据
fields
参数选择不同的查询方法(例如get_col()
用于查询 ID,get_results()
用于查询完整对象)。
- 构建好 SQL 查询语句后,
-
处理查询结果:
- 查询完成后,
get_terms()
方法会对查询结果进行一些处理,例如更新术语缓存、更新术语元数据缓存等。 - 如果设置了
hierarchical
或pad_counts
参数,则会调用_pad_term_counts()
函数来更新术语计数。 - 最后,它会将查询结果存储在
$this->terms
属性中,并返回结果。
- 查询完成后,
重要参数详解
参数名 | 说明 |
---|---|
taxonomy |
指定分类法,例如 category 、post_tag 等。 |
object_ids |
指定关联的文章 ID,只返回与这些文章关联的术语。 |
search |
搜索术语名称。 |
slug |
根据术语别名查询。 |
term_id |
根据术语 ID 查询。 |
name |
根据术语名称查询。 |
hide_empty |
是否隐藏空分类,默认为 true 。 |
orderby |
排序方式,例如 name 、count 、term_id 等。 |
order |
排序顺序,ASC (升序)或 DESC (降序)。 |
number |
返回术语的数量,默认为返回所有术语。 |
offset |
偏移量,用于分页。 |
fields |
指定返回的字段,例如 all (返回所有字段)、ids (只返回 ID)、names (只返回名称)等。 |
count |
是否返回术语总数,默认为 false 。 |
hierarchical |
是否按层级结构返回术语,默认为 true 。 |
parent |
只返回指定父级术语的子术语。 |
child_of |
返回指定术语的所有子术语。 |
exclude |
排除指定的术语 ID。 |
include |
只返回指定的术语 ID。 |
tax_query |
使用 WP_Tax_Query 类构建更复杂的分类法查询条件。 |
meta_query |
使用 WP_Meta_Query 类构建更复杂的元数据查询条件。 |
lang |
(如果使用了多语言插件) 指定语言。 |
WP_Tax_Query
和 WP_Meta_Query
:查询利器
WP_Term_Query
类还支持使用 WP_Tax_Query
和 WP_Meta_Query
类来构建更复杂的查询条件。这两个类就像是分类术语查询的利器,能让你更加灵活地控制查询过程。
-
WP_Tax_Query
: 用于构建复杂的分类法查询条件,例如查询同时属于多个分类法的术语,或者查询属于某个分类法但不属于另一个分类法的术语。$args = array( 'taxonomy' => array( 'category', 'post_tag' ), 'tax_query' => array( 'relation' => 'AND', // 术语必须同时属于 category 和 post_tag array( 'taxonomy' => 'category', 'field' => 'slug', 'terms' => array( 'news' ), ), array( 'taxonomy' => 'post_tag', 'field' => 'id', 'terms' => array( 1, 2, 3 ), ), ), ); $term_query = new WP_Term_Query( $args );
-
WP_Meta_Query
: 用于构建复杂的元数据查询条件,例如查询具有特定元数据的术语,或者查询元数据值在某个范围内的术语。$args = array( 'taxonomy' => 'category', 'meta_query' => array( 'relation' => 'AND', array( 'key' => 'color', 'value' => 'red', 'compare' => '=', ), array( 'key' => 'size', 'value' => array( 10, 20 ), 'compare' => 'BETWEEN', 'type' => 'NUMERIC', ), ), ); $term_query = new WP_Term_Query( $args );
缓存机制:提升性能的关键
为了提高性能,WP_Term_Query
类使用了缓存机制。WordPress 会将查询结果缓存在内存中,下次查询相同的条件时,直接从缓存中读取结果,而不需要再次执行数据库查询。
WP_Term_Query
类使用 update_term_cache()
函数来更新术语缓存,使用 update_termmeta_cache()
函数来更新术语元数据缓存。
过滤器:灵活扩展的基石
WP_Term_Query
类提供了多个过滤器,允许开发者自定义查询过程。例如,可以使用 get_terms_orderby
过滤器来修改排序方式,使用 get_terms_order
过滤器来修改排序顺序,使用 get_terms_sql
过滤器来修改 SQL 查询语句。
add_filter( 'get_terms_orderby', 'my_custom_terms_orderby', 10, 2 );
function my_custom_terms_orderby( $orderby, $args ) {
if ( $args['taxonomy'] === 'my_taxonomy' ) {
$orderby = 'tt.count'; // 按照文章数量排序
}
return $orderby;
}
总结:WP_Term_Query
的精髓
WP_Term_Query
类是 WordPress 中用于查询分类术语的核心类。它通过解析查询参数、构建 SQL 查询语句、执行查询、处理结果和使用缓存机制,实现了高效灵活的术语查询功能。WP_Tax_Query
和 WP_Meta_Query
类提供了更强大的查询能力,而各种过滤器则允许开发者自定义查询过程。
实战演练:一个自定义术语查询函数
为了更好地理解 WP_Term_Query
类的用法,咱们来编写一个自定义的术语查询函数。
/**
* 查询指定分类法下的热门术语。
*
* @param string $taxonomy 分类法名称。
* @param int $number 返回术语的数量,默认为 5。
* @return WP_Term[]|WP_Error 术语对象数组,如果出错则返回 WP_Error 对象。
*/
function get_popular_terms( $taxonomy, $number = 5 ) {
$args = array(
'taxonomy' => $taxonomy,
'orderby' => 'count', // 按照文章数量排序
'order' => 'DESC', // 降序排列
'number' => $number, // 返回指定数量的术语
'hide_empty' => true, // 隐藏空分类
);
$term_query = new WP_Term_Query( $args );
if ( is_wp_error( $term_query->terms ) ) {
return $term_query->terms;
}
return $term_query->terms;
}
// 使用示例
$popular_categories = get_popular_terms( 'category', 3 );
if ( ! empty( $popular_categories ) ) {
echo '<ul>';
foreach ( $popular_categories as $category ) {
echo '<li><a href="' . get_term_link( $category ) . '">' . $category->name . ' (' . $category->count . ')</a></li>';
}
echo '</ul>';
}
这个函数可以查询指定分类法下的热门术语,并按照文章数量降序排列,返回指定数量的术语。
彩蛋:性能优化小技巧
- 尽量使用缓存,避免重复查询。
- 只查询需要的字段,避免返回不必要的数据。
- 使用
tax_query
和meta_query
类构建复杂的查询条件,避免在 PHP 代码中进行复杂的筛选。 - 合理使用过滤器,自定义查询过程。
- 如果需要查询大量术语,可以考虑使用 SQL 直接查询数据库。
好了,今天的 WP_Term_Query
源码探秘之旅就到这里了。希望大家通过今天的学习,对 WordPress 的术语查询机制有了更深入的了解。记住,源码的世界充满了乐趣,只要你敢于探索,就能发现更多的惊喜!下次再见!