Blog

query_postsを捨てよ、pre_get_postsを使おう【追記あり】【報告あり】

Posted by admin at 8:27 日時 2013/03/25

[2013-07-15追記] より詳しい補足記事を書きました。

WordPressでページ送りが動かないのはどう考えてもquery_postsが悪い!【pre_get_postsまとめ】


Thrown In The Trash

WordPressのテンプレートをカスタマイズしようとして高確率でハマったり事故ったりするのが query_posts 関数というやつでして、ぐぐってみたらこの1年以内にも query_posts の使い方を「WordPress使うなら必須知識!」として解説したり、いまだに $paged を引数で渡さなきゃいかんとか、書いてあるブログ記事もたくさん見つかりまして頭痛が痛くなります。この際、はっきり言っておきましょう。

もう query_posts は一切使う必要ありません。

いやまあ、かく言うワタシも迷っていた時期がありまして。でも周りのWPerにquery_posts要らないらしいよ?という話を聞くたびにホントかな?と思って色々調べたりコードを読んだりした結果、今では一切使っていないのですが、そういえばCodexでどう書いてあるのかなと確認してみたら、もう冒頭から「お前らこれ使うなよ?絶対だぞ?使ってもいいことないぞ?」と書いてあるわけですね。

query_posts() is the easiest, but not preferred or most efficient, way to alter the main query that WordPress uses to display posts.

It is strongly recommended that you use the pre_get_posts filter instead, and alter the main query by checking is_main_query

以前はここまで使うなよってニュアンスの書き方じゃなかった気がするんですが。だいたいのっけからページ送りで問題が起こるサンプルコードを載せているあたり、徹底しています。「それでも使うんなら、俺知ーらね」って感じです。さっそく喜び勇んで日本語版のCodexにもモリッと反映いたしました。

そもそもquery_postsはメインクエリーを改変するために存在する関数で、バージョン1.5のころからある古株です。当時のWordPressのコアに入っていたコードはこんな感じ。

function &query_posts($query) {  	global $wp_query;  	return $wp_query->query($query);  }

単にグローバル変数に格納されているクエリーに引数をつけて呼び出しているだけですね。これをやってしまうと、条件を追加する前の状態に戻せなくなっちゃう!アナタどうしてくれるのよ!ということで、途中でコアの記述はこんなふうに変わりました。いったんメインクエリーを消しちゃって、新しくデータベースからデータを取得しよう。PHPのunsetとは、データを消しちゃうって意味です。

function &query_posts($query) {  	unset($GLOBALS['wp_query']);  	$GLOBALS['wp_query'] =& new WP_Query();  	return $GLOBALS['wp_query']->query($query);  }

そして、横においてあったメインクエリーを戻してきて、テンプレートタグの内容をもとに戻す関数を作ろうということになりました。wp_reset_queryがコアコードに登場したのが2.3からです。

function wp_reset_query() {  	$GLOBALS['wp_query'] = $GLOBALS['wp_the_query'];  	wp_reset_postdata();  }

要するに、「query_postsが呼ばれてからwp_reset_queryが呼ばれるまでの間は完全に別世界」ということです。ページテンプレートで投稿を表示することもできます。カテゴリーアーカイブでタグアーカイブを表示することもできます。いやいや、そこまでする必要はなくて、ちょっと件数を変えたり特定のカテゴリーを除外したりしたいだけなのよ、と言ってもお構いなしです。

そして、もとに戻すことを忘れる人が多数発生しました。あと、もともとあったクエリーを完全に消してイチから作っているのに、「ページ送りが効かなくなった!その他もろもろ、なぜか引き継がれない!」と言う人もたくさん発生しました。

さらには、メインクエリーを改変するための関数なのに、派生的なクエリーを取得するのに使う人もたくさんいました(派生的とは、記事ページの下に同じカテゴリーの最新記事を5件表示するとか、そういうの)。だって、新規にクエリーを作っているのだから、そう使っていいように見えますよね。派生的なクエリーの作り方に関しては、query_postsとget_postsの違い[追記あり]も参考にしてください。

結局、WordPressは画面全体がひとつのモジュールなので、何か変更を行ったら画面全体に予期せぬ影響を及ぼす可能性があるのです。concrete5など画面内の各パーツがそれぞれにモジュール化されている場合は、起こらないことですが、これはブログベースのシステムの特性かもしれません。

