OXY NOTES

WordPressの新・旧ループからカスタムクエリ・アーカイブまで徹底解説

WordPressの鬼門、ループについて理解しよう

ページネーションが動かない!
ループを変更したら管理画面までおかしくなった!
カスタム投稿タイプをアーカイブページとして使いたい!

そんな阿鼻叫喚がネットのいたるところで聞こえてきますが、これは全てWordPressのクエリやループについて理解が足りないためです。
長い記事になってしまいましたが、ページの後半にあるような複雑なハックもできるようになるのでWordPressをとことんカスタマイズしたいという方にお勧めします。


目次


非推奨になったquery_posts()を使ったクエリの変更

このサイトでも解説したのことのある、query_posts()を使ったクエリの変更。テンプレートファイルでquery_posts()を使ってメインループのクエリを変更する方法です。

今ではquery_posts()を使ったクエリの変更は非推奨になりましたが、何が良くないのかを理解する上でも使い方を解説します。

今回は例としてカスタム投稿タイプbookのアーカイブページで表示する投稿を20件に変更します。

適用されるテンプレートは「archive-book.php」とします。

<?php
$args = array(/* 配列に複数の引数を追加 */
     'post_type' => 'book', // 投稿タイプを指定
     'posts_per_page' => 20, // 表示するページ数
); ?>
<?php query_posts( $args ); // 上で指定したクエリ(問い合わせ内容)の指定 ?>
<?php if ( have_posts() ) while ( have_posts() ) : the_post(); // お決まりのループ開始処理 ?>
<?php // ここに表示するタイトルやコンテンツなどを指定  ?>
<?php endwhile; ?>
<?php wp_pagenavi_dropdown(); // #WP-PageNaviプラグイン用タグ ?>
<?php wp_reset_query(); // 忘れずにリセットする必要がある ?>

query_posts()はメインクエリを変更するためwp_reset_query()で忘れずにリセットする必要があります。

この方法の問題点はページネーションが正しくできないという点です。

そのためget_query_var()でページ数を取得してpagedに渡すことで解決します。

<?php
$paged = (get_query_var('paged')) ? get_query_var('paged') : 1;

$args = array(
     'post_type' => 'book', // 投稿タイプを指定
     'posts_per_page' => 10, // 表示するページ数
     'paged' => $paged, // 表示するページ数
);
?>

簡単に解説するとget_query_var(‘paged’)は1ページ目の時に値が無いため、1ページ目を表す「1」を代入して渡しています。

このquery_posts()を使ったメインクエリの変更は今でも問題なく動作しますが以下のデメリットがあります。

1.内部でデータベースへの問い合わせを2回している

テンプレートファイルを読み込んだ時点で既に$wp_postのクエリを取得しているにも関わらず、新しくquery_posts()で再取得するため、$wp_queryを二重で取得します。
下記のイラストのように24$wp_queryに二回変数をセットしています。

ただでさえ速度の遅くなるデータベースのアクセスを二度するため、速度の低下を招きます。

2.メインクエリに手を付けるため表示全体に影響を与える

メインクエリを改編するため、サイドメニューやフッターなどメインクエリをフラグにして表示をコントロールしているものに影響が出ます。
そのためwp_reset_query()で変更したクエリをリセットする必要があります。

こうした仕組みが原因で「ページネーションが動かない」「表示がおかしくなった」という質問が各所で巻き起こりました。

そこで登場したのがアクションフックpre_get_postsを使ったクエリの変更です。


pre_get_postsを使った新しいメインクエリの変更方法

pre_get_postsはアクションフックと呼ばれるもので、functions.phpに記述することでクエリを変更することができます。
具体的にはquery_varsでデータベースへアクセスする前に実行されるため、変更を行ってもクエリを二重で取得するようなことはありません。

またメインクエリを書き換えているのでページネーションのための値をセットしたり、クエリをリセットする必要もありません。

さらに詳しくは「Codexのpre_get_postsのページ」を見てください。

pre_get_postsの使い方

