解释 `wp_term_query` 类的源码,它是如何查询分类术语的?

大家好,欢迎来到今天的 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 类的灵魂,咱们来一步步地分析它的实现过程:

  1. 参数解析与准备:

    • 首先,它会接收构造函数传入的查询参数,并进行一些预处理,例如设置默认值、处理分类法参数等。
    • 它会检查 meta_keymeta_value 参数,如果存在,则将其转换为 meta_query 数组,以便后续处理。
    • 对传入的taxonomy参数进行处理,确保其为数组,并且是已注册的分类法。
  2. 构建 SQL 查询语句:

    • 这是最核心的部分,get_terms() 方法会根据查询参数构建 SQL 查询语句。
    • 它会根据 orderbyorder 参数设置排序方式和顺序。
    • 它会根据各种条件参数(例如 object_idssearchslugnameparentchild_ofexcludeinclude 等)构建 WHERE 子句,用于筛选术语。
    • 它会根据 tax_querymeta_query 参数构建更复杂的筛选条件。
    • 它会根据 numberoffset 参数设置 LIMIT 子句,用于限制返回结果的数量。
    • 它会根据 fields 参数设置 SELECT 子句,用于指定要返回的字段。
  3. 执行 SQL 查询:

    • 构建好 SQL 查询语句后,get_terms() 方法会使用 $wpdb 对象执行查询。
    • 它会根据 fields 参数选择不同的查询方法(例如 get_col() 用于查询 ID,get_results() 用于查询完整对象)。
  4. 处理查询结果:

    • 查询完成后,get_terms() 方法会对查询结果进行一些处理,例如更新术语缓存、更新术语元数据缓存等。
    • 如果设置了 hierarchicalpad_counts 参数,则会调用 _pad_term_counts() 函数来更新术语计数。
    • 最后,它会将查询结果存储在 $this->terms 属性中,并返回结果。

重要参数详解

参数名 说明
taxonomy 指定分类法,例如 categorypost_tag 等。
object_ids 指定关联的文章 ID,只返回与这些文章关联的术语。
search 搜索术语名称。
slug 根据术语别名查询。
term_id 根据术语 ID 查询。
name 根据术语名称查询。
hide_empty 是否隐藏空分类,默认为 true
orderby 排序方式,例如 namecountterm_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_QueryWP_Meta_Query:查询利器

WP_Term_Query 类还支持使用 WP_Tax_QueryWP_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_QueryWP_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_querymeta_query 类构建复杂的查询条件,避免在 PHP 代码中进行复杂的筛选。
  • 合理使用过滤器,自定义查询过程。
  • 如果需要查询大量术语,可以考虑使用 SQL 直接查询数据库。

好了,今天的 WP_Term_Query 源码探秘之旅就到这里了。希望大家通过今天的学习,对 WordPress 的术语查询机制有了更深入的了解。记住,源码的世界充满了乐趣,只要你敢于探索,就能发现更多的惊喜!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注