探究 WordPress `get_terms()` 函数的源码:它如何使用 `WP_Term_Query` 类来查询分类术语。

嘿,大家好!今天咱们来聊聊 WordPress 里的一个“老朋友”—— get_terms() 函数。别看它名字简单,背后可藏着不少门道。今天咱们就扒开它的源码,看看它是怎么调兵遣将,最终把我们想要的分类术语给揪出来的。

咱们的目标是:搞清楚 get_terms() 是如何巧妙地利用 WP_Term_Query 类来实现分类术语查询的。

第一幕:get_terms() 函数闪亮登场

首先,我们得找到 get_terms() 函数的真身。它就藏在 wp-includes/taxonomy.php 文件里。

function get_terms( $args = '', $deprecated = '' ) {
    global $wpdb, $_wp_term_hierarchy;

    $defaults = array(
        'taxonomy' => 'category',
        'orderby' => 'name',
        'order' => 'ASC',
        'hide_empty' => true,
        'exclude' => array(),
        'exclude_tree' => array(),
        'include' => array(),
        'number' => '',
        'fields' => 'all',
        'slug' => '',
        'parent' => '',
        'hierarchical' => true,
        'search' => '',
        'name__like' => '',
        'description__like' => '',
        'pad_counts' => false,
        'offset' => '',
        'child_of' => 0,
        'childless' => false,
        'get' => '',
        'name' => '',
        'term_taxonomy_id' => '',
        'update_term_meta_cache' => true,
        'meta_query' => '',
        'suppress_filter' => false,
    );

    if ( ! empty( $deprecated ) ) {
        $args['taxonomy'] = $deprecated;
        _deprecated_argument( __FUNCTION__, '3.0', sprintf(
            /* translators: 1: Argument name, 2: get_terms() */
            __( 'The %1$s argument of %2$s is deprecated.' ),
            '<code>$deprecated</code>',
            '<code>get_terms()</code>'
        ) );
    }

    $args = wp_parse_args( $args, $defaults );

    $taxonomy = $args['taxonomy'];

    if ( ! taxonomy_exists( $taxonomy ) ) {
        return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
    }

    $get_terms_args = array(
        'taxonomy' => $taxonomy,
        'orderby' => $args['orderby'],
        'order' => $args['order'],
        'hide_empty' => $args['hide_empty'],
        'exclude' => $args['exclude'],
        'exclude_tree' => $args['exclude_tree'],
        'include' => $args['include'],
        'number' => $args['number'],
        'fields' => $args['fields'],
        'slug' => $args['slug'],
        'parent' => $args['parent'],
        'hierarchical' => $args['hierarchical'],
        'search' => $args['search'],
        'name__like' => $args['name__like'],
        'description__like' => $args['description__like'],
        'pad_counts' => $args['pad_counts'],
        'offset' => $args['offset'],
        'child_of' => $args['child_of'],
        'childless' => $args['childless'],
        'get' => $args['get'],
        'name' => $args['name'],
        'term_taxonomy_id' => $args['term_taxonomy_id'],
        'update_term_meta_cache' => $args['update_term_meta_cache'],
        'meta_query' => $args['meta_query'],
        'suppress_filter' => $args['suppress_filter'],
    );

    $term_query = new WP_Term_Query( $get_terms_args );

    return apply_filters( 'get_terms', $term_query->get_terms(), $taxonomy, $get_terms_args );
}

简单来说,get_terms() 函数的作用就是:

  1. 接收参数: 接收一个 $args 数组,里面包含了各种查询条件,比如要查询哪个分类法 (taxonomy),排序方式,是否隐藏空分类等等。
  2. 参数标准化:使用 wp_parse_args() 函数将传入的参数与默认参数合并,确保所有需要的参数都有值。
  3. 实例化 WP_Term_Query: 创建一个 WP_Term_Query 类的实例,并将处理后的参数传递给它。
  4. 调用 get_terms() 方法: 通过 $term_query->get_terms() 获取查询结果。
  5. 应用过滤器:使用 apply_filters() 应用 get_terms 过滤器,允许其他插件或主题修改查询结果。
  6. 返回结果:将最终的查询结果返回。

关键的一步是:$term_query = new WP_Term_Query( $get_terms_args ); 它创建了一个 WP_Term_Query 类的实例。 这就像是给 get_terms() 函数配了一个专业的“搜索犬”,专门负责分类术语的搜索工作。

第二幕:WP_Term_Query 类登场

WP_Term_Query 类位于 wp-includes/class-wp-term-query.php 文件中。 这个类是整个查询的核心,它负责构建 SQL 查询语句,并从数据库中获取分类术语。