pre_get_postsは多少使い方に癖があるのですが、既に安全な雛形が作成されています。
これからのpre_get_postsの話をしよう」という有名なスライドを発表された「notnil creation weblog」さんの雛形を利用します。

function 関数( $query ) {
    if ( is_admin() || ! $query->is_main_query() )
        return;
 
    if ( クエリーの改変を適用する条件 ) {
        $query->set( 'パラメーター', '値' );
        return;
    }
}
add_action( 'pre_get_posts', '関数名' );

この関数をテーマディレクトリにあるfunctions.phpに記述することでメインクエリを変更します。

何をしているかを簡単に解説すると最初のis_admin()でダッシュボードまたは管理パネルが表示されているかどうかをチェックしています。ダッシュボードが表示されている場合はreturnとなって以降の処理を中断します。
メインクエリを変更するため、ダッシュボード等の表示に影響が出るのを防いでいます。
同じようにis_main_query()でメインクエリかをチェックしています。「!」がついているので「メインクエリではない場合」に処理を中断します。

続くif文の「クエリーの改変を適用する条件」には何のページを表示している時にメインクエリを変更するかを指定します。

具体的にはアーカイブページかどうかをチェックするには「$query->is_archive()」。カスタム投稿タイプbookのアーカイブページであるかどうかを調べるには「$query->is_post_type_archive( ‘book’ )」といった具合です。

条件分岐タグは大量に用意されているので、詳しくは「Codexの条件分岐のページ」をご覧ください。

条件に一致した場合、$query->setを使って$query_varにクエリ変数をセットしています。この$query_varのクエリ変数を利用して要求された投稿を取得するわけです。

ここで使えるパラメータも多数用意されているので詳しくは「CodexのWP_Queryパラメータのページ」を参照してください。

tax_queryなどの指定の仕方は少し複雑なので必要でしたら「過去の投稿」を参照してください。

最後にadd_action()で値をセットして終了です。

実際にpre_get_postsを使ってメインクエリを書き換える

解説だけだと分かりにくいと思うので、query_post()と同じようにクエリを変更します。

カスタム投稿タイプbookのアーカイブページで、1ページに表示する投稿数を20件にする例です。

function custom_query( $query ) {
    if ( is_admin() || ! $query->is_main_query() )
        return;
 
    if ( $query->is_post_type_archive( 'book' ) ) {
        $query->set( 'posts_per_page', '20' );
        return;
    }
}
add_action( 'pre_get_posts', 'custom_query' );

ページ送りのパラメータである「posts_per_page」に20をセットしています。

後は通常と同じようにテンプレート「archive-book.php」で投稿をループさせれば20件取得できます。
一番基本的な書き方はhave_posts()で投稿の有無を調べてthe_post()で次の投稿へ進める方法です。

<?php if ( have_posts() ) while ( have_posts() ) : the_post(); ?><!-- お決まりのループ開始処理 -->
<!-- ここに表示するタイトルやコンテンツなどを指定 -->
<?php endwhile; // end of the loop. ?>
<?php wp_pagenavi_dropdown(); ?><!-- #WP-PageNaviプラグイン用タグ -->

query_posts()の時のようにget_query_var(‘paged’)でページ数を取得したり、wp_reset_query()でクエリをリセットする必要もありません。

pre_get_postsにできないこと

すでにお気付きの方も居ると思いますが、pre_get_postsでは固定ページ(page)をアーカイブページとして利用するハックは使えません。

固定ページ(page)は、固定ページ専用のアーカイブページ、
投稿(post)は、投稿専用のアーカイブページ、
カスタム投稿タイプは、カスタム投稿タイプ専用のアーカイブページで表示する。

もしも別の一覧ページを作りたいなら「カスタム分類のアーカイブページを利用する」か、「別のカスタム投稿タイプを追加する」これがWordPressの結論です。

pre_get_postsで変更できるのは「各種アーカイブページの表示件数を変更する」といった用途です。

固定ページをアーカイブページとして利用する方法

WordPressの推奨がなんだろうが、固定ページでアーカイブページを作りたい。」という需要もあると思います。

