分析 WordPress `WP_Comment_Query` 类的源码:如何构建评论查询的参数和 SQL。

各位观众,晚上好!今天咱们来聊聊 WordPress 里的“评论侦探”—— WP_Comment_Query 类。这家伙专门负责从数据库里搜寻各种评论信息,就像福尔摩斯一样,根据你给的线索,给你找出你想要的评论。

我们今天要做的,就是解剖这位“侦探”,看看他是怎么接受线索(参数),又是怎么在茫茫评论数据中找到目标的(构建 SQL)。

第一幕:认识“侦探”——WP_Comment_Query

首先,咱们得对这位“侦探”有个基本的了解。WP_Comment_Query 类是 WordPress 提供的一个用于查询评论的类。它允许你通过各种参数来筛选评论,比如作者、日期、状态、关键词等等。

你可以像这样使用它:

$args = array(
  'author_email' => '[email protected]',
  'status'       => 'approve', // 只获取已批准的评论
  'number'       => 10,         // 最多获取 10 条评论
);

$comment_query = new WP_Comment_Query( $args );

$comments = $comment_query->comments;

if ( $comments ) {
  foreach ( $comments as $comment ) {
    echo '<p>' . $comment->comment_content . '</p>';
  }
} else {
  echo '没有找到符合条件的评论。';
}

这段代码创建了一个 WP_Comment_Query 对象,并传入了一个参数数组 $args。然后,它调用 $comment_query->comments 获取查询结果,并遍历显示评论内容。

第二幕:解读“线索”——参数的秘密

WP_Comment_Query 接受的参数种类繁多,就像福尔摩斯面前摆满了各种线索。这些参数可以分为几个大类:

  • 基本信息:

    • comment__in:只获取指定 ID 的评论(数组)。
    • comment__not_in:排除指定 ID 的评论(数组)。
    • number:获取评论的最大数量。
    • offset:从第几条评论开始获取。
    • paged:分页参数,用于获取指定页码的评论。
    • fields:返回的字段,可选值包括 'ids'(只返回 ID) 和 'all'(返回所有字段)。
  • 作者信息:

    • author_email:指定作者的邮箱地址。
    • author__in:指定作者 ID 的评论(数组)。
    • author__not_in:排除指定作者 ID 的评论(数组)。
  • 文章信息:

    • post_id:指定文章 ID。
    • post__in:指定文章 ID 的评论(数组)。
    • post__not_in:排除指定文章 ID 的评论(数组)。
    • post_author__in:指定文章作者 ID 的评论(数组)。
    • post_author__not_in:排除指定文章作者 ID 的评论(数组)。
    • post_type:指定文章类型。
    • post_status:指定文章状态。
  • 日期信息:

    • date_query:使用 WP_Date_Query 对象进行日期查询。
    • year:年份。
    • month:月份。
    • monthnum:月份(数字)。
    • w:周数。
    • day:天数。
    • hour:小时。
    • minute:分钟。
    • second:秒。
  • 内容信息:

    • keyword:关键词,用于搜索评论内容。
    • search:关键词,效果同 keyword
  • 状态信息:

    • status:评论状态,可选值包括 'approve'(已批准)、'hold'(待审核)、'spam'(垃圾评论)、'trash'(已删除) 和 'all'(所有状态)。
    • type:评论类型,比如 'comment''pingback''trackback'
    • parent:父级评论 ID。
    • parent__in:父级评论 ID(数组)。
    • parent__not_in:排除父级评论 ID(数组)。
  • 排序信息:

    • orderby:排序字段,可选值包括 'comment_ID''comment_post_ID''comment_author''comment_author_email''comment_date''comment_content''comment_karma''comment_approved'
    • order:排序方式,可选值包括 'ASC'(升序) 和 'DESC'(降序)。

这只是冰山一角,还有很多其他参数可以使用。你可以参考 WordPress 官方文档来了解更多信息。

第三幕:现场还原——SQL 构建过程