我们先来看看它的构造函数:

    public function __construct( $query = '' ) {
        if ( ! empty( $query ) ) {
            $this->query( $query );
        }
    }

构造函数很简单,如果传入了查询参数 $query,就调用 query() 方法来处理这些参数。

接下来,我们重点关注 query() 方法和 get_terms() 方法。

    public function query( $query ) {
        global $wpdb;

        $this->query_vars = wp_parse_args( $query, $this->defaults );
        $this->query_vars = sanitize_term_field( 'query_vars', $this->query_vars, 'db', 'query' );

        /**
         * Fires before taxonomy terms are retrieved.
         *
         * @since 4.6.0
         *
         * @param WP_Term_Query $this The WP_Term_Query instance (passed by reference).
         */
        do_action_ref_array( 'pre_get_terms', array( &$this ) );

        $this->sql = $this->get_sql( $this->query_vars );

        if ( is_null( $this->sql ) ) {
            return;
        }

        $this->terms = $wpdb->get_results( $this->sql );

        if ( 'ids' === $this->query_vars['fields'] ) {
            $this->terms = array_map( 'intval', $this->terms );
        }

        if ( $this->query_vars['update_term_meta_cache'] ) {
            update_term_meta_cache( wp_list_pluck( $this->terms, 'term_id' ) );
        }

        /**
         * Fires after taxonomy terms are retrieved.
         *
         * @since 4.6.0
         *
         * @param WP_Term_Query $this The WP_Term_Query instance (passed by reference).
         */
        do_action_ref_array( 'get_terms', array( &$this ) );
    }

query() 方法的流程如下:

  1. 参数合并与清理: 使用 wp_parse_args() 将传入的查询参数与默认参数合并,并使用 sanitize_term_field() 对参数进行清理,防止 SQL 注入。
  2. pre_get_terms 动作: 触发 pre_get_terms 动作,允许其他插件或主题在查询之前进行一些操作。
  3. 构建 SQL 查询语句: 调用 get_sql() 方法,根据查询参数构建 SQL 查询语句。
  4. 执行查询: 使用 $wpdb->get_results() 执行 SQL 查询,获取查询结果。
  5. 处理结果: 根据 fields 参数,对查询结果进行处理,比如只返回 ID。
  6. 更新术语元数据缓存: 如果 update_term_meta_cache 参数为 true,则更新术语元数据缓存,提高后续查询效率。
  7. get_terms 动作: 触发 get_terms 动作,允许其他插件或主题在查询之后进行一些操作。