固定ページをアーカイブページにするのは導入が容易で、専用のテンプレートファイルを選ぶのも簡単で、異なる投稿タイプを組み合わせたり、人気順に並べ替えたりといった操作もテンプレートファイル内で簡単に制御できます。

数ページだけイレギュラーなアーカイブページを作る」といった場合には手軽なため使いたくなるのが人情だと思います。

そんな時はWP_Query()を使ってクエリを取得します。
get_posts()でも動作しますが、指定するパラメータと返ってくる値がquery_post()同じなためWP_Query()のほうがお勧めです。

上と同じように固定ページにカスタム投稿タイプbook20件表示する方法です。

表示するテンプレートは「page-book.php」とします。

<?php
$paged = (get_query_var('paged')) ? get_query_var('paged') : 1;

$args = array(
     'post_type' => 'book', // 投稿タイプを指定
     'posts_per_page' => 10, // 表示するページ数
     'paged' => $paged, // 表示するページ数
); ?>
<?php $wp_query = new WP_Query( $args ); ?><!-- クエリの指定 -->
<?php while ( $wp_query->have_posts() ) : $wp_query->the_post(); ?>
<!-- ここに表示するタイトルやコンテンツなどを指定 -->
<?php endwhile; ?>
<?php wp_reset_postdata(); ?><!-- 忘れずにリセットする必要がある -->

基本的な書き方は同じで「new WP_Query()」で新しくオブジェクトを作成。
生成された$my_queryインスタンスをアロー演算子で繋いで$my_query->have_posts()で投稿の有無を判断しています。
ページネーションするにはquery_post()でメインクエリを書き換えた時と同じようにget_query_var(‘paged’)でページ数を渡す必要があります。

最後はwp_reset_postdata()で$postのリセット。wp_reset_query()ではないので注意。

テンプレートを変更する方法

固定ページをアーカイブページに使いたい一番の理由はテンプレートファイルを簡単に切り替えることができる点だと思います。実は以下の方法を使うと固定ページだけでなく、他の投稿タイプでもテンプレートを自由に変更できます。

function custom_page_template( $template ) {
	if ( is_page( 'book_archive' )  ) {
		$new_template = locate_template( array( 'book_archive-template.php' ) );
		if ( "" != $new_template ) {
			return $new_template ;
		}
	}
	return $template;
}
add_filter( 'template_include', 'custom_page_template' );

template_includeというアクションフックを利用して新しいテンプレートである$new_templateを返しています。

具体的には固定ページのスラッグがbook_archiveの場合、book_archive-template.phpというテンプレートを指定しています。
locate_templateは現在のテーマディレクトリにテンプレートファイルが存在するか調べる関数で、ない場合は「“”」を返すので存在の確認に利用しています。

テンプレートの指定が数十ページあって面倒という場合でも、ページ名に接頭辞を付けてフラグにすれば簡単にテンプレートを振り分けることができます。

またis_single()is_singular( ‘hoge’ )で条件分岐すれば、個別ページでなくとも特定の投稿だけ別のテンプレートを指定することができます。
テンプレートを変更できる」という理由だけで固定ページを利用している場合はこの方法で代用できます。


清く正しいサブループの書き方

ここでちょっと話しは逸れて、せっかくWordPress推奨のメインループについてまとめているので、サブループの書き方も合わせて解説します。

サブループは1ページに好きなだけ追加することができます。人気の投稿○件や、関連する投稿○件新しい投稿○件といった表示はこの方法で実現します。
固定ページをアーカイブページにする方法でも書きましたが、get_posts()ではなくWP_Query()のほうが扱いやすいのでお勧めです。

例によってカスタム投稿タイプbookの投稿を20件表示する例です。

<?php
$args = array(
     'post_type' => 'book', // 投稿タイプを指定
     'posts_per_page' => 20, // 表示するページ数
); ?>
<?php $wp_query = new WP_Query( $args ); ?><!-- クエリの指定 -->
<?php while ( $wp_query->have_posts() ) : $wp_query->the_post(); ?>
<!-- ここに表示するタイトルやコンテンツなどを指定 -->
<?php endwhile; ?>
<?php wp_reset_postdata(); ?><!-- 忘れずにリセットする必要がある -->