そんなこんなで、フォーラムにもたびたび投稿されるquery_posts由来のトラブル。まさにquery_posts狂騒曲。そこに現れた福音の声。

pre_get_postsフィルターを使いましょう

pre_get_postsフィルターとは、WordPressがクエリーを取得するまえに必ず実行されるフィルターフックです。これも昔からあるのですが、WordPressがあらゆる投稿データを取得するときに通るフィルターなので、無警戒に使うと管理画面も派生的なクエリーも何もかも改変されてしまいます。このままでは使いにくいのですが、query_postsがもともと想定していた用途はメインクエリーの変更ですので、pre_get_postsフィルターでメインクエリーかどうかのチェックがかんたんにできるように修正されました。

したがって、いまではこのフィルターを使うのはかなりかんたんですし、query_postsにできてpre_get_postsフィルターでできないことはありませんし、query_postsにできてget_postsにできないこともありません。固定ページのテンプレートにquery_postsを書いて何らかのアーカイブとして使うという手法も根強く採用されているかと思いますが、それもカスタム投稿タイプの出現で過去のものです。もうquery_postsの使い方を覚える必要はないですし、一切使う必要もありません。めでたしめでたし。

では、pre_get_postsフィルターの使い方。これらのコードは基本的にfunctions.phpに書くとおぼえてください。

例:アーカイブで1ページに表示される件数を5件にする(管理画面の設定を上書きする)

function change_posts_per_page($query) {  	if ( is_admin() || ! $query->is_main_query() )  		return;    	if ( $query->is_archive() ) {  		$query->set( 'posts_per_page', '5' );  	}  }  add_action( 'pre_get_posts', 'change_posts_per_page' );

冒頭のこの記述がポイントです。

if ( is_admin() || ! $query->is_main_query() )  	return;

基本毎回コピペでいいと思います。管理画面、またはメインクエリーでない場合は、ここで処理を中止してそれ以降のコードは実行されないようにします。これを書かないと、管理画面の一覧も、サイドバーのウィジェットも、全部5件ずつの表示になってしまいます。

どんな条件の時にクエリを変更するか設定

メインクエリーかどうかチェックしたあとは、条件分岐でアーカイブの時だけに限定しています。

if ( $query->is_archive() ) { ... }

その他にも、下記の条件分岐が使えます(WordPress3.5.1時点、抜粋)。

ポストタイプアーカイブか?(引数はポストタイプ名,または配列)(ラベルではない)

$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)

フロントページか?

$query->is_front_page()

固定ページか?(引数はページID,タイトル,スラッグ,またはそれらの配列)

$query->is_page( $page )

検索結果か?

$query->is_search()

投稿か?(引数は投稿ID,タイトル,スラッグ,またはそれらの配列)

$query->is_single( $post )

どの投稿タイプのシングルか?(引数は投稿タイプ,またはその配列)

$query->is_singular( $post_types )

404ページか?

$query->is_404()

クエリを設定

条件が指定できたら、いよいよメインクエリーを変更しましょう。変更といっても、query_postsのように必要なパラメーターをすべて揃える必要はありません。追加したいパラメーターをセットするだけです。基本的な書き方はこうです。

$query->set( 'パラメーター名', 'パラメーターの値' );

上のサンプルコードでは、1ページに表示する件数を5件に設定しています。

$query->set( 'posts_per_page', '5' );

どんなパラメーターが使えるかは、Codexの関数リファレンス/WP_Queryのページを参考にしてください。

クエリパラメータの値を取得

この書き方で、すでに設定されているクエリーパラメーターの値を取得することもできます。

$query->get( 'パラメーター名' );

他のサンプル

IDが2のカテゴリーを表示する際、IDが6のカテゴリーにも属している記事だけを表示する

function customize_main_query($query) {  	if ( is_admin() || ! $query->is_main_query() )  		return;    	if ( $query->is_category(2) ) {  		$query->set( 'category__and', array(2,6) );  	}  }  add_action( 'pre_get_posts', 'customize_main_query' );

検索結果から、IDが3の著者の記事を除外する

function customize_main_query($query) {  	if ( is_admin() || ! $query->is_main_query() )  		return;    	if ( $query->is_search() ) {  		$query->set( 'author', '-3' );  	}  }  add_action( 'pre_get_posts', 'customize_main_query' );

