自定义查询

1条评论

插件通过hook钩子(动作钩子和过滤器钩子)来扩展WordPress的功能。钩子能够改变WordPress的运行方式。有时插件需要执行自定义查询,这比在WordPress中加上动作钩子或是过滤器钩子要复杂得多。下面这篇文章介绍了自定义查询的相关信息,以及插件开发者怎样执行自定义查询。

注意:

  • 文章假设读者对编写插件用插件创建数据库表插件API的动作钩子和过滤器钩子以及MySQL数据库查询语言有基本了解。
  • 文章只适用于博客访问者能够看到的页面,不适用于WordPress后台界面(但某些操作可能会影响日志列表的后台页面)。
  • 文中所提到的文件名都是相对WordPress根目录而言。

背景知识介绍

定义

在本文中,查询指的是WordPress用来在主循环中寻找即将显示在页面上的日志列表的数据库查询(本文中的“数据库查询”即一般类型的数据库查询)。默认情况下,WordPress查询会查找属于当前页面的日志,无论是某一篇日志,或是静态页面、类别存档、日期存档、搜索结果、feed以及博客所有日志列表;查询日志限制在一定数量内(可以在WordPress后台的“设置”中设定),所有日志按时间逆序排列(最近发表的日志排在列表的最上方)。插件可以用自定义查询来改变日志排列顺序。例如:

  • 以不同顺序显示日志,如按字母顺序排列“glossary”分类中的日志
  • 更改将要显示在页面上的日志的默认数量;例如,glossary插件可能会显示更多“glossary”类别中的日志(相对于其它类别而言)
  • 从特定页面中删除某些文章;例如,属于“glossary”分类的日志将只出现在“glossary”类别页面,而不出现在主页和存档页面中。
  • 扩大WordPress默认的关键字查找范围(WordPress默认关键字查找范围仅限于日志标题和正文);例如某个地域标签插件的城市、州、乡村等字段。
  • 允许用example.com/blog?geostate=oregon 或example.com/blog/geostate/oregon等类似自定义URL来表示带有“Oregon”(俄勒冈州)标签的日志存档。

WordPress默认运行方式

修改WordPress中的默认查询前,我们需要了解WordPress的默认运行方式。查询语句概览中简要描述了WordPress生成博客页面的过程以及插件对该过程的修改。

执行自定义查询

现在我们可以开始执行自定义查询了。接下来我们会用几个例子来说明怎样修改查询,开始时的例子比较简单,之后会慢慢过度到相对复杂的例子。

日志排列顺序与总数限制

在第一个例子中,我们假设目前有一个glossary插件,网站/博客管理员可通过该插件将日志归入“glossary”类别(由插件保存在全局变量$gloss_category中)。我们希望达到的效果是:访问者访问glossary类别存档时看到的会是按字母顺序排列而不是按时间顺序排列的日志,是glossary类别下的所有日志而不是由网站管理规定显示数量的日志。

此时我们需要用以下两种方法来修改查询:

1. 为查询的ORDER BY语句加上一个过滤器钩子,将glossary类别下的日志列表改为字母顺序排列。过滤器钩子名为'posts_orderby',过滤SQL语句中ORDER BY后的内容。

2. 为查询的LIMIT语句加上一个过滤器钩子,解除日志的数量限制。过滤器钩子名为'post_limits',过滤SQL语句中包括包括LIMIT关键字在内关于日志数量限制的内容。

在上面两种方法中,过滤器函数都只会在我们浏览glossary类别时执行查询修改(用is_category函数进行逻辑操作)。因此,接下来我们需要编写如下代码:

add_filter('posts_orderby', 'gloss_alphabetical' );
add_filter('post_limits', 'gloss_limits' );

function gloss_alphabetical( $orderby )
{
  global $gloss_category;

  if( is_category( $gloss_category )) {
     // alphabetical order by post title
     return "post_title ASC";
  }

  // not in glossary category, return default order by
  return $orderby;
}

