阐述 `wp_set_post_terms()` 函数的源码,它是如何为文章设置分类术语并处理 `taxonomy` 计数更新的?

各位观众,欢迎来到今天的 "WordPress源码刨析" 讲座!今天我们要聊的是一个在WordPress开发中经常用到的函数:wp_set_post_terms()。 别看名字平平无奇,它可是控制文章分类的关键人物,相当于文章的"户口登记员",负责把文章归到不同的"社区" (也就是分类术语)。

咱们今天就来扒一扒它的源码,看看这个"户口登记员"是怎么工作的,以及它背后的那些数据更新操作。

开场白: 认识一下我们的主角:wp_set_post_terms()

wp_set_post_terms() 函数的主要作用是:为指定的文章(post)设置指定分类法(taxonomy)下的分类术语(terms)。 简单来说,就是告诉WordPress,这篇文章属于哪个或哪些分类。

/**
 * Sets the terms for a post.
 *
 * Relates the post to the term(s) and taxonomy. Works much like
 * wp_set_object_terms() except it applies to post objects.
 *
 * @since 2.3.0
 *
 * @param int          $post_id   Post ID.
 * @param int|string|array $terms     Single term ID, single term slug, or array of term IDs or slugs.
 * @param string       $taxonomy  Taxonomy name.
 * @param bool         $append    Optional. Whether to append new terms to the old terms. Default false.
 * @return array|WP_Error Array of term taxonomy IDs on success, WP_Error on failure.
 */
function wp_set_post_terms( $post_id = 0, $terms = array(), $taxonomy = 'category', $append = false ) {
    return wp_set_object_terms( $post_id, $terms, $taxonomy, $append );
}

看到了吧? wp_set_post_terms() 本身只是一个wrapper函数, 它实际上调用了 wp_set_object_terms() 函数来完成任务。 这样做的好处是, wp_set_object_terms() 可以更通用地处理不同类型的对象(例如文章、用户等)与分类术语之间的关系。

第一幕: wp_set_object_terms() 源码解析

好了,既然主角是 wp_set_object_terms(),我们就直接跳到它身上。 准备好,源码之旅开始了!

/**
 * Sets the terms for an object.
 *
 * Relates the object to the term(s) and taxonomy. Creates term relationships if they don't already exist.
 * Handles taxonomy counts.
 *
 * @since 2.3.0
 * @since 4.3.0 Added 'id=>taxonomy' support for `$terms` parameter.
 * @since 4.5.0 Added `$args` parameter.
 *
 * @param int          $object_id Object ID.
 * @param int|string|array $terms     Single term ID, single term slug, array of term IDs, array of term slugs,
 *                                  or an associative array of term IDs to taxonomies.
 * @param string|array $taxonomy  Taxonomy name or array of taxonomy names.
 * @param bool         $append    Optional. Whether to append new terms to the old terms. Default false.
 * @param array        $args      {
 *     Optional. Array of arguments for setting object terms.
 *
 *     @type bool $use_cache Whether to update the cached term counts. Default true.
 * }
 * @return array|WP_Error Array of term taxonomy IDs on success, WP_Error on failure.
 */
