深入理解 `WP_Site_Query` 类的源码,它在多站点模式下是如何查询子站点的?

各位观众老爷,大家好!我是你们的老朋友,BUG终结者。今天咱们来聊聊WordPress多站点模式下,一个神秘而强大的类:WP_Site_Query。它就像一个神通广大的侦探,专门负责在你的站点宇宙中寻找各种子站点。

准备好了吗? 咱们这就开始一场源码探险之旅!

1. 什么是 WP_Site_Query? 为什么要学习它?

在单站点WordPress中,我们主要和文章、页面打交道。但在多站点模式下,情况就复杂多了。我们需要管理多个独立的站点,每个站点都有自己的内容、用户和设置。WP_Site_Query 就应运而生,它提供了一种结构化的方式来查询和获取这些子站点的信息。

为什么要学习它?原因很简单:

  • 定制化需求: WordPress自带的站点管理界面可能无法满足你所有的需求。例如,你可能需要根据特定条件筛选站点,或者以不同的方式展示站点列表。
  • 性能优化: 如果你需要频繁地查询站点信息,直接使用SQL语句可能会导致性能问题。WP_Site_Query 提供了一些缓存机制,可以提高查询效率。
  • 理解底层机制: 深入了解 WP_Site_Query 可以帮助你更好地理解 WordPress 多站点的工作原理,从而更好地进行开发和维护。

2. WP_Site_Query 的构造函数:侦探的初始化

咱们先从 WP_Site_Query 的构造函数开始。这就像侦探拿到案件卷宗,开始了解基本情况。

/**
 * WP_Site_Query class.
 *
 * @since 4.6.0
 */
class WP_Site_Query {

    /**
     * SQL query string.
     *
     * @since 4.6.0
     * @access public
     * @var string
     */
    public $request;

    /**
     * List of found site IDs.
     *
     * @since 4.6.0
     * @access public
     * @var array
     */
    public $sites;

    /**
     * The total number of found sites.
     *
     * @since 4.6.0
     * @access public
     * @var int
     */
    public $found_sites = 0;

    /**
     * The number of found sites for the current query.
     *
     * @since 4.6.0
     * @access public
     * @var int
     */
    public $site_count = 0;

    /**
     * Query vars set by the user.
     *
     * @since 4.6.0
     * @access public
     * @var array
     */
    public $query_vars = array();

    /**
     * Default values for query vars.
     *
     * @since 4.6.0
     * @access public
     * @var array
     */
    public $default_query = array(
        'orderby'        => 'id',
        'order'          => 'ASC',
        'number'         => 100,
        'offset'         => '',
        'fields'         => 'all',
        'search'         => '',
        'network_id'     => 0,
        'ID'             => 0,
        'site__in'       => array(),
        'site__not_in'   => array(),
        'public'         => null,
        'archived'       => null,
        'mature'         => null,
        'spam'           => null,
        'deleted'        => null,
        'lang_id'        => 0,
        'domain'         => '',
        'path'           => '',
        'domain__like'   => '',
        'path__like'     => '',
        'search_columns' => array( 'domain', 'path' ),
        'update_site_cache' => true,
        'cache_domain'    => 'core',
    );

    /**
     * Constructor.
     *
     * @since 4.6.0
     * @access public
     *
     * @param string|array $query Optional. Array or query string of site query parameters.
     */
    public function __construct( $query = '' ) {
        $this->query_vars = wp_parse_args( $query );
        $this->query_vars = array_merge( $this->default_query, $this->query_vars );
        $this->prepare_query();
        $this->query();
    }
}

这个构造函数接收一个可选的 $query 参数,它可以是一个数组或者一个查询字符串,用于指定查询的条件。

  • wp_parse_args(): 这个函数会将传入的 $query 参数解析成一个数组,并将其与默认的查询参数合并。 这样,即使你只传入了部分参数,也能保证所有的查询参数都有值。
  • prepare_query(): 这个函数会根据查询参数,构建SQL查询语句。
  • query(): 这个函数会执行SQL查询,并将结果保存到 $sites 属性中。

可以看到,构造函数的主要任务是:

  1. 解析查询参数。
  2. 准备SQL查询语句。
  3. 执行查询。

3. 查询参数:侦探的工具箱

WP_Site_Query 提供了丰富的查询参数,可以让你像侦探一样,根据各种线索来寻找目标站点。

