WordPress ループ&クエリーのモヤモヤを解消しよう!

12月25日まで毎日ブログをつないでいく WordPress Advent Calendar、12日目担当 福山カズヒデ (@kzxtreme) です、こんにちは。aka aus Cothodyntomo(@aka_aus_pd)さんの「錆びたブログをまた動かすときに必要な事柄」からバトンをいただきました!
WordPress 都市伝説のひとつ「query_posts() は危険」のデマは、地道な「wp_reset_query() を使おうキャンペーン」によりかなり沈静化しましたが、それでもなお「get_posts() 以外は信用ならん」「WP_Query 最強!」と各派閥がシノギを削る争いを繰り広げています。ラブ&ピースなクリスマスを迎えるために、WordPress の最も基本的な機能でありながら最も正しく理解されていないこの最大のモヤモヤを、今日ここでスッキリと解消してしまいましょう!

複数ループ

index.php などで

while( have_posts() ) : the_post();
    /* do stuff */
endwhile;

のように WordPress ループ(メインループ)の処理をいきなり書けるのは、テンプレートファイルが実行される時点ですでに「サイトにアクセスされた URL からクエリ変数を抽出し、それらの条件に一致する記事オブジェクトを取得している」からです。

そして、例えばホーム(トップページ)やサイドバーなどで、メインループとは別の条件で記事を取得(,出力)するループ処理がある場合、それらを複数ループ(Multiple Loops)と言います。
ここでよくある不具合が

  • サイドバーやフッターで条件分岐タグが正しく判定されない。
  • ページネーションが正しくできない。
  • $post を使う関数の結果が正しくない。

などなど。。。エラーにならないけど値が変わっている、動作がおかしい、WordPress は不安定?と、謎現象として長らく恐れられています。

グローバル変数の扱いを理解しよう

複数ループは不安定、危険などと言われる原因は、ループ&クエリーでのグローバル変数の扱いが十分に理解されていないことにあります。

以下は、ループ&クエリーに関係のある主なグローバル変数です。

$wp_the_query ..... WP_Query オブジェクト。URL のクエリーバックアップ用。
$wp_query ......... WP_Query オブジェクト。カレントのクエリー。
$posts ............ クエリーを実行して取得した記事オブジェクトの配列。
$post ............. カレントの記事オブジェクト。

そして、これらのグローバル変数を上書きする関数は以下の通り。

query_posts() ........................ $wp_query を上書き。
the_post(), WP_Query::the_post() ..... $post を上書き。

テンプレートファイル内では $wp_query, $posts, $post はグローバル変数として宣言されているので、これらも何らかの値を代入すれば上書きされます。

さらに、上書きされたグローバル変数を復元する関数があります。

wp_reset_query() ........ $wp_query を $wp_the_query, $post を $wp_query->post で復元。
wp_reset_postdata() ..... $post を $wp_query->post で復元。

例えば is_category() などの条件分岐タグや posts_nav_link() などはグローバル変数の $wp_query を元にしています。「メインループの状態が欲しいのにグローバル変数を上書きしたまま復元していない」「グローバル変数を復元した後に複数ループ側の状態を取得している」といった現在のグローバル変数の状態を正しく認識していないことが、複数ループにまつわる問題の原因です。

正しい書き方

以下は、query_posts(), get_posts(), WP_Query() 各々についての、現在のところ副作用が無く不具合を作り込み難い書き方です。なお、いずれの関数/クラスも内部では最終的に WP_Query::get_posts() を実行しています。

query_posts()
$arg = array( 
    /* 省略 */ 
    'paged' => get_query_var( 'paged' ), // ページネーションするなら必須
);
query_posts( $args );
    while ( have_posts() ) : 
        the_post();
        /* do stuff 
           the_title(), the_permalink() 等使用可
        */
    endwhile;
    /* 必要なら endwhile 〜 wp_reset_query() の間でページネーション */
wp_reset_query();

グローバル変数の $wp_query, $post を使うテンプレートタグ/関数がそのまま利用できるので便利です。ちなみに

query_posts();
  /* */
query_posts();
  /* */
wp_reset_query();

のように複数の query_posts() の最後に wp_reset_query() しても問題ありませんが、気持ち悪いのでやめましょう。

get_posts()
$arg = array( 
    /* 省略 */ 
);
$my_posts = get_posts( $args );  //  $posts = とは書かない
    global $post;  // テンプレートファイル内なら書かなくても良い
    foreach ( $my_posts as $post ) : 
        setup_postdata( $post );
        /* do stuff 
           the_title(), the_permalink() 等使用可
        */
    endforeach;
wp_reset_postdata();

$posts = と書かないのは、$posts を復元する関数が用意されていないからです。他で使用されるフシが無く、

$GLOBALS['posts'] = & $wp_query->posts;