function wp_set_object_terms( $object_id = 0, $terms = array(), $taxonomy = 'category', $append = false, $args = array() ) {
    global $wpdb;

    $object_id = (int) $object_id;

    if ( empty( $object_id ) ) {
        return new WP_Error( 'invalid_object_id', __( 'Invalid object ID.' ) );
    }

    $defaults = array(
        'use_cache' => true,
    );

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

    if ( ! is_array( $taxonomy ) ) {
        $taxonomy = array( $taxonomy );
    }

    $taxonomies = array();
    foreach ( $taxonomy as $tax ) {
        $tax_obj = get_taxonomy( $tax );
        if ( ! $tax_obj ) {
            return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
        }
        $taxonomies[] = $tax_obj->name;
    }

    $term_ids = array();

    if ( ! is_array( $terms ) ) {
        $terms = array( $terms );
    }

    if ( empty( $terms ) ) {
        $cleared = _wp_remove_object_terms( $object_id, $taxonomies );

        if ( is_wp_error( $cleared ) ) {
            return $cleared;
        }

        wp_cache_delete( $object_id, 'terms' );
        return array();
    }

    $id_taxonomy = false;
    foreach ( (array) $terms as $key => $value ) {
        if ( is_string( $key ) && is_string( $value ) ) {
            $id_taxonomy = true;
            break;
        }
    }

    if ( $id_taxonomy ) {
        $new_taxonomies = array();
        foreach ( (array) $terms as $term_id => $taxonomy ) {
            $term_id = (int) $term_id;
            if ( ! $term_id ) {
                continue;
            }

            if ( ! taxonomy_exists( $taxonomy ) ) {
                continue;
            }

            if ( ! in_array( $taxonomy, $new_taxonomies, true ) ) {
                $new_taxonomies[] = $taxonomy;
            }

            if ( isset( $term_ids[ $taxonomy ] ) ) {
                $term_ids[ $taxonomy ][] = $term_id;
            } else {
                $term_ids[ $taxonomy ] = array( $term_id );
            }
        }

        $taxonomies = $new_taxonomies;
        unset( $new_taxonomies );
    } else {
        foreach ( $taxonomies as $taxonomy ) {
            $term_ids[ $taxonomy ] = array();
        }

        foreach ( (array) $terms as $term ) {
            $term_info = term_exists( $term, $taxonomies[0] ); // Check if the term exists.
            if ( is_array( $term_info ) ) {
                $term_ids[ $taxonomies[0] ][] = (int) $term_info['term_id'];
            } else {
                // Skip if no matching term ID was found.
                continue;
            }
        }
    }

    // Get the existing terms.
    $old_term_ids = array();
    foreach ( $taxonomies as $taxonomy ) {
        $old_term_ids[ $taxonomy ] = wp_get_object_terms( $object_id, $taxonomy, array( 'fields' => 'ids', 'orderby' => 'none' ) );
    }

    // Only delete existing terms if we're not appending.
    if ( ! $append ) {
        $cleared = _wp_remove_object_terms( $object_id, $taxonomies );

        if ( is_wp_error( $cleared ) ) {
            return $cleared;
        }
    }

    $tt_ids = array();
    foreach ( $taxonomies as $taxonomy ) {
        if ( isset( $term_ids[ $taxonomy ] ) ) {
            $term_ids[ $taxonomy ] = array_map( 'intval', $term_ids[ $taxonomy ] );
            $term_ids[ $taxonomy ] = array_unique( $term_ids[ $taxonomy ] );

            // Remove already-assigned terms.
            $new_ids = array_diff( $term_ids[ $taxonomy ], $old_term_ids[ $taxonomy ] );

            if ( empty( $new_ids ) ) {
                continue;
            }

            $tt_ids = array_merge( $tt_ids, wp_add_object_terms( $object_id, $new_ids, $taxonomy ) );
        }
    }

    if ( $args['use_cache'] ) {
        wp_cache_delete( $object_id, 'terms' );
    }

    wp_update_term_count( $tt_ids, $taxonomies );

    /**
     * Fires after the terms are set for an object.
     *
     * @since 2.9.0
     *
     * @param int   $object_id Object ID.
     * @param array $terms     An array of object term IDs.
     * @param array $tt_ids    Array of term taxonomy IDs.
     * @param array $taxonomy  Array of taxonomy names.
     * @param bool  $append    Whether to append the terms.
     * @param array $old_term_ids Array of pre-existing term IDs.
     */
    do_action( 'set_object_terms', $object_id, $terms, $tt_ids, $taxonomy, $append, $old_term_ids );

    return $tt_ids;
}

第二幕: 源码解读,庖丁解牛

