WordPress

【wordpress】pre_get_postsを使ってみませんか?

More than 1 year has passed since last update.

概要

テーマテンプレートの中で

<?php
    $paged = get_query_var('paged') ? get_query_var('paged') : 1 ;
    $args = array(
        'post_type' => 'post',
        'post_status' => 'publish',
        'posts_per_page' => 10,
        'paged' => $paged
    );

    $the_query = new WP_Query($args);
?>
<?php if($the_query->have_posts()): while($the_query->have_posts()): $the_query->the_post(); ?>

    <p>ここになにかしらのHTML</p>
<?php endwhile; endif; ?>

こんな書き方してませんか? スニペットのように使っている人もいると思います。しかし、この書き方だとテンプレート内が少し読みにくくなりますよね。
また、他にも少しだけこの書き方で起きるデメリットがあるのでその部分についてまとめます。そして、これからはpre_get_posts使おう!って思ってもらえるといいな。

Wordpressで記事を表示するときの仕組み

そもそもどうやってWordpressがアーカイブならアーカイブの情報、詳細ページなら詳細ページの記事データを取得してどこに格納しているのかというのをざっくりと説明します。

メインクエリ

パーマリンクをいじっていないデフォルトのWordpressでアーカイブページや詳細ページを見るとURLの後ろに?=~のような文字列がくっついていると思います。これをパラメーターと言います。Wordpressではこのパラメーターを元にデータベースへ問い合わせ必要な情報を集めます。
例えば下記のような感じですね。

パーマリンク構造を変更している場合上記のようなリンク構造は確認できない場合があります。
それはWordpressがパーマリンク設定に基づきリライトしているからです。

そしてその問い合わせ結果は「$wp_query」オブジェクトに格納されます。これをメインクエリといいます。

メインループ

Wordpressでテンプレートを作っている以上は避けて通れないのがループです。ループっていうのはある条件下での繰り返し処理の事を言います。
テンプレート内でよく見かける

<?php if ( have_posts() ) : while ( have_posts() ) : the_post(); ?>
     <p>記事があるときのHTMLがここにある</p>
<?php endwhile; else: ?>
    <p>記事がないときはこっち/p>
<?php endif; ?>

こんな感じのコードがループ部分ですね。
メインクエリを元にこのような繰り返し処理を行うものをメインループと言います。

簡単に上記の処理の解説をすると、have_posts()は$wp_queryに値がセットされているか判定する関数なので、まずif文でメインクエリで問い合わせた結果があるかどうかを判断します。ある場合はループに進み、ない場合は記事がないという旨を表示するブロックに進みます。

次にこのhave_posts()は表示できる記事がある場合はtrue、記事がない場合はfalseを返すのでwhileの条件に渡すことで表示できる記事がある間ずっと繰り返すという処理を書くことができます。

the_post()ですが、Codexにも書いてあるとおり、ループを次の投稿へ進める関数です。同時に$wp_queryからデータを1件取り出して$postにデータを格納し投稿データを次に進めます。

この処理がぐるぐる繰り返すのでループと呼ばれるわけです。

ループ内で使うテンプレートタグというものが多々ありますが、これはこの$postの内容を元に値を表示したり取得したりしているのでループ外だと動かないわけですね。

クエリの書き換え

オリジナルでテーマを作る場合、テーマのループをデフォルトのクエリのまま使うことは多くないと思います。大体は冒頭でも出てきた

<?php
    $paged = get_query_var('paged') ? get_query_var('paged') : 1 ;
    $args = array(
        'post_type' => 'post',
        'post_status' => 'publish',
        'posts_per_page' => 10,
        'paged' => $paged
    );

    $the_query = new WP_Query($args);
?>
<?php if($the_query->have_posts()): while($the_query->have_posts()): $the_query->the_post(); ?>
<?php endwhile; endif; ?>

こういうコードを書いてカスタマイズしていると思います。
これはメインクエリを書き換えているのですが、書き換える方法もいくつかあるので解説します。

query_posts()

この関数の詳しい説明
現在は非推奨のテンプレートタグになっているので使わないでください。一応の手段としてご紹介します。
この関数の一番重要なところはメインクエリを書き換えるという点です。メインクエリを書き換えてしまうということはこのタグが読み込まれ、書き換えが行われた後、wp_reset_query()を呼び出さないかぎりサイト全体のクエリが書き換わった状態になってしまうため、意図しない結果が表示される危険性がある点で注意が必要です。

get_posts()

この関数の詳しい説明
この関数は渡されたパラメーターを元に配列を作る関数です。これはメインクエリを書き換えることは無いため、この関数を読んだ後にリセットなどの処理は不要です。

この関数を使って作ったループの中ではthe_titleなど所謂ループの中で使用してください的なテンプレートタグは機能しません。
これはこの関数が返す値の形がそれらのテンプレートタグが動く形で返ってこないからです。

この関数を使ってthe_titleなどのテンプレートタグを使いたい場合はsetup_postdataを使ってください。

<?php
$args = array( 'posts_per_page' => 3 );
$lastposts = get_posts( $args );
foreach ( $lastposts as $post ) :
  setup_postdata( $post ); ?>
    <h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
    <?php the_content(); ?>
<?php endforeach; ?>
<?php wp_reset_postdata(); //setup_postdata()でセットされた$postをリセットする。?>

setup_postdataは$postの内容を書き換えてしまうためsetup_postdataを使用した後は必ずwp_reset_postdata()でリセットする必要があります。