function gloss_limits( $limits )
{
  global $gloss_category;

  if( is_category( $gloss_category )) {
     // remove limits
     return "";
  }

  // not in glossary category, return default limits
  return $limits;
}

删除某类别日志

这里我们继续以glossary插件为例,这次我们希望能够禁止glossary类别下的日志出现在指定的页面上(主页与非类别存档页)以及feed中。要达到预期效果,首先要添加一个'pre_get_posts'动作钩子,该动作钩子能够探测当前访问者所请求的页面类型,之后根据页面类型删除glossary类别下的日志。根据查询说明(存储在$wp_query->query_vars中)中的规定,我们还可以在某个类别索引号前加上一个符号“-”以删除该类别。因此,修改后的代码如下:

add_action('pre_get_posts', 'gloss_remove_glossary_cat' );

function gloss_remove_glossary_cat( $notused )
{
  global $wp_query;
  global $gloss_category;

  // Figure out if we need to exclude glossary - exclude from
  // archives (except category archives), feeds, and home page
  if( is_home() || is_feed() ||
      ( is_archive() && !is_category() )) {
     $wp_query->query_vars['cat'] = '-' . $gloss_category;
  }
}

插件表中的关键字查找

这个例子中我们假设目前有一个地域标签插件,插件为每一篇日志贴上一个或多个城市、州、乡村的标签。该插件将所有标签存在自备的数据库表中;我们假设数据库表名在全局变量$geotag_table中且表名具有geotag_post_id, geotag_city, geotag_state, geotag_country字段。我们希望在这个例子中,每当有人进行关键字查找时(WordPress默认关键字查找范围仅限于日志标题和正文),都能够查找到内容中带有插件表字段关键字的日志。

因此,我们需要通过多种方式来修改之前用于查找日志的SQL查询(在查找页面上例外):

  • 用'posts_join'过滤器函数连接插件的数据库表与日志表,该函数运行在SQL JOIN语句上。
  • 用'posts_where'过滤器函数扩展查询的WHERE语句,使之能够查看插件表的字段。之后根据WordPress查找日志标题字段的原理,我们也可以对自定义表的字段进行相同操作(如不是复制WordPress复杂的逻辑语句)。WordPress添加的语句可能是这样的:(post_title LIKE 'xyz')。
  • 用'posts_groupby'过滤器函数为查询加上一个GROUP BY语句,该过滤器函数过滤SQL语句中GROUP BY之后的内容。这样如果有日志同时被标上Portland、Oregon和Salem、Oregon标签,访问者查找“Oregon”时,我们只会返回一次这篇日志。

结果代码显示如下:

add_filter('posts_join', 'geotag_search_join' );
add_filter('posts_where', 'geotag_search_where' );
add_filter('posts_groupby', 'geotag_search_groupby' );

function geotag_search_join( $join )
{
  global $geotag_table, $wpdb;

  if( is_search() ) {
    $join .= " LEFT JOIN $geotag_table ON " . 
       $wpdb->posts . ".ID = " . $geotag_table . 
       ".geotag_post_id ";
  }

  return $join;
}

function geotag_search_where( $where )
{
  if( is_search() ) {
    $where = preg_replace(
       "/\(\s*post_title\s+LIKE\s*(\'[^\']+\')\s*\)/",
       "(post_title LIKE \\1) OR (geotag_city LIKE \\1) OR (geotag_state LIKE \\1) OR (geotag_country LIKE \\1)", $where );
   }

  return $where;
}

function geotag_search_groupby( $groupby )
{
  global $wpdb;

  if( !is_search() ) {
    return $groupby;
  }

  // we need to group on post ID

  $mygroupby = "{$wpdb->posts}.ID";

  if( preg_match( "/$mygroupby/", $groupby )) {
    // grouping we need is already there
    return $groupby;
  }

  if( !strlen(trim($groupby))) {
    // groupby was empty, use ours
    return $mygroupby;
  }

  // wasn't empty, append ours
  return $groupby . ", " . $mygroupby;
}

