阐述 WordPress `pre_get_posts` 钩子源码:在 `WP_Query` 执行前如何修改查询参数。

各位观众老爷们,晚上好!欢迎来到今天的 WordPress 源码解读小课堂。今天咱们聊聊 pre_get_posts 这个神奇的钩子,它就像一个在厨师做菜之前,你可以偷偷往锅里加点料的魔法开关。咱们保证让你听得懂、学得会、用得上!

开场白:WP_Query,WordPress 的心脏

在深入 pre_get_posts 之前,咱得先了解 WP_Query 是个啥。简单来说,WP_Query 是 WordPress 里负责从数据库里捞数据的核心类。 无论是你访问首页、分类页、搜索结果页,还是自定义的页面,背后都离不开 WP_Query 在默默干活。

pre_get_posts:截胡的艺术

pre_get_posts 钩子,就给了我们一个机会,在 WP_Query 真正执行数据库查询之前,拦截并修改它的查询参数。想象一下,你原本想吃红烧肉,但通过 pre_get_posts,你可以让厨师把红烧肉变成糖醋里脊,是不是很棒?

pre_get_posts 的源码在哪里?

pre_get_posts 本身并不是一个函数,而是一个 action hook。它在 WP_Query::get_posts() 方法里被触发。 咱们简单追踪一下:

  1. WP_Query::get_posts(): 这个方法是 WP_Query 类里真正执行查询的地方。
  2. do_action_ref_array( 'pre_get_posts', array( &$this ) );: 在 get_posts() 方法的开头,你会看到这行代码。 do_action_ref_array() 就是触发 action hook 的函数。 pre_get_posts 就是我们今天要研究的钩子,而 &$this 则是把当前的 WP_Query 对象以引用的方式传递给所有挂载到 pre_get_posts 的函数。

举个栗子:修改首页的文章数量

假设你想在首页只显示 5 篇文章,而不是默认的 10 篇。你可以这样写:

function my_custom_pre_get_posts( $query ) {
  if ( $query->is_home() && $query->is_main_query() ) {
    $query->set( 'posts_per_page', 5 );
  }
}
add_action( 'pre_get_posts', 'my_custom_pre_get_posts' );

这段代码做了什么?

  • my_custom_pre_get_posts( $query ): 这是一个自定义函数,它接收一个 $query 对象,这个对象就是当前的 WP_Query 实例。
  • $query->is_home(): 检查当前是否是首页。
  • $query->is_main_query(): 检查当前是否是主查询。这个非常重要,因为 WordPress 里可能有很多 WP_Query 实例,我们通常只想修改主查询。
  • $query->set( 'posts_per_page', 5 ): 使用 set() 方法修改查询参数。这里我们把 posts_per_page 设置为 5。
  • add_action( 'pre_get_posts', 'my_custom_pre_get_posts' ): 把我们的自定义函数挂载到 pre_get_posts 钩子上。

几个重要的函数和方法

pre_get_posts 里,你经常会用到以下几个函数和方法:

函数/方法 作用
$query->is_home() 检查是否是首页
$query->is_single() 检查是否是单篇文章页
$query->is_archive() 检查是否是文章归档页(分类、标签、作者等)
$query->is_category() 检查是否是分类页
$query->is_tag() 检查是否是标签页
$query->is_search() 检查是否是搜索结果页
$query->is_main_query() 检查是否是主查询
$query->set( $key, $value ) 设置查询参数。 $key 是参数名,$value 是参数值。
$query->get( $key ) 获取查询参数。 $key 是参数名。

更高级的用法:根据用户角色修改查询

假设你想让管理员看到所有文章,而普通用户只能看到已发布的文章。你可以这样做:

function my_custom_pre_get_posts( $query ) {
  if ( ! is_admin() && $query->is_main_query() ) {
    $current_user = wp_get_current_user();
    if ( ! in_array( 'administrator', (array) $current_user->roles ) ) {
      $query->set( 'post_status', 'publish' );
    }
  }
}
add_action( 'pre_get_posts', 'my_custom_pre_get_posts' );

这段代码做了什么?

  • ! is_admin(): 确保代码不在后台运行。
  • wp_get_current_user(): 获取当前用户信息。
  • ! in_array( 'administrator', (array) $current_user->roles ): 检查当前用户是否是管理员。
  • $query->set( 'post_status', 'publish' ): 如果用户不是管理员,就把 post_status 设置为 publish,只显示已发布的文章。

再来一个例子:自定义文章类型的排序

假设你有一个自定义文章类型 product,你想按照价格(自定义字段 price)排序。

function my_custom_pre_get_posts( $query ) {
  if ( ! is_admin() && $query->is_main_query() && $query->is_post_type_archive( 'product' ) ) {
    $query->set( 'orderby', 'meta_value_num' );
    $query->set( 'meta_key', 'price' );
    $query->set( 'order', 'ASC' ); // 升序
  }
}
add_action( 'pre_get_posts', 'my_custom_pre_get_posts' );