検索結果から、カスタムフィールド “exclude_search” に 1 が保存されている投稿を除外する

function customize_main_query($query) {  	if ( is_admin() || ! $query->is_main_query() )  		return;    	if ( $query->is_search() ) {  		$query->set(  			'meta_query',  			array(  				array(  					'key' => 'exclude_search',  					'value' => '1',  					'compare' => '!=',  					'type' => 'NUMERIC'  				)  			)  		);  	}  }  add_action( 'pre_get_posts', 'customize_main_query' );

ね、かんたんでしょ。ページ送りの引数を引き継がなきゃいけないとか、リセットしないと他の部分がおかしくなるとか、まったく配慮しなくてもOKです。

さらには、ユーザーやコメントの検索も、今後は似たような書き方になっていきますよ。いま覚えておかないとWordPressの世界についていけなくなりますよ〜。

function customize_user_query($query) {  	$query->set( 'exclude', array(5,6) );  }  add_action( 'pre_user_query', 'customize_user_query' );

 

function customize_comment_query($query) {  	$query->set( 'order', 'ASC');  }  add_action( 'pre_get_comments', 'customize_comment_query' );

最後にダメ押し。

そもそも、query_postsは非推奨にしようぜって意見が挙がっているようです。

WordPress 3.5 の変更点などのメモ – Waviaei

(使用すべきでないという意味で)間違った使い方をされているquery_posts()。テーマチェックのリストに使用禁止として追加しては?

個人的にはさっさと非推奨にしたらいいと思うのですが、Codexのあちこちで現在も推奨されているということで、まずはそれらの記述をquery_postsを奨めない内容に書き換えようとしているみたいです。すでに、wp_reset_query()の方にははっきりと非推奨の文字が。

さて、来年のいまごろは

まだquery_posts使ってるの?遅れてる〜

という世の中になってるかも?ですなぁ。今年出る本でquery_postsでカスタマイズしましょうみたいに書いてあったらもう古いぜ!というか今月はquery_postsバリバリのテーマを引き継いで追加機能の開発をしてたのですが正直作業してて疲れました。…はい、そろそろ文句言ってないで仕事に戻ります。

pre_get_postsエヴァンジェリストの先生方のブログ記事

3/26追記

と言う感じで、どうもwp_reset_query()の方で非推奨って書いちゃったのはコア開発チームとの連携が取れてないかも?という話が出て来ましたので、続報が分かり次第また追記していきます。

pre_get_postsも万能じゃないよねという補足記事

WP_Queryで投稿データを取得する【追記あり】

全体的に、あまりquery_postsとか使わなくても、基本的なことはプラグインとカスタム投稿タイプでカバーできるという方向に進むんじゃないかなと個人的には思っております。そのためにデベロッパーフレンドリーなAPIに変わっていっているのでは。

3/29追記

その後、ツイッターでwp_reset_query()のCodexに非推奨(Deprecated)という表記が入ったのは、実際に関数が非推奨(Deprecated Function)になったわけではないのに、言葉として強すぎるのでどうか?という意見があり…

Make WordPress Documentation のスレッドで議論があり…

Siobhan 7:35 pm on March 27, 2013

Yes, you’re correct – deprecated isn’t the correct term to use in this instance. It should read: “query_posts is not recommended and should only be used if absolutely necessary”.

Deprecated does have a strict meaning, as you say.

Codex Gardening: query_posts | Make WordPress Documentation

Codexの表記も非推奨(Deprecated)から推奨されない(Not Reccomended)に変わりました。日本語にすると似ているのですが、前者は廃止予定だから使わないほうがいい、後者は問題ないけど避けたほうが無難、くらいのニュアンスでしょうか。

関数リファレンス/wp_reset_query

WordPressの公式ドキュメントも、オープンな議論の元に作成されていて、誰でもそこに参加することができるんだなぁ、ということが分かったいい経験になりました。なかなか、オープンソースのドキュメントの整備って難しいんですよね。企業が主導で開発しているところは、企業が力を入れて作ったりもしていますが、やはりコミュニティがどうやって有益な情報を共有するかという視点が大事だなと思います。

Toruさん、Tenpuraさん、ありがとうございました。


Share this entry