例えばサイドバーや、複数のカスタム投稿があるサイトでカスタム投稿ごとにお知らせを出したい、特定のカスタム投稿の新着5件を出したいなど細かいカスタマイズをしなければならないものが1ページの中に混在している場合や、メインループの内容の他にループを作らなければならない場合などに使える関数です。

メインループ以外のループのことをサブループといいます。

WP_Query

この関数の詳しい説明
この関数も渡されたパラメーターを元に元のメインクエリを破壊せずに投稿データを取得することができます。get_posts()と同じじゃないかと思われるかもしれませんが、違いもあります。こちらの方法で取得したデータは$wp_queryと同じ構造のオブジェクトとして返ってくる点が最大の違いで、オブジェクトの中にはis_home()などの条件分岐タグの判定のためのフラグも含まれています。

使い分け

使い分けとしては何か単順に記事を取得するだけならget_posts()、WP_Queryどちらを使っても問題無いですが、少し込み入った条件分岐タグを使うような処理をループ内で行う必要がある場合にはWP_Queryを使うような使い分けがいいと思います。

デメリット

これらの関数はカスタマイズする上で欠かせない部分ではありますが、デメリットがいくつかあります。

データベースへの問い合わせが増える

最初の方で説明しましたが、基本的にURLがリクエストされた時点でデフォルトのメインクエリを元にデータベースへの問い合わせが行われます。しかし、紹介した3つの関数はその関数まで処理が到達した時点でもう一度データベースへ問い合わせを行います。たった1回増えるだけじゃないかと思われるかもしれませんが、例えば1秒間に数百アクセスがあるWordpressサイトがあったとするとデータベースへのアクセスはその2倍になるという計算になり、動作速度の低下や、最悪の場合サイトが落ちる可能性もあります。

可読性

また、PHPコードとHTMLのコードが入り交じるテンプレートは可読性が悪く保守性も悪くなる場合があります。デザイン上サブループをガシガシ書かないといけないのであればしょうがない場合もありますが、サブループを必要としない部分でもクエリの書き換えを行うコードを書くのは前述のものと併せて避けたほうがいいと思います。

保守性

クエリのカスタマイズが必要なページが数ページ渡っていて、それぞれにクエリのコードが書いてあった場合、コードがテーマ内に散乱し保守性が下がることも懸念されます。

pre_get_postsのご提案

上記のデメリットを解消するため、pre_get_postsをおすすめします。

pre_get_postsとは

これはWordpressのアクションフックに分類されるものです。Wordpressがクエリを実行する前に呼び出されているフックなのでこれを使うことでそれぞれのページに合ったクエリの問い合わせ結果をメインクエリとして扱うことができます。

つまり、それぞれのページにWP_Queryとかget_posts()と一緒にパラメーター用の$argsの中にごにょごにょ書かずにループを書けるということです。また、各ページのクエリがfunction.phpに集約されることで可読性、保守性なども確保でき、データベースへの問い合わせも1度になるため動作の高速化も期待できます。

使い方

基本的な使い方はこんな感じ

function change_posts_per_page($query) {
 /* 管理画面,メインクエリに干渉しないために必須 */
if ( is_admin() || ! $query->is_main_query() ){
     return;
 }

 /* カテゴリーページの表示件数を5件にする */
 if ( $query->is_category() ) {
     $query->set( 'posts_per_page', '5' );
     return;
 }

}
add_action( 'pre_get_posts', 'change_posts_per_page' );

ここで注意点があって、

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

この部分についてです。このフックはメインクエリが実行される前のクエリを書き換えてしまうため、管理画面やプレビュー時に表示がされなくなる場合があります。それを回避するために上記の分岐を一番最初に書きます。この記述は必須となります。

公開ステータスの変更を伴うポストの更新時、プレビューが効かなくなる場合がありますので、その際は除外にis_preview()を追加し、対処してください。

 if ( $query->is_category() ) {
     $query->set( 'posts_per_page', '5' );
     return;
 }

続いてこちらの方ですが、まずどのページでどのクエリを使うかを判定するための条件分岐を書きます。主な条件分岐を列挙します。

// TOPページ
$query->is_home()

//詳細ページ 
$query->is_single()

//固定ページ 
$query->is_page()

// アーカイブページ 
$query->is_archive()

// カスタム投稿タイプアーカイブページ カスタム投稿タイプを入れてください 
$query->is_post_type_archive( 'post_type' )

// 日付アーカイブページ 
$query->is_date()

// 年別アーカイブページ 
$query->is_year()

// 月別アーカイブページ 
$query->is_month()

// 制作者アーカイブページ 
$query->is_author()

// カテゴリーページ 
$query->is_category()

// カテゴリーページ 配列での指定(カテゴリID3,カテゴリスラッグfoo,カテゴリ名Bar barのいずれか) 
$query->is_category( array(3,'foo','Bar bar') )

// タグページ 
$query->is_tag()

// タクソノミーページ 
$query->is_tax()

// タクソノミーページ fooというスラッグのタクソノミーアーカイブが表示された時
$query->is_tax( 'foo' )

// タクソノミーページ bar1,bar2のスラッグがfooタクソノミーアーカイブで表示された時 
$query->is_tax( 'foo', array('bar1','bar2') )

// 検索結果ページ 
$query->is_search()

// フィードページ
$query->is_feed()

// 404ページ 
$query->is_404()

公式リファレンスに条件分岐タグをまとめたページがあるので詳しくはそちらを参照してください。

条件分岐タグ

これらを使いクエリを書き換えたいページを特定し、$query->set( '①', '②' );で値をセットします。
これはWP_Queryで渡せるステータスのキーと値をそのまま使えます。①にキー名(posts_per_pageだったりshow_posts)を指定して、②にセットしたい値を入れれば完了です。