クエリのリセットにはwp_reset_postdata()を使います。
他にもnew WP_Query()の前に「$temp = $wp_query;」でクエリを保存しておいて、メインループ終了後に「$wp_query = $temp;」で元に戻す方法もあります。

結構質問をもらうのですが、サブループはページネーションできません。
そのためposts_per_pageで設定した表示件数以上を表示したい場合は専用のアーカイブページを利用する必要があります。
(初めから20ページ分取得して、javaScriptなどを利用して表示を分ける等の工夫は可能です)


カスタム投稿タイプのシングルページを別のカスタム投稿タイプのアーカイブページとして使う方法

クエリを変更することでページの役割を変更します。これにはrequestフックを使います。

このフックでカスタム投稿タイプhogeのシングルページを、カスタム投稿タイプbookのアーカイブページとして動作させます。

function alter_the_query( $request ) {
    $query = new WP_Query(); // 通常のクエリの取得
    $query->parse_query( $request ); // 条件分岐タグで使用されるすべてのis_変数を設定

    // 条件に一致した場合にクエリをセットし直す(カスタムポストタイプ以外だとpost_typeという引数は無いので注意)
    if ( isset($query->query['post_type']) && $query->query['post_type'] === 'hoge' ){
		$request['name'] = ''; // アーカイブページに変更するために個別ページ名は削除
		$request['posts_per_page'] = '20'; // ページ数の指定
		$request['post_type'] = 'book'; // ポストタイプの指定
	}
    return $request;
}
add_filter( 'request', 'alter_the_query' );

ポイントは$request[‘name’]を空にすること。
投稿名が無いことで内部的にアーカイブページとして解釈するようです。(ソースは追ってないので詳細は不明)

カスタム投稿タイプを別のカスタム投稿タイプのアーカイブページにできないか」という質問されることがあるので不可能ではないという意味で紹介します。ちなみに正規のハックでは無いのでいつ動作しなくなるかわかりません。
少し敷居が高いですが、ページ下部のカスタムアーカイブを使った方法を推奨します。

ちなみに、この場合のテンプレートはbookのアーカイブページとして機能するため「archive-book.php」となります。


カスタム投稿タイプのタイトルを利用して別のカスタム投稿タイプをカスタム分類で絞り込む

カスタム、カスタムばかり出てきてわけがわからなくなってきました。

カスタム投稿タイプhogeのシングルページhugaで、カスタム投稿タイプbookのカスタム分類がhugaの投稿をページネーションしたいという場合です。(ページ名をカスタム分類の条件にしている)

function query_exclude_hoge( $wp_query ) {
	if ( is_admin() || ! $wp_query->is_main_query() ) //管理画面とメインクエリー以外に影響を与えない定型文
		return;

	$paged = get_query_var( 'paged' ) ? get_query_var( 'paged' ) : 1; //ページネーション用

	//通常のthe_title()では取得できないためこの方法を使う
	if ( isset( $wp_query->query["name"] ) ) {
		$title = urldecode( $wp_query->query["name"] ); // huga

		if ( isset($wp_query->query["book"]) ) {
			//この方法だとカスタム投稿タイプのデフォルトテンプレートはarchive.phpになるので、
			//そこから専用のテンプレートに振り分けることができる。
			$args = array(
				'book' => $title, // huga
				'posts_per_page' => 20,
				'paged' => $paged,
				'tax_query' => array(array(
					'taxonomy' => 'book',
					'field' => 'slug',
					'terms' => $title // huga
				)),
			);
			$wp_query->query($args);
		}
	} //isset
}
add_action( 'pre_get_posts', 'query_exclude_hoge' );

こちらも無理やり動作させているので解説は控えます。「こんなへんてこなこともできるよ」という例です。

続いてコメントにあるようにarchive.phpで分岐します。

if ( isset($wp_query->query_vars["book"]) ){
	get_template_part( 'content', 'single-book' );
}

これでbook用のテンプレートはcontent-single-book.phpになります。


カスタムクエリを作成してデータベースの値を直接取得する