现在,咱们来深入 WP_Comment_Query 的内部,看看它是如何将这些“线索”转化为 SQL 查询语句的。

  1. 初始化:__construct() 方法

    当我们创建一个 WP_Comment_Query 对象时,__construct() 方法会被调用。这个方法主要做两件事:

    • 合并默认参数和用户传入的参数。
    • 调用 prepare_query() 方法来构建 SQL 查询语句。
    public function __construct( $query = '' ) {
      $this->query_vars = wp_parse_args( $query, $this->defaults );
      $this->query_vars = $this->fill_query_vars( $this->query_vars );
    
      /**
       * Fires before the comment query is run.
       *
       * @since 3.1.0
       *
       * @param WP_Comment_Query &$this The WP_Comment_Query instance (passed by reference).
       */
      do_action_ref_array( 'pre_get_comments', array( &$this ) );
    
      $this->prepare_query();
    }

    wp_parse_args() 函数用于将用户传入的参数和默认参数合并。$this->defaults 是一个包含默认参数的数组。

  2. 准备查询:prepare_query() 方法

    prepare_query() 方法是构建 SQL 查询语句的核心。它会根据 query_vars 属性中的参数,逐步构建 SQL 语句的各个部分。

    public function prepare_query( $query_vars = '' ) {
      global $wpdb;
    
      if ( empty( $query_vars ) ) {
        $this->query_vars = $this->fill_query_vars( $this->query_vars );
        $qv = &$this->query_vars;
      } else {
        $query_vars = $this->fill_query_vars( $query_vars );
        $qv = &$query_vars;
      }
    
      $this->comment_fields = '*';
    
      // Query parts.
      $this->query_from  = "FROM {$wpdb->comments}";
      $this->query_where = 'WHERE 1=1';
      $this->query_orderby = '';
      $this->query_limit = '';
      $this->query_groupby = '';
      $this->query_join = '';
    
      // ID sanity check.
      if ( ! empty( $qv['comment__in'] ) ) {
        $qv['comment__in'] = wp_parse_id_list( $qv['comment__in'] );
      }
    
      if ( ! empty( $qv['comment__not_in'] ) ) {
        $qv['comment__not_in'] = wp_parse_id_list( $qv['comment__not_in'] );
      }
    
      $where = '';
    
      // comment_id.
      if ( ! empty( $qv['comment__in'] ) ) {
        $where .= ' AND ' . $wpdb->comments . '.comment_ID IN ( ' . implode( ',', array_map( 'intval', $qv['comment__in'] ) ) . ' )';
      } elseif ( ! empty( $qv['comment__not_in'] ) ) {
        $where .= ' AND ' . $wpdb->comments . '.comment_ID NOT IN ( ' . implode( ',', array_map( 'intval', $qv['comment__not_in'] ) ) . ' )';
      }
    
      // Authors.
      if ( ! empty( $qv['author_email'] ) ) {
        $where .= $wpdb->prepare( ' AND ' . $wpdb->comments . '.comment_author_email = %s', $qv['author_email'] );
      }
    
      if ( ! empty( $qv['author__in'] ) ) {
        $where .= ' AND ' . $wpdb->comments . '.comment_user_id IN ( ' . implode( ',', array_map( 'intval', $qv['author__in'] ) ) . ' )';
      } elseif ( ! empty( $qv['author__not_in'] ) ) {
        $where .= ' AND ' . $wpdb->comments . '.comment_user_id NOT IN ( ' . implode( ',', array_map( 'intval', $qv['author__not_in'] ) ) . ' )';
      }
    
      // Post ID.
      if ( ! empty( $qv['post_id'] ) ) {
        $where .= $wpdb->prepare( ' AND ' . $wpdb->comments . '.comment_post_ID = %d', $qv['post_id'] );
      }
    
      if ( ! empty( $qv['post__in'] ) ) {
        $where .= ' AND ' . $wpdb->comments . '.comment_post_ID IN ( ' . implode( ',', array_map( 'intval', $qv['post__in'] ) ) . ' )';
      } elseif ( ! empty( $qv['post__not_in'] ) ) {
        $where .= ' AND ' . $wpdb->comments . '.comment_post_ID NOT IN ( ' . implode( ',', array_map( 'intval', $qv['post__not_in'] ) ) . ' )';
      }
    
      // Post Author ID.
      if ( ! empty( $qv['post_author__in'] ) ) {
        $where .= ' AND ' . $wpdb->posts . '.post_author IN ( ' . implode( ',', array_map( 'intval', $qv['post_author__in'] ) ) . ' )';
        $this->query_from .= " INNER JOIN {$wpdb->posts} ON {$wpdb->comments}.comment_post_ID = {$wpdb->posts}.ID";
      } elseif ( ! empty( $qv['post_author__not_in'] ) ) {
        $where .= ' AND ' . $wpdb->posts . '.post_author NOT IN ( ' . implode( ',', array_map( 'intval', $qv['post_author__not_in'] ) ) . ' )';
        $this->query_from .= " INNER JOIN {$wpdb->posts} ON {$wpdb->comments}.comment_post_ID = {$wpdb->posts}.ID";
      }
    
      // Post Type.
      if ( ! empty( $qv['post_type'] ) ) {
        $post_types = wp_parse_slug_list( $qv['post_type'] );
        if ( $post_types ) {
          $post_types = array_map( array( $wpdb, 'escape' ), $post_types );
          $where .= " AND {$wpdb->posts}.post_type IN ('" . implode( "', '", $post_types ) . "')";
          $this->query_from .= " INNER JOIN {$wpdb->posts} ON {$wpdb->comments}.comment_post_ID = {$wpdb->posts}.ID";
        }
      }
    
      // Post Status.
      if ( ! empty( $qv['post_status'] ) ) {
        $post_statuses = wp_parse_slug_list( $qv['post_status'] );
        if ( $post_statuses ) {
          $post_statuses = array_map( array( $wpdb, 'escape' ), $post_statuses );
          $where .= " AND {$wpdb->posts}.post_status IN ('" . implode( "', '", $post_statuses ) . "')";
          $this->query_from .= " INNER JOIN {$wpdb->posts} ON {$wpdb->comments}.comment_post_ID = {$wpdb->posts}.ID";
        }
      }
    
      // Date queries.
      if ( ! empty( $qv['date_query'] ) && is_array( $qv['date_query'] ) ) {
        $date_query = new WP_Date_Query( $qv['date_query'], $wpdb->comments . '.comment_date' );
        $where .= $date_query->get_sql();
      } else {
        // Legacy date parameters.
        $date_query = new WP_Date_Query( $qv, $wpdb->comments . '.comment_date' );
        $where .= $date_query->get_sql();
      }
    
      // Keywords.
      if ( ! empty( $qv['keyword'] ) || ! empty( $qv['search'] ) ) {
        $search_term = ! empty( $qv['keyword'] ) ? $qv['keyword'] : $qv['search'];
        $search_term = esc_sql( $wpdb->esc_like( $search_term ) );
        $where .= " AND {$wpdb->comments}.comment_content LIKE '%{$search_term}%'";
      }
    
      // Comment status.
      if ( ! empty( $qv['status'] ) ) {
        $approved = wp_get_comment_status( $qv['status'] );
    
        if ( is_array( $approved ) ) {
          $approved = array_map( 'esc_sql', $approved );
          $where .= " AND {$wpdb->comments}.comment_approved IN ( '" . implode( "', '", $approved ) . "' )";
        } else {
          $where .= $wpdb->prepare( " AND {$wpdb->comments}.comment_approved = %s", $approved );
        }
      }
    
      // Comment type.
      if ( ! empty( $qv['type'] ) ) {
        $types = wp_parse_slug_list( $qv['type'] );
        $types = array_map( 'esc_sql', $types );
        $where .= " AND {$wpdb->comments}.comment_type IN ( '" . implode( "', '", $types ) . "' )";
      }
    
      // Comment parent.
      if ( ! empty( $qv['parent'] ) ) {
        $where .= $wpdb->prepare( ' AND ' . $wpdb->comments . '.comment_parent = %d', $qv['parent'] );
      }
    
      if ( ! empty( $qv['parent__in'] ) ) {
        $where .= ' AND ' . $wpdb->comments . '.comment_parent IN ( ' . implode( ',', array_map( 'intval', $qv['parent__in'] ) ) . ' )';
      } elseif ( ! empty( $qv['parent__not_in'] ) ) {
        $where .= ' AND ' . $wpdb->comments . '.comment_parent NOT IN ( ' . implode( ',', array_map( 'intval', $qv['parent__not_in'] ) ) . ' )';
      }
    
      $this->query_where .= $where;
    
      // Ordering.
      $orderby_array = array();
    
      if ( ! empty( $qv['orderby'] ) ) {
        $orders = is_array( $qv['orderby'] ) ? $qv['orderby'] : preg_split( '/[,s]+/', $qv['orderby'] );
    
        foreach ( $orders as $order ) {
          $order = strtolower( $order );
    
          switch ( $order ) {
            case 'comment_id':
              $orderby_array[] = $wpdb->comments . '.comment_ID';
              break;
            case 'comment_post_id':
              $orderby_array[] = $wpdb->comments . '.comment_post_ID';
              break;
            case 'comment_author':
              $orderby_array[] = $wpdb->comments . '.comment_author';
              break;
            case 'comment_author_email':
              $orderby_array[] = $wpdb->comments . '.comment_author_email';
              break;
            case 'comment_date':
              $orderby_array[] = $wpdb->comments . '.comment_date';
              break;
            case 'comment_content':
              $orderby_array[] = $wpdb->comments . '.comment_content';
              break;
            case 'comment_karma':
              $orderby_array[] = $wpdb->comments . '.comment_karma';
              break;
            case 'comment_approved':
              $orderby_array[] = $wpdb->comments . '.comment_approved';
              break;
            default:
              $orderby_array[] = $wpdb->comments . '.comment_ID';
          }
        }
      }
    
      if ( ! empty( $orderby_array ) ) {
        $this->query_orderby = 'ORDER BY ' . implode( ',', $orderby_array );
    
        if ( isset( $qv['order'] ) ) {
          $this->query_orderby .= ' ' . strtoupper( $qv['order'] );
        }
      }
    
      // Limit.
      if ( ! empty( $qv['number'] ) ) {
        if ( ! empty( $qv['offset'] ) ) {
          $this->query_limit = $wpdb->prepare( 'LIMIT %d, %d', $qv['offset'], $qv['number'] );
        } else {
          $this->query_limit = $wpdb->prepare( 'LIMIT %d', $qv['number'] );
        }
      }
    
      // Paged.
      if ( $qv['number'] && $qv['paged'] ) {
        $page = absint( $qv['paged'] );
        $number = absint( $qv['number'] );
        $offset = ( $page - 1 ) * $number;
        $this->query_limit = $wpdb->prepare( 'LIMIT %d, %d', $offset, $number );
      }
    
      // 'fields' => 'ids' returns only the IDs.
      if ( 'ids' === $qv['fields'] ) {
        $this->comment_fields = "{$wpdb->comments}.comment_ID";
      }
    
      $this->request = "SELECT {$this->comment_fields} {$this->query_from} {$this->query_where} {$this->query_groupby} {$this->query_orderby} {$this->query_limit}";
    }

    这个方法做了很多事情,咱们挑几个重点来说:

    • 构建 WHERE 子句: 它会根据 query_vars 中的各种参数,构建 SQL 语句的 WHERE 子句。比如,如果指定了 author_email,就会添加 AND comment_author_email = %s 这样的条件。注意,这里使用了 $wpdb->prepare() 来防止 SQL 注入。
    • 处理日期查询: 如果使用了 date_query 参数,它会创建一个 WP_Date_Query 对象,并调用其 get_sql() 方法来获取日期查询的 SQL 语句。
    • 构建 ORDER BY 子句: 它会根据 orderbyorder 参数,构建 SQL 语句的 ORDER BY 子句。
    • 构建 LIMIT 子句: 它会根据 numberoffsetpaged 参数,构建 SQL 语句的 LIMIT 子句,用于限制返回的评论数量。
    • 构建完整的 SQL 语句: 最后,它会将各个部分组合起来,形成完整的 SQL 查询语句,并将其存储在 $this->request 属性中。
  3. 执行查询:get_comments() 方法

    get_comments() 方法负责执行 SQL 查询语句,并返回查询结果。

    public function get_comments() {
      global $wpdb;
    
      /**
       * Fires before the comment query executes.
       *
       * @since 3.1.0
       *
       * @param WP_Comment_Query &$this The WP_Comment_Query instance (passed by reference).
       */
      do_action_ref_array( 'pre_comment_query', array( &$this ) );
    
      $comments = $wpdb->get_results( $this->request );
    
      if ( 'ids' === $this->query_vars['fields'] ) {
        $this->comments = array_map( 'intval', $comments );
        return $this->comments;
      }
    
      // Prime caches.
      if ( $comments ) {
        update_comment_cache( $comments );
        $this->comments = array_map( 'get_comment', $comments );
      } else {
        $this->comments = array();
      }
    
      return $this->comments;
    }

    这个方法主要做了以下几件事:

    • 执行 SQL 查询: 它使用 $wpdb->get_results() 方法执行 $this->request 中存储的 SQL 查询语句。
    • 处理查询结果: 如果 fields 参数设置为 'ids',它会将查询结果转换为整数数组,并返回。否则,它会调用 update_comment_cache() 函数来更新评论缓存,并调用 get_comment() 函数将查询结果转换为 WP_Comment 对象数组,并返回。