参数 描述 默认值
orderby 排序的字段,可以是 iddomainpath 等。 id
order 排序的方式,可以是 ASC(升序)或 DESC(降序)。 ASC
number 返回站点的数量。 100
offset 偏移量,用于分页。 ''
fields 返回的字段,可以是 all(返回所有字段)或 ids(只返回站点ID)。 all
search 搜索关键词,会在 domainpath 字段中搜索。 ''
network_id 网络ID,用于指定查询哪个网络下的站点。 0
ID 站点ID,用于精确匹配某个站点。 0
site__in 一个站点ID数组,用于查询指定ID的站点。 array()
site__not_in 一个站点ID数组,用于排除指定ID的站点。 array()
public 是否公开,可以是 1(公开)或 0(不公开)。 null
archived 是否已存档,可以是 1(已存档)或 0(未存档)。 null
mature 是否包含成人内容,可以是 1(包含)或 0(不包含)。 null
spam 是否被标记为垃圾站点,可以是 1(是)或 0(否)。 null
deleted 是否已删除,可以是 1(已删除)或 0(未删除)。 null
lang_id 语言ID,用于指定查询哪个语言的站点。 0
domain 域名,用于精确匹配某个站点。 ''
path 路径,用于精确匹配某个站点。 ''
domain__like 域名,支持模糊匹配。 ''
path__like 路径,支持模糊匹配。 ''
search_columns 搜索的字段,可以是 domainpath array('domain', 'path')
update_site_cache 是否更新站点缓存。 true
cache_domain 缓存的域名。 core

这些参数就像侦探工具箱里的各种工具,可以根据不同的案件需求选择合适的工具。

4. prepare_query():构建SQL查询语句

prepare_query() 函数是 WP_Site_Query 的核心之一,它负责根据查询参数,构建最终的SQL查询语句。

    /**
     * Prepares the site query variables.
     *
     * @since 4.6.0
     * @access public
     *
     * @return null
     */
    public function prepare_query() {
        global $wpdb;

        $qv = $this->query_vars;

        $orderby = $this->parse_orderby( $qv['orderby'] );

        $qv['order'] = strtoupper( $qv['order'] );
        if ( 'DESC' !== $qv['order'] ) {
            $qv['order'] = 'ASC';
        }

        $number = absint( $qv['number'] );
        $offset = absint( $qv['offset'] );

        $this->query_vars = $qv;

        $site_fields = $this->get_fields_for_db( $qv['fields'] );

        $where = $this->get_sql_clauses( $qv );

        $join = $where['join'];
        $where = $where['where'];

        $limits = '';
        if ( ! empty( $number ) ) {
            if ( $offset ) {
                $limits = 'LIMIT ' . $offset . ',' . $number;
            } else {
                $limits = 'LIMIT ' . $number;
            }
        }

        $this->request = "SELECT $site_fields FROM {$wpdb->sitemeta} AS sm INNER JOIN {$wpdb->sites} AS s ON sm.site_id = s.id $join WHERE 1=1 $where ORDER BY {$orderby} {$qv['order']} $limits";
        $this->query_vars['number'] = $number;
        $this->query_vars['offset'] = $offset;
    }

这个函数的主要步骤如下:

  1. 解析排序参数: 使用 parse_orderby() 函数解析 orderby 参数,将其转换为SQL语句中使用的排序字段。
  2. 处理排序方式:order 参数转换为大写,并确保其值为 ASCDESC
  3. 处理数量和偏移量:numberoffset 参数转换为整数。
  4. 获取数据库字段: 使用 get_fields_for_db() 函数根据 fields 参数,获取需要查询的数据库字段。
  5. 构建WHERE子句: 使用 get_sql_clauses() 函数根据查询参数,构建SQL语句中的 WHERE 子句。这是最复杂的部分,后面会详细介绍。
  6. 构建LIMIT子句: 根据 numberoffset 参数,构建SQL语句中的 LIMIT 子句,用于分页。
  7. 构建完整的SQL语句: 将所有的SQL片段拼接在一起,形成最终的SQL查询语句,并将其保存到 $this->request 属性中。

5. get_sql_clauses():构建 WHERE 子句的核心