自定义存档

这部分我们仍然以地域标签插件为例。首先假设我们需要用插件来激活类似于www.example.com/blog?geostate=oregon形式的自定义永久链接,让WordPress查找与“oregon”州名相符合的日志并显示在页面上。

要实现以上效果,插件必须进行以下操作:

  • 将“geostate”添加到WordPress能够识别的查询变量列表(query_vars过滤器函数),以确保WordPress解析URL时,州名被保存在查询变量中。下面是操作方法:
    add_filter('query_vars', 'geotag_queryvars' );
    
    function geotag_queryvars( $qvars )
    {
      $qvars[] = 'geostate';
      return $qvars;
    }
  • 查找到“geostate”查询变量时执行正确的查询;过程与之前提到的自定义查询相似。唯一的区别在于,这里用测试来判断“geostate”查询变量是否被探测到,而不是判断is_search或者posts_where以及其它各种数据库查询过滤器函数。用下面代码来代替上个例子中的if( is_search() ) 语句:
    global $wp_query;
    if( isset( $wp_query->query_vars['geostate'] )) {
       // modify the where/join/groupby similar to above examples
    }
  • 插件可能还需要生成这些永久链接。例如,插件可能有一个叫做geotags_list_states的函数,该函数能够查找出地域标签表中的各个州名,并为这些州名生成链接:
    function geotags_list_states( $sep = ", " )
    {
      global $geotag_table, $wpdb;
    
      // find list of states in DB
      $qry = "SELECT geotag_state FROM $geotag_table " . 
          " GROUP BY geotag_state ORDER BY geotag_state";
      $states = $wpdb->get_results( $qry );
    
      // make list of links
    
      $before = '<a href="' . get_bloginfo('home') . '?geostate=';
      $mid = '">';
      $after = "</a> ";
      $cur_sep = "";
      foreach( $states as $row ) {
        $state = $row->state;
        
        echo $cur_sep . $before . rawurlencode($state) . $mid . 
             $state . $after;
    
        // after the first time, we need separator
        $cur_sep = $sep;
      }
    
    }

    自定义存档的永久链接

    如果网站/博客管理员没有已激活的非默认永久链接,我们可以延续上一个例子(自定义存档)的内容,激活 example.com/blog/geostate/oregon,这样可以列出所有标有“Oregon”标签的日志。首先我们需要在WordPress的重写规则中加入一些内容,告诉WordPress如何解析永久链接类型的URL。具体来说,我们要添加一个能够告诉WordPress怎样解析/geostate/oregon 与?geostate=oregon的重写规则。(重写过程参见查询语句概览。)

    实际定义一个新的重写规则可以分两步:(1)用init过滤器函数“清理”缓存的重写规则,迫使WordPress重新计算重写规则,(2)计算重写规则时,用generate_rewrite_rules动作还属来添加新规则:

    add_action('init', 'geotags_flush_rewrite_rules');
    
    function geotags_flush_rewrite_rules() 
    {
       global $wp_rewrite;
       $wp_rewrite->flush_rules();
    }

    生成新规则相对复杂。首先,重写规则是一个关联数组,数组的关键字是能够匹配永久链接URL的正则表达式,而数组值则是相应非永久链接类型的URL。因此,要定义一个能与/geostate/oregon类似URL(其中oregon可以换成其它任意州名)相匹配的重写规则,并且告诉WordPress与?geostate=oregon相对应,我们需要进行以下操作:

    add_action('generate_rewrite_rules', 'geotags_add_rewrite_rules');
    
    function geotags_add_rewrite_rules( $wp_rewrite ) 
    {
      $new_rules = array( 
         'geostate/(.+)' => 'index.php?geostate=' .
           $wp_rewrite->preg_index(1) );
    
      $wp_rewrite->rules = $new_rules + $wp_rewrite->rules;
    }