看起来代码很长,但别慌,我们一步一步来。

  1. 参数验证和准备

    • 首先,函数会把 $object_id 转换为整数,并检查是否为空。 如果为空,直接返回一个 WP_Error 对象,提示 ID 无效。
    • 然后,它会设置一个默认参数 $args,其中 'use_cache' 默认为 true,表示要更新缓存。
    • 接下来,它会确保 $taxonomy 是一个数组,方便后续处理。
    $object_id = (int) $object_id;
    
    if ( empty( $object_id ) ) {
        return new WP_Error( 'invalid_object_id', __( 'Invalid object ID.' ) );
    }
    
    $defaults = array(
        'use_cache' => true,
    );
    
    $args = wp_parse_args( $args, $defaults );
    
    if ( ! is_array( $taxonomy ) ) {
        $taxonomy = array( $taxonomy );
    }
  2. 验证 Taxonomy 是否存在

    • 遍历 $taxonomy 数组,使用 get_taxonomy() 函数检查每个 taxonomy 是否存在。 如果有不存在的,也会返回一个 WP_Error 对象。
    • 把验证通过的 taxonomy name 保存在 $taxonomies 数组中。
    $taxonomies = array();
    foreach ( $taxonomy as $tax ) {
        $tax_obj = get_taxonomy( $tax );
        if ( ! $tax_obj ) {
            return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
        }
        $taxonomies[] = $tax_obj->name;
    }
  3. 处理 $terms 参数

    • $terms 参数可以有多种形式:单个 term ID、单个 term slug、term ID 数组、term slug 数组,甚至是一个关联数组,其中 key 是 term ID,value 是 taxonomy。 函数会根据 $terms 的类型进行不同的处理。
    • 如果 $terms 为空,表示要移除该对象的所有分类术语。 此时,会调用 _wp_remove_object_terms() 函数来删除关联关系,并清除缓存。
    $term_ids = array();
    
    if ( ! is_array( $terms ) ) {
        $terms = array( $terms );
    }
    
    if ( empty( $terms ) ) {
        $cleared = _wp_remove_object_terms( $object_id, $taxonomies );
    
        if ( is_wp_error( $cleared ) ) {
            return $cleared;
        }
    
        wp_cache_delete( $object_id, 'terms' );
        return array();
    }
    
    $id_taxonomy = false;
    foreach ( (array) $terms as $key => $value ) {
        if ( is_string( $key ) && is_string( $value ) ) {
            $id_taxonomy = true;
            break;
        }
    }
    
    if ( $id_taxonomy ) {
        // 处理 `$terms` 是一个关联数组,其中 key 是 term ID,value 是 taxonomy 的情况
        $new_taxonomies = array();
        foreach ( (array) $terms as $term_id => $taxonomy ) {
            $term_id = (int) $term_id;
            if ( ! $term_id ) {
                continue;
            }
    
            if ( ! taxonomy_exists( $taxonomy ) ) {
                continue;
            }
    
            if ( ! in_array( $taxonomy, $new_taxonomies, true ) ) {
                $new_taxonomies[] = $taxonomy;
            }
    
            if ( isset( $term_ids[ $taxonomy ] ) ) {
                $term_ids[ $taxonomy ][] = $term_id;
            } else {
                $term_ids[ $taxonomy ] = array( $term_id );
            }
        }
    
        $taxonomies = $new_taxonomies;
        unset( $new_taxonomies );
    } else {
        // 处理 `$terms` 是 term ID 或 term slug 数组的情况
        foreach ( $taxonomies as $taxonomy ) {
            $term_ids[ $taxonomy ] = array();
        }
    
        foreach ( (array) $terms as $term ) {
            $term_info = term_exists( $term, $taxonomies[0] ); // Check if the term exists.
            if ( is_array( $term_info ) ) {
                $term_ids[ $taxonomies[0] ][] = (int) $term_info['term_id'];
            } else {
                // Skip if no matching term ID was found.
                continue;
            }
        }
    }
  4. 获取已存在的分类术语

    • 使用 wp_get_object_terms() 函数获取该对象已存在的分类术语 ID。
    $old_term_ids = array();
    foreach ( $taxonomies as $taxonomy ) {
        $old_term_ids[ $taxonomy ] = wp_get_object_terms( $object_id, $taxonomy, array( 'fields' => 'ids', 'orderby' => 'none' ) );
    }
  5. 删除旧的分类术语(如果不是追加模式)

    • 如果 $append 参数为 false (默认值),表示要替换现有的分类术语。 此时,会调用 _wp_remove_object_terms() 函数删除旧的关联关系。
    if ( ! $append ) {
        $cleared = _wp_remove_object_terms( $object_id, $taxonomies );
    
        if ( is_wp_error( $cleared ) ) {
            return $cleared;
        }
    }
  6. 添加新的分类术语

    • 遍历 $taxonomies 数组,对于每个 taxonomy,将 $term_ids 数组中的 term ID 转换为整数,并去除重复的 ID。
    • 使用 array_diff() 函数找出需要添加的新 term ID (即不在 $old_term_ids 中的 ID)。
    • 调用 wp_add_object_terms() 函数添加新的关联关系。
    $tt_ids = array();
    foreach ( $taxonomies as $taxonomy ) {
        if ( isset( $term_ids[ $taxonomy ] ) ) {
            $term_ids[ $taxonomy ] = array_map( 'intval', $term_ids[ $taxonomy ] );
            $term_ids[ $taxonomy ] = array_unique( $term_ids[ $taxonomy ] );
    
            // Remove already-assigned terms.
            $new_ids = array_diff( $term_ids[ $taxonomy ], $old_term_ids[ $taxonomy ] );
    
            if ( empty( $new_ids ) ) {
                continue;
            }
    
            $tt_ids = array_merge( $tt_ids, wp_add_object_terms( $object_id, $new_ids, $taxonomy ) );
        }
    }
  7. 更新缓存

    • 如果 $args['use_cache']true,则清除缓存。
    if ( $args['use_cache'] ) {
        wp_cache_delete( $object_id, 'terms' );
    }
  8. 更新分类术语计数

    • 调用 wp_update_term_count() 函数更新分类术语的计数。 这是非常重要的一步,因为它确保了分类术语的计数与实际关联的文章数量保持一致。
    wp_update_term_count( $tt_ids, $taxonomies );
  9. 触发 Action Hook

    • 触发 set_object_terms action hook,允许其他插件或主题执行自定义操作。
    do_action( 'set_object_terms', $object_id, $terms, $tt_ids, $taxonomy, $append, $old_term_ids );
  10. 返回结果

    • 函数返回一个数组,包含所有关联的 term taxonomy ID (tt_id)。
    return $tt_ids;