こちらはCodexでも解説されている正規のハックです。
自らデータベースへ問い合わせるクエリを作成して表示される投稿をコントロールします。

英語版Codexのカスタムクエリのページ」が参考になります。

例によってカスタム投稿タイプbookの投稿を20件取得する例です。

表示させたい投稿のテンプレートに記述します。

<?php
	$querystr = "
	SELECT $wpdb->posts.* /* 取り出す項目の指定(全て) */
	FROM $wpdb->posts /* 取り出すテーブルの指定(postsテーブル) */
	WHERE $wpdb->posts.post_status = 'publish'  /* 条件の指定、ステータスが公開 */
	AND $wpdb->posts.post_type = 'post'  /* ポストタイプの指定post_type */
	AND $wpdb->posts.post_date < NOW()  /* 投稿日時は現在より古いもの */
	ORDER BY $wpdb->posts.post_date DESC  /* 投稿日時でソート */
	LIMIT 20
	";
	$pageposts = $wpdb->get_results($querystr, OBJECT);
?>

$querystrにクエリを作成して、$wpdbクラスを利用してテーブルのデータをオブジェクトとして取得しています。
続けてループ部分を記述します。

<?php if ($pageposts): ?>
<?php global $post; ?>
<?php foreach ($pageposts as $post): ?>
<?php setup_postdata($post); ?>

<?php // ここに投稿があった場合の処理 ?>
<?php the_title(); ?>
<?php the_content(); ?>

<?php endforeach; ?>
<?php else : ?>
<?php // ここに投稿が無かった場合の処理 ?>
<?php endif; ?>

取得したオブジェクトを投稿のグローバル変数にセットするにはsetup_postdata()を利用します。the_title()等の変数を利用するには「global $post」でグローバル変数をリファレンスします。

こうすることでthe_title()the_content()が正しく動作します。このへんの書式はテンプレートなのでセットで覚えてください。
後は普通のループと同じように動作します。


カスタムアーカイブをルーティングから設計する

もはやここまで来るとWordPressで作業する必要があるのか疑問がわきますが、プラグインの作成等をする人には需要があると思います。
かなり敷居が高いと思いますが、ここまでカスタマイズできれば、自由自在にデータを表示することができるようになります。取得できないデータや、動作しないページングで悩まされることも無くなるはずです。

Codexのカスタムクエリのページ」が参考になります。

まずは新しいパラメータを扱えるようにするためにquery_varsフィルタフックを利用します。

function custom_queryvars( $qvars )
{
  $qvars[] = 'hoge';
  return $qvars;
}
add_filter('query_vars', 'custom_queryvars' );

これでWordPressに標準で用意されている「index.php?page=○○」や「index.php?year=○○」といったパラメータに「hoge」が加わりました。

続いて「index.php?hoge=○○」にアクセスした場合に適用されるカスタムテンプレートを指定します。
カスタムテンプレートの指定はtemplate_includeを利用します。

function hoge_template( $template ) {
	global $wp_query;
	if ( isset( $wp_query->query_vars['hoge'] ) ) {
		$new_template = locate_template( array( 'hoge.php' ) );
		if ( '' != $new_template ) {
			return $new_template ;
		}
	}
	return $template;
}
add_filter( 'template_include', 'hoge_template' );

これで「index.php?hoge=○○」にアクセスした場合にテンプレートファイル「hoge.php」が適用されます。

続いてテンプレートファイル「hoge.php」を実際に作成してカスタムクエリを作成します。
せっかくなのでカテゴリIDを渡すとカテゴリの投稿を取得するという例にします。

