query_posts, get_posts で複数の order 指定に対応する

元ネタは公式フォーラムのカスタムフィールドでソートした後に、日付の降順にしたいです。query_posts(), get_posts() で記事を取得する時に複数の項目でソートしたい、ことはよくあります。WordPress では orderby に半角スペース区切りで並び替えのキーとなる複数の項目を指定することができますが、昇順降順の order パラメータには ASC, DESC のどちらか1つしか指定できない仕様になっています。バグなハズはない。きっと深いワケがあるハズ。とはいえ、項目ごとに並び順を指定する方が需要は多いハズなのでやってみましょう!

※ order も複数指定できるよ、って話を以前どこかで見た気がするけど仕様変更されたのかしら。

コアのソースを見てみよう

query_posts() や素の get_posts() は WP3.2.1 wp-includes/query.php WP_Query::&get_posts() を呼んでます。で、当該処理は以下のようになってます。

if ( empty($q['order']) || ((strtoupper($q['order']) != 'ASC') && (strtoupper($q['order']) != 'DESC')) )
  $q['order'] = 'DESC';  // ■order は ASC か DESC じゃなければ DESC になります

// Order by
if ( empty($q['orderby']) ) {
  $orderby = "$wpdb->posts.post_date " . $q['order'];
} elseif ( 'none' == $q['orderby'] ) {
  $orderby = '';
} else {
  // Used to filter values
  $allowed_keys = array('author', 'date', 'title', 'modified', 'menu_order', 'parent', 'ID', 'rand', 'comment_count');
  if ( !empty($q['meta_key']) ) {
    $allowed_keys[] = $q['meta_key'];
    $allowed_keys[] = 'meta_value';
    $allowed_keys[] = 'meta_value_num';
  }
  $q['orderby'] = urldecode($q['orderby']);
  $q['orderby'] = addslashes_gpc($q['orderby']);

  $orderby_array = array();
  foreach ( explode( ' ', $q['orderby'] ) as $i => $orderby ) {  // ■orderby は半角スペース区切り!
    // Only allow certain values for safety
    if ( ! in_array($orderby, $allowed_keys) )
      continue;

    switch ( $orderby ) {
      case 'menu_order':
          break;
      case 'ID':
          $orderby = "$wpdb->posts.ID";
          break;
      case 'rand':
          $orderby = 'RAND()';
          break;
      case $q['meta_key']:
      case 'meta_value':
           $orderby = "$wpdb->postmeta.meta_value";
           break;
      case 'meta_value_num':
           $orderby = "$wpdb->postmeta.meta_value+0";
           break;
      case 'comment_count':
           $orderby = "$wpdb->posts.comment_count";
           break;
      default:
           $orderby = "$wpdb->posts.post_" . $orderby;
    }

    $orderby_array[] = $orderby;
  }
  $orderby = implode( ',', $orderby_array ); // ■orderby パラメータを半角コンマで連結

  if ( empty( $orderby ) )
    $orderby = "$wpdb->posts.post_date ".$q['order']; // ■orderby 指定が無効な場合は日付の降順
  else
    $orderby .= " {$q['order']}";  // ■最後に order パラメータを追加
}

ポイントとなる箇所にコメントを記載してます。これで、

  • order は ASC, DESC どちらか1つのみ
  • orderby は半角スペース区切りで複数指定可能
  • order パラメータは半角コンマ区切りで連結した orderby の最後に追加される

という現状把握ができました。

フィルタで ORDER BY 節をいじろう

節はブシじゃないですよ。WP_Query::&get_posts() ではSQL 文の各節ごとにフィルタが用意されているので書き換えは簡単です。

if ( !$q['suppress_filters'] ) { // ■get_posts() はこれがデフォルトで true!
  $where        = apply_filters_ref_array( 'posts_where_paged',    array( $where, &$this ) );
  $groupby    = apply_filters_ref_array( 'posts_groupby',        array( $groupby, &$this ) );
  $join        = apply_filters_ref_array( 'posts_join_paged',    array( $join, &$this ) );
  $orderby    = apply_filters_ref_array( 'posts_orderby',        array( $orderby, &$this ) ); // ■これを使う!
  $distinct    = apply_filters_ref_array( 'posts_distinct',    array( $distinct, &$this ) );
  $limits        = apply_filters_ref_array( 'post_limits',        array( $limits, &$this ) );
  $fields        = apply_filters_ref_array( 'posts_fields',        array( $fields, &$this ) );

  // Filter all clauses at once, for convenience
  $clauses = (array) apply_filters_ref_array( 'posts_clauses', array( compact( $pieces ), &$this ) );
  foreach ( $pieces as $piece )
     $$piece = isset( $clauses[ $piece ] ) ? $clauses[ $piece ] : '';
}