第三幕: 幕后英雄: wp_add_object_terms()wp_update_term_count()

wp_set_object_terms() 函数调用了两个重要的助手函数:wp_add_object_terms()wp_update_term_count()。 让我们简单了解一下它们。

  • wp_add_object_terms():添加对象与分类术语的关联关系

    这个函数负责在 wp_term_relationships 表中插入数据,建立对象与分类术语之间的关联。

    /**
     * Adds term(s) to an object.
     *
     * Creates term relationships if they don't already exist.
     *
     * @since 2.3.0
     *
     * @param int          $object_id Object ID.
     * @param int|string|array $terms     Single term ID, single term slug, or array of term IDs or slugs.
     * @param string       $taxonomy  Taxonomy name.
     * @return array Array of term taxonomy IDs.
     */
    function wp_add_object_terms( $object_id, $terms, $taxonomy ) {
        global $wpdb;
    
        $object_id = (int) $object_id;
    
        $term_ids = array();
        $taxonomy_obj = get_taxonomy( $taxonomy );
    
        if ( ! $taxonomy_obj ) {
            return array();
        }
    
        if ( ! is_array( $terms ) ) {
            $terms = array( $terms );
        }
    
        foreach ( $terms as $term ) {
            $term_info = term_exists( $term, $taxonomy );
    
            if ( is_array( $term_info ) ) {
                $term_id = $term_info['term_id'];
            } else {
                $term_id = term_exists( $term, $taxonomy );
                if ( ! is_int( $term_id ) ) {
                    continue;
                }
            }
    
            $term_ids[] = (int) $term_id;
        }
    
        $term_ids = array_map( 'intval', $term_ids );
        $term_ids = array_unique( $term_ids );
    
        $tt_ids = array();
    
        if ( ! empty( $term_ids ) ) {
            $tt_ids = _wp_add_object_terms( $object_id, $term_ids, $taxonomy );
        }
    
        return $tt_ids;
    }
    
    /**
     * Adds term(s) to an object.
     *
     * @access private
     * @since 4.3.0
     *
     * @global wpdb $wpdb WordPress database abstraction object.
     *
     * @param int   $object_id Object ID.
     * @param array $term_ids  Array of term IDs.
     * @param array $taxonomy  Taxonomy name.
     * @return array Array of term taxonomy IDs.
     */
    function _wp_add_object_terms( $object_id, $term_ids, $taxonomy ) {
        global $wpdb;
    
        $object_id = (int) $object_id;
    
        $taxonomy_obj = get_taxonomy( $taxonomy );
    
        if ( ! $taxonomy_obj ) {
            return array();
        }
    
        $tt_ids = array();
    
        $term_ids = array_map( 'intval', $term_ids );
        $term_ids = array_unique( $term_ids );
    
        $check = sanitize_sql_in( $term_ids, false );
        if ( ! $check ) {
            return array();
        }
    
        $exists = $wpdb->get_col( $wpdb->prepare( "SELECT term_taxonomy_id FROM $wpdb->term_taxonomy WHERE term_id IN ($check) AND taxonomy = %s", $taxonomy ) );
        $term_ids = array_intersect( $term_ids, $exists );
    
        if ( empty( $term_ids ) ) {
            return array();
        }
    
        $query = "SELECT term_id, term_taxonomy_id FROM $wpdb->term_taxonomy WHERE term_id IN ($check) AND taxonomy = %s";
        $term_taxonomy_ids = $wpdb->get_results( $wpdb->prepare( $query, $taxonomy ), OBJECT_K );
    
        $values = array();
        $placeholders = array();
    
        foreach ( $term_ids as $term_id ) {
            $tt_id = $term_taxonomy_ids[ $term_id ]->term_taxonomy_id;
            $values[] = $wpdb->prepare( "(%d, %d, %s)", $tt_id, $object_id, 0 );
            $placeholders[] = '%d, %d, %s';
            $tt_ids[] = $tt_id;
        }
    
        if ( ! empty( $values ) ) {
            $query = "INSERT IGNORE INTO $wpdb->term_relationships (term_taxonomy_id, object_id, term_order) VALUES ";
            $query .= implode( ', ', $values );
    
            $wpdb->query( $query );
        }
    
        wp_cache_delete( $object_id, 'terms' );
    
        return $tt_ids;
    }
  • wp_update_term_count():更新分类术语的计数

    这个函数负责更新 wp_term_taxonomy 表中每个分类术语的 count 字段,确保它反映了实际关联的文章数量。 这个函数非常重要,因为它影响着分类列表的显示和相关功能的运作。

    /**
     * Updates Term Count.
     *
     * Recalculates term counts, using a taxonomy query.
     *
     * @since 2.3.0
     *
     * @param int|array $terms    Single term ID or array of term IDs.
     * @param string|array $taxonomies Single taxonomy name or array of taxonomy names.
     */
    function wp_update_term_count( $terms, $taxonomies ) {
        global $wpdb;
    
        $object_types = array_unique( (array) apply_filters( 'wp_term_taxonomy_object_types', array( 'post' ), $taxonomies ) );
    
        foreach ( (array) $taxonomies as $taxonomy ) {
            $taxonomy_obj = get_taxonomy( $taxonomy );
            if ( empty( $taxonomy_obj->object_type ) ) {
                continue;
            }
    
            $_terms = (array) $terms;
            if ( ! empty( $_terms ) ) {
                $terms = "'" . implode( "', '", array_map( 'esc_sql', $_terms ) ) . "'";
            }
    
            // 'post' is the only type that is supported by default.
            if ( isset( $taxonomy_obj->object_type ) && is_array( $taxonomy_obj->object_type ) ) {
                $object_types = array_intersect( $object_types, $taxonomy_obj->object_type );
            }
    
            $object_types = array_map( 'esc_sql', $object_types );
    
            /**
             * Filters the object types used in term count queries.
             *
             * @since 4.9.0
             *
             * @param array  $object_types Array of object types.
             * @param string $taxonomy     The taxonomy being updated.
             */
            $object_types = apply_filters( 'wp_update_term_count_object_types', $object_types, $taxonomy );
    
            $object_types = "'" . implode( "', '", $object_types ) . "'";
    
            $query = "UPDATE $wpdb->term_taxonomy SET count = (
                SELECT COUNT(*) FROM $wpdb->term_relationships, $wpdb->posts
                WHERE $wpdb->term_relationships.term_taxonomy_id = $wpdb->term_taxonomy.term_taxonomy_id
                AND $wpdb->posts.ID = $wpdb->term_relationships.object_id
                AND $wpdb->posts.post_status IN ('publish', 'future', 'draft', 'pending', 'private')
                AND $wpdb->posts.post_type IN ($object_types)
            ) WHERE taxonomy = '$taxonomy'";
            if ( ! empty( $_terms ) ) {
                $query .= " AND term_id IN ($terms)";
            }
            $wpdb->query( $query );
        }
    
        wp_cache_flush();
    }