で復元すれば良さそうですが、余計なことはしない方が得策です。また、グローバル変数の $post を使用せず $my_post のようにも書けますが、the_title() などのテンプレートタグはグローバル $post 専用のため、echo get_the_title( $my_post ) のように書く必要があります。

ついでに get_posts(), get_children()query_posts(), WP_Query()の WordPress ループの内側で入れ子ループとしても利用できます。そういえばメインループから見るとここで挙げている3つの関数/クラスはどれも二重ループの内側になりますね。

ただし query_posts(), get_posts(), WP_Query() の同じ関数/クラスでループを入れ子にするのはやめましょう。その必要はまずありませんし、労多くして得るもの少なしです。

WP_Query()
$arg = array( 
    /* 省略 */ 
);
$my_query = new WP_Query( $args );
    while ( $my_query->have_posts() ) : 
        $my_query->the_post();  // global の $post を上書きする
        /* do stuff 
           the_title(), the_permalink() 等使用可
        */
    endwhile;
wp_reset_postdata();

$my_query->the_post() がグローバル変数の $post を上書きすることに注意しましょう。これよりは query_posts() の方が素直で使い易いかな、と思います。

クエリーの改変

もしメインループとは別のループ、ではなく、メインループそのものを変更したいだけなら pre_get_posts アクションを使うと良いでしょう。WordPress はメインループ用に URL からクエリ変数を抽出し、条件に一致する記事オブジェクトを取得していますが、これにも WP_Query::get_posts() が使用されています。

pre_get_posts アクションは、WP_Query::get_posts() の内部でオリジナルのクエリーからクエリ変数や条件分岐タグ用の変数を設定した後かつ記事オブジェクトを取得する前に実行されるため、元のクエリーを判定して新たなクエリーを与えることができます。複数ループにする必要がなくメインループだけで完結するので、クエリ実行のコストが節約できますね。

例)カテゴリーアーカイブは1ページ20件で日付の昇順にする場合

テーマフォルダの functions.php に以下を追加します。

add_action( 'pre_get_posts', 'my_pre_get_posts' );
function my_pre_get_posts( $query ) {
    if ( ! is_admin() && is_category() ) {
        $query->set( 'posts_per_page', 20 );
        $query->set( 'order', 'ASC' );
    }
}

! is_admin() の条件が無いと、管理画面でも有効になります。

CONCLUSION

謎めいた現象に見えても、プログラムは書いた通りにしか動いていないのでした。仕組みがわかりさえすればとてもシンプルなことでしたね。

明日は @shinichiN さんです!お楽しみに!

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