这段代码做了什么?

  • $query->is_post_type_archive( 'product' ): 检查当前是否是 product 这种文章类型的归档页。
  • $query->set( 'orderby', 'meta_value_num' ): 设置排序方式为按照数字类型的自定义字段排序。
  • $query->set( 'meta_key', 'price' ): 设置自定义字段的键名为 price
  • $query->set( 'order', 'ASC' ): 设置排序顺序为升序。

pre_get_posts 的使用注意事项

  1. is_main_query() 的重要性: 一定要检查是否是主查询。否则,你可能会影响到 WordPress 后台或者其他地方的查询,导致意想不到的问题。
  2. 性能问题: pre_get_posts 会在每次查询之前都执行,所以你的代码要尽量简洁高效。避免执行复杂的逻辑或者数据库查询。
  3. 调试: 如果你的代码没有生效,可以使用 var_dump( $query ) 或者 wp_die( print_r( $query, true ) ) 来查看 $query 对象的内容,看看你的修改是否正确。

进阶技巧:使用 parse_query 钩子

除了 pre_get_posts,还有一个类似的钩子叫做 parse_query。它们的主要区别在于:

  • parse_query: 在 WP_Query 对象初始化之后,但是在解析查询字符串之前触发。你可以在这里访问原始的查询字符串,并进行修改。
  • pre_get_posts: 在 WP_Query 对象解析完查询字符串之后,但在真正执行查询之前触发。你在这里可以修改已经解析好的查询参数。

通常情况下,pre_get_posts 更常用,因为它更方便修改查询参数。但是,如果你需要访问或修改原始的查询字符串,parse_query 可能会更适合你。

案例分析:实现一个自定义的排序选项

假设你想在文章归档页添加一个自定义的排序选项,允许用户按照文章标题的长度排序。

  1. 前端添加排序选项: 首先,你需要在文章归档页添加一个下拉菜单,让用户选择排序方式。这部分涉及到 HTML 和 PHP,这里只给出 HTML 部分的示例:

    <select name="orderby_title_length">
      <option value="default">默认排序</option>
      <option value="title_length_asc">标题长度升序</option>
      <option value="title_length_desc">标题长度降序</option>
    </select>
  2. 获取用户选择的排序方式: 当用户选择排序方式并提交表单时,你需要获取用户选择的值。

    $orderby_title_length = isset( $_GET['orderby_title_length'] ) ? $_GET['orderby_title_length'] : 'default';
  3. 使用 pre_get_posts 修改查询: 接下来,你可以使用 pre_get_posts 钩子,根据用户选择的排序方式修改查询。

    function my_custom_pre_get_posts( $query ) {
      if ( ! is_admin() && $query->is_main_query() && $query->is_archive() && isset( $_GET['orderby_title_length'] ) ) {
        $orderby_title_length = $_GET['orderby_title_length'];
    
        if ( $orderby_title_length === 'title_length_asc' ) {
          add_filter( 'posts_orderby', 'my_custom_orderby_title_length_asc' );
        } elseif ( $orderby_title_length === 'title_length_desc' ) {
          add_filter( 'posts_orderby', 'my_custom_orderby_title_length_desc' );
        }
      }
    }
    add_action( 'pre_get_posts', 'my_custom_pre_get_posts' );
    
    function my_custom_orderby_title_length_asc( $orderby ) {
      global $wpdb;
      return "LENGTH({$wpdb->posts}.post_title) ASC";
    }
    
    function my_custom_orderby_title_length_desc( $orderby ) {
      global $wpdb;
      return "LENGTH({$wpdb->posts}.post_title) DESC";
    }

    这段代码做了什么?

    • isset( $_GET['orderby_title_length'] ): 检查 orderby_title_length 参数是否存在。
    • add_filter( 'posts_orderby', 'my_custom_orderby_title_length_asc' ): 根据用户选择的排序方式,添加相应的 posts_orderby 过滤器。 posts_orderby 是另一个钩子,允许你修改 SQL 查询语句的 ORDER BY 部分。
    • my_custom_orderby_title_length_asc( $orderby )my_custom_orderby_title_length_desc( $orderby ): 这两个函数分别返回按照标题长度升序和降序排序的 SQL 语句。

总结

pre_get_posts 钩子是 WordPress 里一个非常强大和灵活的工具,它可以让你在 WP_Query 执行之前,对查询参数进行各种各样的修改。 掌握 pre_get_posts,你就可以轻松地实现各种自定义的查询需求,让你的 WordPress 网站更加个性化和强大。 但请记住,能力越大,责任越大,使用 pre_get_posts 时一定要小心谨慎,避免影响网站的性能和稳定性。

今天的课程就到这里,希望大家有所收获! 下课!

发表回复

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