第四幕: 总结

wp_set_post_terms() 函数,以及它调用的 wp_set_object_terms()wp_add_object_terms()wp_update_term_count() 函数,共同完成了为文章设置分类术语的任务。 它们负责验证参数、建立关联关系、更新分类术语计数,并确保数据的完整性和一致性。

函数名 主要作用
wp_set_post_terms() 为文章设置分类术语,是 wp_set_object_terms() 的一个 wrapper 函数。
wp_set_object_terms() 为指定对象(例如文章)设置分类术语,负责参数验证、删除旧关联关系、添加新关联关系、更新缓存和触发 action hook。
wp_add_object_terms() wp_term_relationships 表中插入数据,建立对象与分类术语之间的关联关系。
wp_update_term_count() 更新 wp_term_taxonomy 表中每个分类术语的 count 字段,确保它反映了实际关联的文章数量。

理解这些函数的运作方式,可以帮助我们更好地进行WordPress开发,并能更好地理解WordPress是如何管理文章分类的。

剧终: 感谢观看!

希望今天的讲座能帮助大家更好地理解 wp_set_post_terms() 函数及其背后的机制。 掌握了这些知识,你就可以更加自信地驾驭WordPress,打造出更加强大的网站! 感谢各位的观看,我们下期再见!

发表回复

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