65 Comments

  • 【ブログ】WordPress ループ&クエリーのモヤモヤを解消しよう! http://t.co/84EXs67i #WordPressAdvent2011 @wordpress_fan #wordpressjp

  • 「グローバル変数の上書き」が平然と行われている状況(さらにplugin開発者が上書きすることを見越して「上書きされたグローバル変数を復元する関数」が用意されてる状況)って果たしてどうなの? と正直思わんでもない

  • WordPress Advent Calendar http://t.co/7DTa3gCt 12日目! WordPress ループ&クエリーのモヤモヤを解消しよう! http://t.co/TqboZLen via @kzxtreme さん。明日は @shinichiN さん!

  • WordPress ループ&クエリーのモヤモヤを解消しよう! http://t.co/5nlQtgBX via @kzxtreme

  • 【ブログ】WordPress ループ&クエリーのモヤモヤを解消しよう! http://t.co/84EXs67i #WordPressAdvent2011 @wordpress_fan #wordpressjp

  • WordPress Advent Calendar 12日目 – WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme http://t.co/9r3VWJG6 #wordpressjp

  • WordPress Advent Calendar 12日目 – WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme http://t.co/9r3VWJG6 #wordpressjp

  • shinichin shinichin

    【ブログ】WordPress ループ&クエリーのモヤモヤを解消しよう! http://t.co/84EXs67i #WordPressAdvent2011 @wordpress_fan #wordpressjp

  • “WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme” http://t.co/kyDCTzdr

  • “WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme” http://t.co/kyDCTzdr

  • “WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme” http://t.co/6b7DmvLT

  • “WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme” http://t.co/6b7DmvLT

  • WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme 12月25日まで毎日ブログをつないでいく WordPress Advent Calendar、12日目担当 福山カズヒデ (@kzxtrem… http://t.co/7xnJ749P

  • WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme http://t.co/XsDgM4MM

  • WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme http://t.co/h96qM4lE

  • WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme http://t.co/xG0UrjWH #wordpress

  • “WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme” http://t.co/kyDCTzdr

  • WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme: 12月25日まで毎日ブログをつないでいく WordPress Advent Calendar、12日目担当 福山カズヒデ (@kzxt… http://t.co/zkXNBKaQ

  • [*wordpress] / “WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme” http://t.co/fqc7QBXN

  • WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme http://t.co/pcvKwaUV #PHP

  • WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme http://t.co/XsDgM4MM

  • WordPress ループ&クエリーのモヤモヤを解消しよう! http://t.co/oyZTP1iY via @kzxtreme

  • “WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme” http://t.co/RCAEYF22

  • 最近 get_posts() が怪しいとか思ってたんだけど、この記事に書いてあるとおりにして試してみる。 ”WordPress ループ&クエリーのモヤモヤを解消しよう!” http://t.co/vchiJ8KX @kzxtremeさんから

  • ラブ&ピースなクリスマスを迎える為のTips> WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme http://t.co/32dEKoX2

  • ラブ&ピースなクリスマスを迎える為のTips>

  • queryに対しての挙動など 複数ループするときなどに確認する

  • ラブ&ピースなクリスマスを迎える為のTips> WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme http://t.co/32dEKoX2

  • WordPress ループ&クエリーのモヤモヤを解消しよう! | wpxtreme http://t.co/CYOZs2qe

  • WordPress ループ&クエリーのモヤモヤを解消しよう! http://t.co/WdTOtGJM @kzxtremeさんから

  • WordPress ループ&クエリーのモヤモヤを解消しよう! http://t.co/zBxy2IHw

  • 「 query_posts() は危険」デマに見事に踊らされていたんだぜ!\(^o^)/

  • 「 query_posts() は危険」デマに見事に踊らされていたんだぜ!\(^o^)/ http://t.co/hsLTFu3s

  • WordPress ループ&クエリーのモヤモヤを解消しよう! http://t.co/PI5iWsvj

  • WordPress ループ&クエリーのモヤモヤを解消しよう! http://t.co/nscXlqO5 @kzxtremeさんから WP扱うなら読んどくべき

  • “WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme” http://t.co/NLec3YDS

  • WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme http://t.co/4WC7I97h

  • “WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme” http://t.co/mLAjZGQP

  • WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme http://t.co/U9HlfnVP

  • @HissyNC いえいえ。個人にこの記事と @jim0912 さんの記事 http://t.co/2m1HJxj4 と @kzxtreme さんの記事 http://t.co/RoOCStYD でクエリ周りは何とかなりそうです。

  • extreme wp_reset_postdata() でググって出てきましたm(__)m。 http://t.co/g0dNQ3Mn RT @jim0912: @shinichiN ループの中でループするときに使える。カズさんがブログ書いてたよ。

  • wordpress ループ 必読

  • query モヤモヤ で検索! → 「WordPress ループ&クエリーのモヤモヤを解消しよう!」 http://t.co/g0dNQ3Mn #wordpressjp

  • query モヤモヤ で検索! → 「WordPress ループ&クエリーのモヤモヤを解消しよう!」 http://t.co/g0dNQ3Mn #wordpressjp

  • query モヤモヤ で検索! → 「WordPress ループ&クエリーのモヤモヤを解消しよう!」 http://t.co/g0dNQ3Mn #wordpressjp

  • WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme http://t.co/hChwB992 #wordpress #wp

  • [wordpress] / “WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme” http://t.co/Md1eoBPj

  • かずさん、こんにちは!いつもありがとうございます。

    正しい書き方>query_posts のところなのですが、3行目の

    ‘paged’ => get_query_var( ‘paged’ );
    の最後のコロンがエラーになるようです。

    いつも「query_posts モヤモヤ」でググってここにやってくる私の為に直してくださいと言っているわけではありませんw

    • kz kz

      しんいちさん、こんにちは!
      そこはコンマでしたね、ぎゃふん(><)
      ご指摘ありがとうございました、こっそり直しておきました◎

  • [自動はてブ棚卸し] WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme http://t.co/C9my4GNH

  • “WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme” http://t.co/vloTfQH3

  • 結局どれ使えばいいんだうぼあー。
    WordPress ループ&クエリーのモヤモヤを解消しよう! http://t.co/7Pjl3VfP @kzxtremeさんから

  • 結局どれ使えばいいんだうぼあー。 WordPress ループ&クエリーのモヤモヤを解消しよう! http://t.co/7Pjl3VfP @kzxtremeさんから

  • wordpressループ(WP_Query / query_posts / get_posts)処理に困ったときに読む!めっちゃわかりやすい

  • query_posts get_posts WP_QUERY

  • ループ用のパラメータを排除して、グローバル変数に格納するWordPressの方針がすごく嫌なんだよね…。

  • 前も見た記事だが今の方が意味わかった。 $wp_queryとWP_Queryは名前が似てるがWP_Queryは$postと紐づいてるのか。 WordPress ループ&クエリーのモヤモヤを解消しよう!  |  wpxtreme