WordPressでページ送りが動かないのはどう考えてもquery_postsが悪い!【pre_get_posts、WordPressループまとめ】
Posted by admin at 8:39 日時 2013/07/15
とか、元ネタありのパロタイトルで技術情報を書くのが余計なんだろうなぁと思います、おはようございます。最近はconcrete5とRWDの記事が多かったのですが、久々のWordPressネタです。WordBench神戸でセッションを持たせてもらいました、「これからのpre_get_postsの話をしよう」の補足記事です。なお、WordPress3.5.2時点での情報になりますので、今後のバージョンアップで変更がある場合がありますのでご了承ください。
このセッションは、過去にquery_postsを捨てよ、pre_get_postsを使おうという記事を書いたところ、query_postsを使っていた方からの不安の声が噴出したり、フォーラムでpre_get_postsの使い方を質問する方が複数この記事へのリンクを貼っていたりしたため、これはどこかできちんと説明しないといけないなと思い、WordBenchでのセッション枠をいただき解説を行ったものです。
7/13 WordBench神戸(テーマ祭りとpre_get_posts)のふりかえり
目次
スライド資料
基本的に資料だけで完結するようにテキストメインで作りました。WordPressの動作原理の説明のところでアニメーションを使っているのがPDFでは再現されないのが残念ではありますが。
ポイントとしては、query_postsはそもそもメインクエリーを改変する(主には、表示件数を変更したりといったアーカイブの表示設定の変更が)用途を想定されていたのに、それを越えてサブクエリーを発行するのに非常に多く使われているというところかなと思います。そのため、使う人が想像もしないような副作用がある。そのため、推奨されない流れになっているのだと思います。
サンプルコード
原理まで分かっていなくても使えるように、使いまわせるサンプルコードを資料に含めました。転載になりますので、解説は資料をご覧ください。
pre_get_posts基本文法
function 関数( $query ) { if ( is_admin() || ! $query->is_main_query() ) return; if ( クエリーの改変を適用する条件 ) { $query->set( 'パラメーター', '値' ); return; } } add_action( 'pre_get_posts', '関数名' );
pre_get_postsの書き方はまだこれという定型パターンが定まっていないようで、ブログ記事やCodexでのサンプルコードもまちまちですが、色々考えてこの書き方が一番使いやすいと思います。
まず、冒頭の「if ( is_admin() || ! $query->is_main_query() ) return;」が管理画面とメインクエリー以外に影響を与えない定型文です。メインクエリーのチェックを、他の条件分岐と併用して書いているコード(例えば「if ( is_category() && !is_main_query()」のようなコード)もありますが、PHP初心者の方は「||(論理和)」や「&&(論理積)」は使いにくいと思いますので、このように冒頭で定型文でチェックしたほうがコピペできて良いと思います。
次に、「if ( クエリーの改変を適用する条件 ) { }」でメインクエリーへの変更を適用する条件を書いています。複数の条件を書くことを想定して「return;」を含めています。これを書かないと、条件分岐を理解して書き分けないと、複数の条件指定が実は重なっていたということが起こる可能性があります。事例:【WordPress】pre_get_postsを使ってハマりました。 | ht79.info
パラメーターと値の組み合わせは、Codexの関数リファレンス/WP_Queryのページを参照ください。
pre_get_postsで使える条件分岐サンプル
アーカイブか?
if ( $query->is_archive() )
ポストタイプアーカイブか?(引数はポストタイプ名,または配列)(ラベルではない)
$query->is_post_type_archive( $post_types )
著者アーカイブか?(引数は著者ID,ニックネーム,表示名,またはそれらの配列)
$query->is_author( $author )
カテゴリーアーカイブか?(引数はカテゴリーID,スラッグ,名前,またはそれらの配列)
$query->is_category( $category )
タグアーカイブか?(引数はタグスラッグ,またはその配列)
$query->is_tag( $slug )
タクソノミーアーカイブか?(引数はタクソノミーのスラッグと、タームのID,名前,スラッグ,またはそれらの配列)
$query->is_tax( $taxonomy, $term )
日付アーカイブか?
$query->is_date()
フィードか?(引数はフィードの種類)
$query->is_feed($feeds)
固定ページか?(引数はページID,タイトル,スラッグ,またはそれらの配列)
$query->is_page( $page )
検索結果か?
$query->is_search()
投稿か?(引数は投稿ID,タイトル,スラッグ,またはそれらの配列)
$query->is_single( $post )
どの投稿タイプのシングルか?(引数は投稿タイプ,またはその配列)
$query->is_singular( $post_types )
404ページか?
$query->is_404()
メインページから特定のカテゴリー(IDが1と1347)を除外する
function exclude_category_at_home( $query ) { if ( is_admin() || ! $query->is_main_query() ) return; if ( $query->is_home() ) { $query->set( 'cat', '-1,-1347' ); return; } } add_action( 'pre_get_posts', 'exclude_category_at_home' );
検索結果から特定のカテゴリーを除外する
function search_exclude_cat_1( $query ) { if ( is_admin() || ! $query->is_main_query() ) return; if ( $query->is_search() ) { $query->set( 'category__not_in', array(1) ); return; } } add_action( 'pre_get_posts', 'search_exclude_cat_1' );
検索結果から固定ページを除外(投稿のみ許可)
function search_only_post( $query ) { if ( is_admin() || ! $query->is_main_query() ) return; if ( $query->is_search() ) { $query->set( 'post_type', 'post' ); return; } } add_action( 'pre_get_posts', 'search_only_post' );
条件にしたがって表示件数を変更
function set_post_per_page( $query ) { if ( is_admin() || ! $query->is_main_query() ) return; if ( $query->is_home() ) { $query->set( 'posts_per_page', 1 ); return; } if ( $query->is_post_type_archive( 'movie' ) ) { $query->set( 'posts_per_page', 50 ); return; } } add_action( 'pre_get_posts', 'set_post_per_page');
get_posts記述例
global $post; $args = array( 'posts_per_page' => 5, 'cat' => 1 ); $myposts = get_posts( $args ); foreach( $myposts as $post ) { setup_postdata($post); ?> <h1><?php the_title(); ?></h1> <?php } wp_reset_postdata();
「global $post;」が必要なのが気持ち悪いなーと思いますが入れないと不具合があるそうなのですね。今後の改善に期待です。この微妙な使いにくさが改善されないと、実際query_postsの方が初心者には書きやすいんですよね…。
@HissyNC excerptだけ不具合が出るねー。メインクエリーのURLになっちゃう。。。
— Takayuki Miyauchi (@miya0001) July 13, 2013
WP_Query記述例
サブクエリーの発行で一番不具合が出ず確実なのは個人的にはこの書き方だと思います。ただ、get_posts関数ではサブクエリー用にチューニングされているパラメーターの多くがデフォルト設定のままなので、パラメーターの数が多くなってしまうのがちょっと初心者には取っつきにくいかもしれません…。
$args = array( 'posts_per_page' => 5, 'offset' => 0, 'cat' => 0, 'orderby' => 'post_date', 'order' => 'DESC', 'post_type' => 'post', 'post_status' => 'publish', 'suppress_filters' => true, 'ignore_sticky_posts' => true, 'no_found_rows' => true ); $the_query = new WP_Query( $args ); if ( $the_query->have_posts() ) { while ( $the_query->have_posts() ) { $the_query->the_post(); ?> <h1><?php the_title(); ?></h1> <?php } } wp_reset_postdata();
ただ、パラメーターの多さに目をつむれば、通常のWordPressループと似ているので、分かりやすいかも。お好みでどうぞ。
Twitterでの反応
英語ですが、http://t.co/RGhaJndFfh も手助けになるかと… #wbkobe
— Yoshiyuki Kato (@tyataro) July 13, 2013
硬派ですよ。 #wbkobe
— ユカペコ ヽ(・ω・)/ズコー (@yuka2py) July 13, 2013
うおお、resetはそういう働きだったのか。硬派ひっしー、めちゃ分かり易い!! #wbkobe
— はりー (@harryxhappy) July 13, 2013
メインクエリーはpre_get_posts、サブクエリーはget_posts。#wbkobe
— 額賀順子 (@nukaga) July 13, 2013
メインクエリとサブクエリで皆さん混乱中。プログラマだとすんなりわかるとこだけど、おかしな挙動をするよとかそういうことじゃないとこで混乱されてるのかなぁ… #wbkobe
— Yoshiyuki Kato (@tyataro) July 13, 2013
Hissyさんのお話の分かりやすさに感動。すごく重要なとこの霧が晴れた上に、パワーまで補充できた! #wbkobe
— KLOOM あゆみ (@k_l_m_a_y_m) July 14, 2013
まとまりきっていなかったところもあったと思いますが、ありがとうございました。
いただいた質問
pre_get_postsでもquery_postsと同じパラメーターが使えますか?
はい、使えます。というか、pre_get_posts, query_posts, get_postsの全てで内部的にはWP_Queryクラスのオブジェクトが使われていますので、使えるパラメーターは全く同じです。
今までの案件でquery_postsを使っていましたが、pre_get_postsで書きなおした方がいいですか?
WordPressは後方互換性を重視していますし、query_posts関数自体はまだ非推奨にはなっていません。ただ、トラブルが多いので他の方法を推奨するように切り替えていこう、ということです。正しく動作していれば、過去のテーマを急いで修正する必要はありません。
WebデザインスクールでWordPressを教えていますが、教える内容をpre_get_postsに変えたほうがいいですか?
結論としては、教材に合わせるという形で問題ないと思います。
pre_get_postsフィルター自体は昔からあったのですが、メインクエリーの改変に使うという用途ではありませんでした。query_postsの代替として使用しやすくするため、is_main_query() 関数が導入されたのがバージョン3.3ですので、比較的新しいテクニックです。そのため、多くのWordPressの技術書ではサブクエリーの発行にquery_postsを使用していると思いますが、即使えなくなるということではなく、あまりオススメされないテクニックになったということです。教材としてお使いの書籍でquery_postsを使用していたとしても、当面は問題なく動くと思います。
ただし、今後はCodexのサンプルコードでquery_postsを使用しているところはpre_get_postsを使った書き方に改められていくと思いますし、ブログ記事などで紹介されるサンプルコードもそれに伴って置き換わっていくでしょう。その時のために、説明はできるようにしておいたほうがいいのかなと思います。
functions.phpに書くってのは、初心者にはハードルが高いのではないか?
確かに少し難しいですね。ビューとコントローラーを分けるという発想よりも、ビューに全て書いてしまう方が把握しやすい。ただ、query_postsが引き起こす副作用はそれ以上に難解なので、初心者には分からないんじゃないかな…と。
query_postsからpre_get_postsへの移行方法
コードはCodex/Pagination#Removing query_posts from the main loopからの転載ですが、一部変更しています。例えば、このようなコードをindex.phpとcategory.phpで記載していたとします。
<?php // 表示件数を3件に設定 $paged = (get_query_var('paged')) ? get_query_var('paged') : 1; $args = array('posts_per_page' => 3, 'paged' => $paged ); query_posts($args); ?> <!-- ループ --> <?php if ( have_posts() ) : while (have_posts()) : the_post(); ?> <!-- ループの中身 --> <!-- タイトル、本文など… --> <?php endwhile; ?> <!-- ページ送り --> <?php next_posts_link(); ?> <?php previous_posts_link(); ?> <?php else : ?> <!-- 投稿がなかった時 --> <?php endif; ?>
query_postsの部分をテンプレートから削除します。
<?php // 表示件数を3件に設定 $paged = (get_query_var('paged')) ? get_query_var('paged') : 1; $args = array('posts_per_page' => 3, 'paged' => $paged ); query_posts($args); ?>
テーマのfunctions.phpに下記の記述を追加します。
function my_post_queries( $query ) { if ( is_admin() || ! $query->is_main_query() ) return; // ホームとカテゴリーアーカイブで表示件数を3件に設定 if ( is_home() ) { $query->set('posts_per_page', 3); return; } if ( is_category() ) { $query->set('posts_per_page', 3); return; } } add_action( 'pre_get_posts', 'my_post_queries' );
以上です。
固定ページを他の用途に使うのはやめよう
query_postsの間違った使い方として、サブクエリーの発行に使用するという以上に、「固定ページを何かの一覧にする」というテクニックに使用されることが多いと思います。例えば、プレスリリースの一覧を作りたいという場合に、「press」というカテゴリーを作成。URLは「/category/press」の様になりますが、これを固定ページ「/press」を流用してプレスリリース専用の一覧を作るためにquery_postsを使う。そして、アーカイブでは「press」カテゴリーを除くためにまたquery_postsを使う。こういうカスタマイズは古いテクニックだと思います。
現在のWordPressでは、カスタム投稿タイプを使うのが王道です。「press」というカスタム投稿タイプを作成すれば、カスタム投稿タイプアーカイブが自動的に作成されますし、投稿のアーカイブから特定のカテゴリーを除く必要もありません。
固定ページテンプレートを使ってカスタムクエリのアーカイブを作成するというテクニックはカスタム投稿タイプ、カスタムタクソノミーが無かった時代のものかなと思います。実際、固定ページをアーカイブに変更するカスタマイズをきちんと実装するのは意外と難しいです。また、この手法をpre_get_postsを使ってやるのは、そもそも無理っぽいです。固定ページを他の用途に変更することは、今のところ想定されていないようです。
WordPressでメインクエリ以前にもロード済みのデータがあることについて
どうしても固定ページでアーカイブを作りたいんですと言う場合
一応できますし問題なく動きますので、サンプルを掲載しておきます。WP_Queryを使うのが一番いいと思います
雑感
query_postsからpre_get_postsに切り替えていきましょう、という話自体は、自分の記事以前にも紹介される方がありました。
おそらく自分の書いた記事のタイトルが「捨てよ」なので、ちょっとキャッチーすぎたのかなと…。実際には、すぐさま捨てる必要はないと思いますが、WordPressコミュニティが議論の末に決定した方針ですので、query_postsを使い続けることにはあまりメリットは無いかなと思われます。今後の案件では、ぜひquery_postsを使わないことにチャレンジしてみてはいかがでしょうか。
WordPressはPHP初心者にも簡単にカスタマイズできると思われている傾向はあると思いますが、徐々に本当の初心者(PHPは全く触りたくない)に優しくなり、テーマカスタマイザーなどが整備され、逆にPHP初心者にはハードルが高くなりつつあるのかなと思いますね。
最後に、記事タイトルの元ネタたちに敬意を表して。
私がモテないのはどう考えてもお前らが悪い! (4) (ガンガンコミックスONLINE)
- 著者/訳者:谷川 ニコ
- 出版社:スクウェア・エニックス( 2013-06-22 )
- コミック:137 ページ
- ISBN-10 : 4757539800
- ISBN-13 : 9784757539808
- 定価:¥ 514
私がモテないのはどう考えてもお前らが悪い!(1) (ガンガンコミックスONLINE)
- 著者/訳者:谷川 ニコ
- 出版社:スクウェア・エニックス( 2012-01-21 )
- コミック: ページ
- ISBN-10 : 4757534809
- ISBN-13 : 9784757534803
- 定価:¥ 514
これからの「正義」の話をしよう――いまを生き延びるための哲学
- 著者/訳者:マイケル・サンデル
- 出版社:早川書房( 2010-05-22 )
- 単行本:384 ページ
- ISBN-10 : 4152091312
- ISBN-13 : 9784152091314
- 定価:¥ 2,484
- 著者/訳者:寺山 修司
- 出版社:角川書店( 2004-06-25 )
- 文庫:332 ページ
- ISBN-10 : 4041315220
- ISBN-13 : 9784041315224
- 定価:¥ 555