<?php
	global $wp_query, $wpdb;

	if( isset( $wp_query->query_vars['hoge'] )) {

		$catid = $wp_query->query_vars['hoge'];

		$querystr = "
		SELECT $wpdb->posts.* /* 取り出す項目の指定(全て) */
		FROM $wpdb->posts,$wpdb->term_taxonomy /* 取り出すテーブルの指定(postsテーブル、term_taxonomyテーブル) */
		WHERE $wpdb->posts.post_status = 'publish'  /* 条件の指定、ステータスが公開 */
		AND $wpdb->term_taxonomy.taxonomy = 'category'  /* カテゴリを指定taxonomyにはカスタム分類等もあるため */
		AND $wpdb->term_taxonomy.term_id IN ($catid)  /* カテゴリのIDを指定 */
		AND $wpdb->posts.post_type = 'post'  /* ポストタイプの指定post_type */
		AND $wpdb->posts.post_date < NOW()  /* 投稿日時は現在より古いもの */
		ORDER BY $wpdb->posts.post_date DESC  /* 公開日時の新しいもの順にする */
		LIMIT 20
		";
		$pageposts = $wpdb->get_results($querystr, OBJECT);
	}
?>

<?php if ($pageposts): ?>
<?php global $post; ?>
<?php foreach ($pageposts as $post): ?>
<?php setup_postdata($post); ?>

<?php // ここに投稿があった場合の処理 ?>
<?php the_title(); ?>
<?php the_content(); ?>

<?php endforeach; ?>
<?php else : ?>
<?php // ここに投稿が無かった場合の処理 ?>
<?php endif; ?>

カスタムクエリとの違いだけ解説します。

カテゴリを取得したいのでFROM節で$wpdb->term_taxonomyを指定。
$wpdb->term_taxonomy.taxonomycategory$wpdb->term_taxonomy.term_id IN$wp_query->query_vars[‘hoge’]で代入したカテゴリIDを渡します。

その後のループの部分はカスタムクエリの時と同じです。
以上の設定で「http://example.com/?hoge=123」にアクセスすれば、123というカテゴリIDを持つ投稿の一覧が表示されるはずです。

カスタムリライトルールの追加

このままではURLが「http://example.com/?hoge=123」というパラメータ丸出しになります。パーマリンクをデフォルト(http://example.com/?p=123)以外のものにしている場合都合が悪いこともあると思います。
そこでRewriteルールを変更します。まずはfunctions.phpに以下のコードを追加してリライトルールを消去します。

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

ちなみにこのリライトルールを消去する方法ですが、書き換え時に1度実行するだけです。
動作が遅くなるのでリライトルールを更新したら削除してください。ちなみに「設定>パーマリンク設定」を更新しても同じように動作するので、そちらで代用しても良いと思います。

続いて新しいルールを追加します。generate_rewrite_rulesアクションフックを利用します。

function hoge_add_rewrite_rules( $wp_rewrite ) 
{
  $new_rules = array( 
     'hoge/(.+)' => 'index.php?hoge=' .
       $wp_rewrite->preg_index(1) );

  $wp_rewrite->rules = $new_rules + $wp_rewrite->rules;
}
add_action('generate_rewrite_rules', 'hoge_add_rewrite_rules');

Apacheのmod_rewriteを設定したことのある方ならすんなり理解できると思います。
$wp_rewrite->preg_index(1)でパラメータを取得して、(.+)で後方参照して置き換えます。
そして新しいルール($new_rules)を古いルール($wp_rewrite->rules)に追加しています。

これで「http://example.com/hoge/123」にアクセスした時にカテゴリーIDが123の投稿を20件取得するカスタムアーカイブページを作成することができました。

サイト作成時に正しく設計さえすれば、WordPressが苦手な複雑な組み合わせのアーカイブページも実装できます。


以上、WordPressのループについてまとめました。
こうして俯瞰して見てみると「WordPressはあくまでブログ用のCMSである」という当たり前の前提がはっきりとわかります。

後半のハックはここまでくると「わざわざWordPressでする必要があるのか」と疑問が湧いてきますが、せっかく用意されているので利用しましょう。

更に理解を深めたいという方は参考になるサイトを紹介します。

WordPressでページ送りが動かないのはどう考えてもquery_postsが悪い!【pre_get_posts、WordPressループまとめ】 notnil creation weblogさん

WordPressでホームやアーカイブ毎に表示条件を変える(is_main_query と pre_get_posts フック) Gatespace’s Blogさん

カスタム投稿タイプのリライトに関するまとめ 560DESIGNSさん