get_sql_clauses() 函数是构建 WHERE 子句的核心,它会根据查询参数,生成相应的 SQL 条件。

    /**
     * Generates SQL clauses to be appended to the main query.
     *
     * @since 4.6.0
     * @access protected
     *
     * @param array $qv An array of query variables.
     * @return array An array containing the JOIN and WHERE clauses.
     */
    protected function get_sql_clauses( $qv = array() ) {
        global $wpdb;

        $where = array();
        $join  = '';

        // Network ID.
        if ( ! empty( $qv['network_id'] ) ) {
            $where['network_id'] = $wpdb->prepare( 's.network_id = %d', $qv['network_id'] );
        }

        // ID.
        if ( ! empty( $qv['ID'] ) ) {
            $where['ID'] = $wpdb->prepare( 's.id = %d', $qv['ID'] );
        }

        // Site IDs.
        if ( ! empty( $qv['site__in'] ) ) {
            $site__in = wp_parse_id_list( $qv['site__in'] );
            $where['site__in'] = 's.id IN ( ' . implode( ',', array_map( 'intval', $site__in ) ) . ' )';
        } elseif ( ! empty( $qv['site__not_in'] ) ) {
            $site__not_in = wp_parse_id_list( $qv['site__not_in'] );
            $where['site__not_in'] = 's.id NOT IN ( ' . implode( ',', array_map( 'intval', $site__not_in ) ) . ' )';
        }

        // Public, Archived, Mature, Spam, Deleted.
        foreach ( array( 'public', 'archived', 'mature', 'spam', 'deleted' ) as $key ) {
            if ( is_numeric( $qv[ $key ] ) ) {
                $where[ $key ] = $wpdb->prepare( "s.{$key} = %d", $qv[ $key ] );
            }
        }

        // Lang ID.
        if ( ! empty( $qv['lang_id'] ) ) {
            $where['lang_id'] = $wpdb->prepare( 'sm.meta_value = %d AND sm.meta_key = %s', $qv['lang_id'], 'lang_id' );
            $join .= " INNER JOIN {$wpdb->sitemeta} AS sm ON s.id = sm.site_id";

        }

        // Domain & Path.
        if ( ! empty( $qv['domain'] ) ) {
            $where['domain'] = $wpdb->prepare( 's.domain = %s', $qv['domain'] );
        }

        if ( ! empty( $qv['path'] ) ) {
            $where['path'] = $wpdb->prepare( 's.path = %s', $qv['path'] );
        }

        // Domain & Path LIKE.
        if ( ! empty( $qv['domain__like'] ) ) {
            $where['domain__like'] = $wpdb->prepare( 's.domain LIKE %s', '%' . $wpdb->esc_like( $qv['domain__like'] ) . '%' );
        }

        if ( ! empty( $qv['path__like'] ) ) {
            $where['path__like'] = $wpdb->prepare( 's.path LIKE %s', '%' . $wpdb->esc_like( $qv['path__like'] ) . '%' );
        }

        // Search.
        if ( ! empty( $qv['search'] ) ) {
            $searches = array();
            foreach ( (array) $qv['search_columns'] as $column ) {
                if ( 'domain' === $column ) {
                    $searches[] = $wpdb->prepare( 's.domain LIKE %s', '%' . $wpdb->esc_like( $qv['search'] ) . '%' );
                } elseif ( 'path' === $column ) {
                    $searches[] = $wpdb->prepare( 's.path LIKE %s', '%' . $wpdb->esc_like( $qv['search'] ) . '%' );
                }
            }

            if ( ! empty( $searches ) ) {
                $where['search'] = '(' . implode( ' OR ', $searches ) . ')';
            }
        }

        /**
         * Filters the WHERE clause of the sites query.
         *
         * @since 4.6.0
         *
         * @param array $where An associative array of WHERE clauses.
         * @param WP_Site_Query $this The WP_Site_Query instance.
         */
        $where = apply_filters( 'sites_pre_query', $where, $this );

        return array(
            'join'  => implode( ' ', array_unique( array_filter( $join ) ) ),
            'where' => implode( ' AND ', array_filter( $where ) ),
        );
    }

这个函数会遍历所有的查询参数,并根据不同的参数类型,生成相应的SQL条件。

  • network_idID 使用 $wpdb->prepare() 函数,生成精确匹配的SQL条件。
  • site__insite__not_in 使用 INNOT IN 运算符,生成包含或排除指定ID的SQL条件。
  • publicarchivedmaturespamdeleted 使用 $wpdb->prepare() 函数,生成精确匹配的SQL条件。
  • lang_id 需要特别注意,它查询的是 wp_sitemeta 表,所以需要添加一个 JOIN 子句,将 wp_sites 表和 wp_sitemeta 表连接起来。
  • domainpath 使用 $wpdb->prepare() 函数,生成精确匹配的SQL条件。
  • domain__likepath__like 使用 LIKE 运算符,生成模糊匹配的SQL条件。
  • search 根据 search_columns 参数,在 domainpath 字段中进行模糊搜索。

最后,这个函数会将所有的SQL条件拼接在一起,形成最终的 WHERE 子句,并将其返回。

6. query():执行查询并获取结果