suppress_filters というパラメータはその名の通り、true に設定されているとフィルタを実行しません。WP_Query::&get_posts()query_posts() を使うときは敢えて true を指定しない限り false になるので何も心配しなくてもフィルタ実行してくれます。が、get_posts() という素の関数ではデフォルトパラメータとしてこれが true という仕様なのでフィルタを使用したい場合は false を明示的に指定します。以下にまとめますね。

// 気にしなくて OK!
query_posts( array(  'orderby' => 'date',  'order' => 'ASC' ) );

/ /気にしなくて OK!
$posts = new WP_query( array(  'orderby' => 'date',  'order' => 'ASC' );

// 気にしなくて OK!
$my_query = new WP_query();
$posts = $my_query->get_posts(  array( 'orderby' => 'date',  'order' => 'ASC' ) );

// 気にして!false にして!
$posts = get_posts(  array( 'orderby' => 'date',  'order' => 'ASC',  'suppress_filters' => false ) );
order パラメータの与え方

さて、order に複数パラメータを与えて posts_orderby フィルタで ORDER BY 節を変更したいところですが、このフィルタが呼ばれるのは、先ほどのソースの一番上のコメント部分「 ■order は ASC か DESC じゃなければ DESC になります」より後なので、複数パラメータを与えても既に DESC に変えられてます。
なので、ここでは別のパラメータ myorder を創作してそれに複数 order を指定するようにします。以下、パラメータの与え方。

// 例)メタキー my_meta の値で降順  --> 日付で降順にする場合
$args = array(
    'meta_key' => 'my_meta',
    'orderby' => 'meta_value_num date',
    'myorder' => 'DESC DESC',  // ■創作パラメータ
   // 'suppress_filters' => false,  // ■素の get_posts() で使う場合はこれも必要!
);
query_posts( $args );
 :
フィルタの処理

以下を functions.php に追加すれば OK です。

add_filter( 'posts_orderby','my_posts_orderby', 10, 2 );
function my_posts_orderby( $orderby, $query ) {
    $orders = array_filter( explode( ' ', strtoupper( $query->get( 'myorder' ) ) ) );
    if ( 0 < count( $orders ) ) {  // ■myorder にパラメータが設定されている場合:
        $orderby_array = array();
        foreach ( explode( ',', str_replace( ' DESC', '', $orderby ) ) as $i => $the_orderby ) { // ■最後の DESC を取って半角カンマで項目を分割
            if ( ! isset( $orders[$i] ) || ! in_array( $orders[$i], array( 'ASC', 'DESC') ) ) // ■ASC, DESC 以外の場合:
                $orderby_array[] = $the_orderby . ' DESC' ; // ■DESC にする
            else // ■ASC または DESC なら:
                $orderby_array[] = $the_orderby . ' ' . $orders[$i];  // ■項目の直後に並び順を追加
          }
          $orderby = implode( ',', $orderby_array ); // ■並び順が追加された項目達を連結
    }
    return $orderby; // ■いってらっしゃい!
}

myorder にパラメータが設定されていなくても、myorder にパラメータが設定されている場合にいっちゃう不具合を修正しました。

動作確認バージョン
  • WordPress 3.1.3

4 Comments

  • 【ブログ】query_posts, get_posts で複数の order 指定に対応する http://bit.ly/rfxbnb #wordpressjp

  • malkoski malkoski

    有用な記事ありがとうございます。
    漢字交じりの投稿のタイトルを五十音順に並べるのに苦慮していましたが
    このページのコードを参考にする事で解決できました。
    あなた天才です!!

  • query_posts, get_posts で複数の order 指定に対応する  |  wpxtreme http://t.co/3T2bm067

  • orderby