接下来,我们重点看看 get_sql() 方法是如何构建 SQL 查询语句的:

    protected function get_sql( $query_vars ) {
        global $wpdb;

        $defaults = $this->defaults;

        $this->query_vars = wp_parse_args( $query_vars, $defaults );

        // Cast integers.
        $number         = isset( $this->query_vars['number'] ) ? absint( $this->query_vars['number'] ) : 0;
        $offset         = isset( $this->query_vars['offset'] ) ? absint( $this->query_vars['offset'] ) : 0;
        $child_of       = isset( $this->query_vars['child_of'] ) ? absint( $this->query_vars['child_of'] ) : 0;
        $parent         = isset( $this->query_vars['parent'] ) ? absint( $this->query_vars['parent'] ) : '';
        $hierarchical   = isset( $this->query_vars['hierarchical'] ) ? (bool) $this->query_vars['hierarchical'] : true;
        $pad_counts     = isset( $this->query_vars['pad_counts'] ) ? (bool) $this->query_vars['pad_counts'] : false;
        $hide_empty     = isset( $this->query_vars['hide_empty'] ) ? (bool) $this->query_vars['hide_empty'] : true;
        $childless      = isset( $this->query_vars['childless'] ) ? (bool) $this->query_vars['childless'] : false;
        $name__like     = isset( $this->query_vars['name__like'] ) ? trim( $this->query_vars['name__like'] ) : '';
        $description__like = isset( $this->query_vars['description__like'] ) ? trim( $this->query_vars['description__like'] ) : '';
        $search         = isset( $this->query_vars['search'] ) ? trim( $this->query_vars['search'] ) : '';
        $name           = isset( $this->query_vars['name'] ) ? $this->query_vars['name'] : '';
        $slug           = isset( $this->query_vars['slug'] ) ? $this->query_vars['slug'] : '';
        $term_taxonomy_id = isset( $this->query_vars['term_taxonomy_id'] ) ? $this->query_vars['term_taxonomy_id'] : '';
        $get            = isset( $this->query_vars['get'] ) ? $this->query_vars['get'] : '';

        $taxonomies = (array) $this->query_vars['taxonomy'];

        $selects = array();
        $from = "FROM {$wpdb->terms} AS t INNER JOIN {$wpdb->term_taxonomy} AS tt ON t.term_id = tt.term_id";
        $where = "WHERE tt.taxonomy IN ('" . implode( "', '", array_map( 'esc_sql', $taxonomies ) ) . "')";
        $orderby = '';
        $limits = '';
        $order = strtoupper( $this->query_vars['order'] );

        if ( ! in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
            $order = 'ASC';
        }

        // Fields selects.
        switch ( $this->query_vars['fields'] ) {
            case 'count':
                $selects = array( 'COUNT(*)' );
                break;
            case 'ids':
                $selects = array( 't.term_id' );
                break;
            case 'names':
                $selects = array( 't.name' );
                break;
            case 'all':
            case 'all_with_object_id':
            default:
                $selects = array( 't.*', 'tt.*' );
                break;
        }

        /**
         * Filters the SELECT clause of the terms query.
         *
         * @since 4.6.0
         *
         * @param array        $selects     An array of the SELECT clause columns.
         * @param WP_Term_Query $this The WP_Term_Query instance (passed by reference).
         */
        $selects = apply_filters_ref_array( 'get_terms_fields', array( $selects, &$this ) );

        $fields = implode( ', ', $selects );

        // Exclude term IDs.
        if ( ! empty( $this->query_vars['exclude'] ) ) {
            $exclude = wp_parse_id_list( $this->query_vars['exclude'] );
            $where .= ' AND t.term_id NOT IN (' . implode( ', ', array_map( 'intval', $exclude ) ) . ')';
        }

        // Exclude term IDs from an entire subtree.
        if ( ! empty( $this->query_vars['exclude_tree'] ) ) {
            $exclude_tree = wp_parse_id_list( $this->query_vars['exclude_tree'] );
            $where .= ' AND t.term_id NOT IN (' . implode( ', ', array_map( 'intval', $exclude_tree ) ) . ')';
        }

        // Include term IDs.
        if ( ! empty( $this->query_vars['include'] ) ) {
            $include = wp_parse_id_list( $this->query_vars['include'] );
            $where .= ' AND t.term_id IN (' . implode( ', ', array_map( 'intval', $include ) ) . ')';
        }

        // Checking 'number' string to catch falsey values.
        if ( ! empty( $number ) ) {
            if ( $offset ) {
                $limits = 'LIMIT ' . $offset . ', ' . $number;
            } else {
                $limits = 'LIMIT ' . $number;
            }
        }

        // Term name like.
        if ( ! empty( $name__like ) ) {
            $where .= $wpdb->prepare( ' AND t.name LIKE %s', '%' . $wpdb->esc_like( $name__like ) . '%' );
        }

        // Term description like.
        if ( ! empty( $description__like ) ) {
            $where .= $wpdb->prepare( ' AND tt.description LIKE %s', '%' . $wpdb->esc_like( $description__like ) . '%' );
        }

        // Term slug.
        if ( ! empty( $slug ) ) {
            $slug = sanitize_title_for_query( $slug );
            $where .= $wpdb->prepare( ' AND t.slug = %s', $slug );
        }

        // Term name.
        if ( ! empty( $name ) ) {
            if ( is_array( $name ) ) {
                $name = array_map( array( $this, 'sanitize_term_name_for_query' ), $name );
                $where .= " AND t.name IN ('" . implode( "', '", array_map( 'esc_sql', $name ) ) . "')";
            } else {
                $name = $this->sanitize_term_name_for_query( $name );
                $where .= $wpdb->prepare( ' AND t.name = %s', $name );
            }
        }

        // Term taxonomy ID.
        if ( ! empty( $term_taxonomy_id ) ) {
            if ( is_array( $term_taxonomy_id ) ) {
                $term_taxonomy_id = wp_parse_id_list( $term_taxonomy_id );
                $where .= ' AND tt.term_taxonomy_id IN (' . implode( ', ', array_map( 'intval', $term_taxonomy_id ) ) . ')';
            } else {
                $where .= $wpdb->prepare( ' AND tt.term_taxonomy_id = %d', $term_taxonomy_id );
            }
        }

        // Hide empty terms.
        if ( $hide_empty && ! $hierarchical ) {
            $join = "INNER JOIN {$wpdb->term_relationships} AS tr ON t.term_id = tr.term_taxonomy_id";
            $from .= ' ' . $join;
            $where .= ' AND tt.count > 0';
        }

        // Match term names.
        if ( ! empty( $search ) ) {
            $like = '%' . $wpdb->esc_like( $search ) . '%';
            $where .= $wpdb->prepare( ' AND (t.name LIKE %s)', $like );
        }

        if ( is_numeric( $parent ) ) {
            $where .= $wpdb->prepare( ' AND tt.parent = %d', $parent );
        }

        if ( is_numeric( $child_of ) ) {
            $where .= $wpdb->prepare( ' AND tt.parent = %d', $child_of );
        }

        if ( $childless ) {
            $where .= ' AND tt.parent = 0';
        }

        // 'get' filter.
        if ( 'all' == $get ) {
            $where .= ' AND tt.count > 0';
        }

        // Meta query clauses.
        $meta_query = new WP_Meta_Query( $this->query_vars['meta_query'] );
        $clauses = $meta_query->get_sql( 'term', 't', 'term_id' );

        if ( $clauses ) {
            $from .= $clauses['join'];
            $where .= " AND {$clauses['where']}";
        }

        $orderby_inner = '';

        switch ( $this->query_vars['orderby'] ) {
            case 'none':
                $orderby = '';
                break;
            case 'term_id':
                $orderby = 'ORDER BY t.term_id ' . $order;
                break;
            case 'name':
                $orderby = 'ORDER BY t.name ' . $order;
                break;
            case 'slug':
                $orderby = 'ORDER BY t.slug ' . $order;
                break;
            case 'term_group':
                $orderby = 'ORDER BY t.term_group ' . $order;
                break;
            case 'count':
                $orderby = 'ORDER BY tt.count ' . $order;
                break;
            case 'parent':
                $orderby = 'ORDER BY tt.parent ' . $order;
                break;
            case 'id':
                $orderby = 'ORDER BY t.term_id ' . $order;
                break;
            case 'description':
                $orderby = 'ORDER BY tt.description ' . $order;
                break;
            case 'include':
                $include = wp_parse_id_list( $this->query_vars['include'] );
                $orderby = 'ORDER BY FIELD(t.term_id, ' . implode( ',', array_map( 'intval', $include ) ) . ')';
                break;
            case 'meta_value':
                if ( ! empty( $clauses['orderby'] ) ) {
                    $orderby = 'ORDER BY ' . $clauses['orderby'];
                }
                break;
            case 'meta_value_num':
                if ( ! empty( $clauses['orderby'] ) ) {
                    $orderby = 'ORDER BY ' . $clauses['orderby'];
                }
                break;
            default:

                /**
                 * Filters the taxonomy term query's orderby clause.
                 *
                 * @since 2.8.0
                 *
                 * @param string       $orderby       The ORDERBY clause of the query.
                 * @param array        $query_vars    An array of arguments for the query.
                 * @param string       $taxonomy      The taxonomy of the terms being queried.
                 */
                $orderby = apply_filters( 'get_terms_orderby', 'ORDER BY t.name ' . $order, $this->query_vars, $taxonomies[0] );
        }

        $orderby = trim( $orderby );

        if ( ! empty( $orderby_inner ) ) {
            $orderby = $orderby_inner . ( ! empty( $orderby ) ? ', ' . $orderby : '' );
        }

        $found_rows = '';
        if ( ! empty( $number ) && $this->query_vars['hierarchical'] && $pad_counts ) {
            $found_rows = 'SQL_CALC_FOUND_ROWS';
        }

        $this->sql_clauses['select']  = "SELECT {$found_rows} $fields";
        $this->sql_clauses['from']    = $from;
        $this->sql_clauses['where']   = $where;
        $this->sql_clauses['orderby'] = $orderby;
        $this->sql_clauses['limits']  = $limits;

        $this->sql = "{$this->sql_clauses['select']} {$this->sql_clauses['from']} {$this->sql_clauses['where']} {$this->sql_clauses['orderby']} {$this->sql_clauses['limits']}";

        /**
         * Filters the terms query SQL statement.
         *
         * @since 4.6.0
         *
         * @param string       $sql       The complete SQL query.
         * @param array        $query_vars    An array of arguments for the query.
         * @param WP_Term_Query $this The WP_Term_Query instance (passed by reference).
         */
        $this->sql = apply_filters_ref_array( 'terms_request', array( $this->sql, $this->query_vars, &$this ) );

        if ( ! empty( $number ) && $this->query_vars['hierarchical'] && $pad_counts ) {
            $this->sql_clauses['found_rows'] = "SELECT FOUND_ROWS()";
        }

        return $this->sql;
    }