query() 函数负责执行SQL查询,并将结果保存到 $sites 属性中。

    /**
     * Executes the query, with the current variables.
     *
     * @since 4.6.0
     * @access public
     *
     * @return array|int List of sites, or number of sites when 'count' is passed as a query var.
     */
    public function query() {
        global $wpdb;

        /**
         * Fires before the site query.
         *
         * @since 4.6.0
         *
         * @param WP_Site_Query $this The WP_Site_Query instance.
         */
        do_action( 'pre_get_sites', $this );

        $this->sites = array();
        $this->site_count = 0;

        // Count results if limiting is not in effect.
        if ( ! empty( $this->query_vars['number'] ) ) {
            $found_sites_query = "SELECT FOUND_ROWS()";
        } else {
            $found_sites_query = $this->request;
        }

        if ( 'ids' === $this->query_vars['fields'] ) {
            $this->sites = $wpdb->get_col( $this->request );
        } else {
            $this->sites = $wpdb->get_results( $this->request );
        }

        if ( ! empty( $this->query_vars['number'] ) ) {
            $this->found_sites = (int) $wpdb->get_var( $found_sites_query );
        }

        $this->site_count = count( $this->sites );

        if ( $this->site_count && $this->query_vars['update_site_cache'] ) {
            wp_cache_add_multiple( $this->sites, 'sites', $this->query_vars['cache_domain'] );
        }

        /**
         * Fires after the site query.
         *
         * @since 4.6.0
         *
         * @param array $sites The array of queried sites.
         * @param WP_Site_Query $this The WP_Site_Query instance.
         */
        do_action( 'sites_pre_query', $this->sites, $this );

        return $this->sites;
    }

这个函数的主要步骤如下:

  1. 执行SQL查询: 使用 $wpdb->get_col()$wpdb->get_results() 函数,执行SQL查询,并将结果保存到 $this->sites 属性中。
  2. 获取总数: 如果指定了 number 参数,则使用 FOUND_ROWS() 函数获取总的站点数量,并将其保存到 $this->found_sites 属性中。
  3. 更新缓存: 如果 update_site_cache 参数为 true,则将查询结果添加到缓存中,以提高后续查询的效率。

7. 实际应用:侦探的实战案例

现在,我们来模拟几个实际的应用场景,看看如何使用 WP_Site_Query

案例1:查找所有未存档的站点

$args = array(
    'archived' => 0,
);

$site_query = new WP_Site_Query( $args );
$sites = $site_query->get_sites();

foreach ( $sites as $site ) {
    echo 'Site ID: ' . $site->id . '<br>';
    echo 'Site Domain: ' . $site->domain . '<br>';
    echo 'Site Path: ' . $site->path . '<br>';
    echo '<hr>';
}

案例2:查找域名包含 "example" 的站点

$args = array(
    'domain__like' => 'example',
);

$site_query = new WP_Site_Query( $args );
$sites = $site_query->get_sites();

foreach ( $sites as $site ) {
    echo 'Site ID: ' . $site->id . '<br>';
    echo 'Site Domain: ' . $site->domain . '<br>';
    echo 'Site Path: ' . $site->path . '<br>';
    echo '<hr>';
}

案例3:分页显示站点列表

$paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
$per_page = 10;

$args = array(
    'number' => $per_page,
    'offset' => ( $paged - 1 ) * $per_page,
);

$site_query = new WP_Site_Query( $args );
$sites = $site_query->get_sites();
$total_sites = $site_query->found_sites;

echo '<p>Total Sites: ' . $total_sites . '</p>';

foreach ( $sites as $site ) {
    echo 'Site ID: ' . $site->id . '<br>';
    echo 'Site Domain: ' . $site->domain . '<br>';
    echo 'Site Path: ' . $site->path . '<br>';
    echo '<hr>';
}

// 分页链接
$total_pages = ceil( $total_sites / $per_page );
$paginate_links = paginate_links( array(
    'base'      => get_pagenum_link( 1 ) . '%_%',
    'format'    => '/page/%#%',
    'current'   => $paged,
    'total'     => $total_pages,
) );

echo '<div class="pagination">' . $paginate_links . '</div>';

8. 总结:成为站点侦探

通过今天的学习,我们深入了解了 WP_Site_Query 的源码,掌握了它的基本原理和使用方法。

  • WP_Site_Query 是一个强大的类,用于在 WordPress 多站点模式下查询子站点。
  • 它提供了丰富的查询参数,可以让你根据各种条件来筛选站点。
  • prepare_query() 函数负责构建SQL查询语句。
  • get_sql_clauses() 函数是构建 WHERE 子句的核心。
  • query() 函数负责执行SQL查询,并将结果保存到 $sites 属性中。

掌握了 WP_Site_Query,你就可以像一位经验丰富的站点侦探,轻松地在你的站点宇宙中找到任何你需要的站点信息。

希望今天的讲解对你有所帮助。 如果你还有什么问题,欢迎随时提问。 我们下次再见!

发表回复

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