第四幕:案例分析——一个复杂的查询

为了更好地理解 WP_Comment_Query 的工作原理,咱们来看一个稍微复杂一点的例子。

假设我们需要查询以下评论:

  • 作者邮箱地址为 [email protected]
  • 评论状态为 'approve'
  • 文章类型为 'post'
  • 评论内容包含关键词 'WordPress'
  • 按照评论日期降序排列。
  • 只获取前 10 条评论。

我们可以这样构建查询参数:

$args = array(
  'author_email' => '[email protected]',
  'status'       => 'approve',
  'post_type'    => 'post',
  'keyword'      => 'WordPress',
  'orderby'      => 'comment_date',
  'order'        => 'DESC',
  'number'       => 10,
);

$comment_query = new WP_Comment_Query( $args );

$comments = $comment_query->comments;

WP_Comment_Query 会将这些参数转化为以下 SQL 查询语句(简化版):

SELECT *
FROM wp_comments
INNER JOIN wp_posts ON wp_comments.comment_post_ID = wp_posts.ID
WHERE 1=1
AND wp_comments.comment_author_email = '[email protected]'
AND wp_comments.comment_approved = '1'
AND wp_posts.post_type = 'post'
AND wp_comments.comment_content LIKE '%WordPress%'
ORDER BY wp_comments.comment_date DESC
LIMIT 10

可以看到,WP_Comment_Query 根据我们提供的参数,构建了一个复杂的 SQL 查询语句,从而能够准确地找到我们想要的评论。

第五幕:总结与技巧

WP_Comment_Query 是一个功能强大的评论查询工具,掌握它可以帮助你更灵活地处理 WordPress 中的评论数据。

以下是一些使用 WP_Comment_Query 的技巧:

  • 使用 wp_parse_args() 函数: 可以使用 wp_parse_args() 函数来合并默认参数和用户传入的参数,从而简化代码。
  • 使用 $wpdb->prepare() 函数: 一定要使用 $wpdb->prepare() 函数来防止 SQL 注入。
  • 善用 date_query 参数: date_query 参数可以让你更灵活地进行日期查询。
  • 注意性能优化: 复杂的查询可能会影响性能,尽量避免不必要的查询条件,并合理使用缓存。

结语

希望今天的讲座能让你对 WP_Comment_Query 类有更深入的了解。 记住,熟练掌握 WP_Comment_Query,你就能像一位真正的“评论侦探”一样,轻松驾驭 WordPress 中的评论数据! 谢谢大家!

发表回复

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