get_sql() 方法就像一个经验丰富的建筑师,它根据各种查询参数,一块一块地拼接 SQL 查询语句。

  1. 参数准备: 将查询参数进行类型转换和清理。
  2. SQL 骨架: 构建 SQL 查询语句的基本框架,包括 SELECTFROMWHERE 子句。
  3. 处理各种参数: 根据传入的参数,动态地修改 WHERE 子句,比如排除某些分类 ID,包含某些分类 ID,根据分类名称搜索等等。
  4. 处理元数据查询: 如果传入了 meta_query 参数,则使用 WP_Meta_Query 类来构建元数据查询的 SQL 子句。
  5. 排序: 根据 orderbyorder 参数,构建 ORDER BY 子句。
  6. 分页: 根据 numberoffset 参数,构建 LIMIT 子句。
  7. 应用过滤器: 使用 apply_filters_ref_array() 应用 terms_request 过滤器,允许其他插件或主题修改最终的 SQL 查询语句。
  8. 返回 SQL 语句: 将最终构建好的 SQL 查询语句返回。

get_terms() 方法,它负责返回查询到的术语:

    public function get_terms() {
        if ( ! isset( $this->terms ) ) {
            $this->query();
        }

        return $this->terms;
    }

这个方法很简单,它首先检查 $this->terms 属性是否已经设置,如果还没有设置,就调用 query() 方法执行查询,然后将查询结果返回。

第三幕:WP_Meta_Query 类助阵

WP_Term_Query 类中,如果传入了 meta_query 参数,会使用 WP_Meta_Query 类来构建元数据查询的 SQL 子句。 WP_Meta_Query 类位于 wp-includes/class-wp-meta-query.php 文件中。

WP_Meta_Query 类的作用是:将复杂的元数据查询条件转换成 SQL 查询语句,以便在数据库中进行搜索。 这就像是给 WP_Term_Query 配备了一个专业的“元数据翻译官”,专门负责将元数据查询条件翻译成数据库能够理解的语言。

由于篇幅限制,我们这里就不深入讲解 WP_Meta_Query 类的源码了,但是你需要知道,它是 WordPress 中处理元数据查询的核心类之一。

总结

现在,我们来总结一下 get_terms() 函数是如何使用 WP_Term_Query 类来查询分类术语的:

  1. get_terms() 函数接收查询参数,并将这些参数传递给 WP_Term_Query 类。
  2. WP_Term_Query 类根据查询参数,构建 SQL 查询语句。如果查询涉及到元数据,WP_Meta_Query 类会协助构建元数据查询的 SQL 子句。
  3. WP_Term_Query 类执行 SQL 查询,并将查询结果返回给 get_terms() 函数。
  4. get_terms() 函数对查询结果进行处理,并应用过滤器,然后将最终的结果返回。

用表格来总结一下:

函数/类 职责
get_terms() 接收查询参数,创建 WP_Term_Query 实例,调用 WP_Term_Query->get_terms(),应用过滤器,返回结果。
WP_Term_Query 构建 SQL 查询语句,执行查询,处理查询结果。
WP_Meta_Query 将元数据查询条件转换成 SQL 查询语句。

举个例子

假设我们要查询 category 分类法下,slugnews 的分类术语。我们可以这样调用 get_terms() 函数:

$args = array(
    'taxonomy' => 'category',
    'slug' => 'news',
);

$terms = get_terms( $args );

if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
    foreach ( $terms as $term ) {
        echo 'Term ID: ' . $term->term_id . '<br>';
        echo 'Term Name: ' . $term->name . '<br>';
        echo 'Term Slug: ' . $term->slug . '<br>';
    }
}

在这个例子中,get_terms() 函数会创建一个 WP_Term_Query 实例,并将 taxonomyslug 参数传递给它。WP_Term_Query 类会构建如下 SQL 查询语句:

SELECT t.*, tt.* FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id WHERE tt.taxonomy IN ('category') AND t.slug = 'news'

然后,WP_Term_Query 类会执行这个 SQL 查询语句,并将查询结果返回给 get_terms() 函数。

最后的话

希望通过今天的讲解,你对 get_terms() 函数和 WP_Term_Query 类有了更深入的了解。 掌握了这些知识,你就可以更灵活地使用 WordPress 的分类术语查询功能,并根据自己的需求进行定制。

记住,源码是最好的老师。多看源码,多思考,你就能成为真正的 WordPress 大神!

发表回复

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