WordPress Logo WordPress のループ(Loop)

WordPress のループの使い方や仕組み、グローバル変数 $wp_query や $post、WP_Query クラスなどを理解するための覚書です。

作成日:2019年03月20日

ループ

プログラミング言語において「ループ」は繰り返し処理を意味します。

テンプレートファイルに記述する WordPress のループは、アクセスされた URL にしたがって WordPress が自動的に抽出した投稿のデータを出力するために使用します。

WordPress Codex(日本語版):ループ

WordPress での処理の流れ

ループの詳細の前に「WordPress が自動的に抽出(用意)するデータ」について。

WordPress では、ユーザーからの URL のリクエストによりどのテンプレートでページを表示するかが決定されて、そのページに必要なデータが自動的に用意されるようになっています。

以下は WordPress で各ページにアクセスした時の処理のおおまかな流れです。

  1. ユーザーからアクセスされた URL(パーマリンク) を元にクエリを発行(データベースに問い合わせを実行)し、アクセスされた URL に該当するページのデータをデータベースからの読み込みます。
  2. 取得したデータ(メインクエリ)をグローバル変数 $wp_query に格納します。
    $wp_query は WP_Query クラスのインスタンス(オブジェクト)です。
  3. テンプレート階層に従ってテンプレートが自動的に選択されて、ページの種類に応じたテンプレートが読み込まれます。
  4. テンプレートファイル内のループの記述により記事を出力します(テンプレートの処理)。

個々のテンプレートの処理に入る際は、既に投稿や固定ページ、アーカイブページなどの情報(WordPress が自動的に抽出したデータ)が読み込まれた状態になっています。

そしてテンプレートに記述されたループにより抽出された記事(WordPress が自動的に用意したデータ)が出力されます(このループをメインループと呼びます)。

投稿や固定ページなどの個別ページであればその個別ページのデータが用意され、アーカイブ(一覧)ページであればそのアーカイブの種類の一覧で表示する全ての投稿記事のデータが用意されます。

このため、テンプレートにループ(とその中にデータを出力するテンプレートタグ)を記述すれば、個別ページであればその個別ページの投稿のデータが表示され、アーカイブページであればそのアーカイブの種類の投稿記事のデータを表示することができます。

個別ページなどの場合は、ループで取得する投稿の件数は1件(そのページの記事のみ)ですが、アーカイブページやカテゴリーページなどの一覧ページの場合は取得する投稿の件数は管理画面の「1ページに表示する最大投稿数」で設定した件数になります。

関連ページ:テンプレートからページへの変換

以下のサイトにわかり易い説明があります。

グローバル変数 $wp_query

$wp_query は、WordPress がページ読み込み時にデータベースから自動的に取得した、表示する記事やページなどに関するデータの集まり(オブジェクト)が格納されている変数です。

日本語Codex: WP Query

$wp_query は WordPress のシステム自体がデータベースに問い合わせを実行した結果(クエリ結果)が入っているのでメインクエリとも呼ばれます。

また、$wp_query は WP_Query クラスで生成されたオブジェクトで、クエリオブジェクトとも呼ばれることもあります。

WP_Query クラスは wp-includes/class-wp-query.php で定義されています。

$wp_query は wp-settings.php で以下のように生成されて、スーパーグローバル変数 $GROBALS に格納されています。

/** WordPress Query object
 * @global WP_Query $wp_the_query
 */
$GLOBALS['wp_the_query'] = new WP_Query();

/** Holds the reference to @see $wp_the_query
 * Use this global for WordPress queries
 * @global WP_Query $wp_query
 */
$GLOBALS['wp_query'] = $GLOBALS['wp_the_query'];   

上記はつまり以下のようなことになります。

$wp_the_query = new WP_Query();
$wp_query = $wp_the_query;

上記のように実際には、WordPress では「表示する記事やページなどのデータ」を $wp_query$wp_the_query の2つのグローバル変数で管理しています。

$wp_the_query はページを読み込んだ時点で取得したデータで復元用のバックアップのような存在です。処理の中でのその時点(時々)のデータは $wp_query に反映されています。

$wp_query は関数などの操作によっては内容が変更されてしまうことがありますが、システム全体で共有されるグローバル変数なので操作終了後は変更前の状態に戻します。

そのため、独自に(自分で)$wp_query を変更をした場合(非推奨の query_posts() を使用した場合など)は wp_reset_query() を使って元に戻す必要があります。wp_reset_query() は $wp_the_query を使って $wp_query の内容を元に戻す関数です(wp_reset_query() のソース)。

$wp_query や $wp_the_query が書き換えられてしまうと、WordPress が期待通りに動作しなくなってしまうので注意が必要です。

$wp_query の内容はテンプレートで var_dump() や print_r() を使って確認することができます。以下では見易いように<pre>タグを使っています。

<pre><?php var_dump($wp_query); ?></pre>

出力される内容はテンプレートやアクセスする URL、記述する場所によって異なります。

以下はフロントページでの出力例です。そのページに関する様々なデータが取得され格納されています。

5行目からの ["query_vars"] には get_query_var() で取得できる query_vars プロパティの値が格納されています。

query_vars(配列)には URL から解析されたキーと値のペアが入っています(ページの種類やパーマリンク設定により取得される値は異なります)。

22行目の ["posts"] には取得した投稿の情報が配列で格納されています。この例の場合、array(3)とあるので3件の投稿が取得されています。

続いて最初の投稿([0])の情報が表示されています。object(WP_Post) とあるように投稿(WP_Post クラス)のオブジェクトです。

58行目の ["post"] には現在の(最初に表示する)投稿の情報が格納されています。投稿の内容は、最初の投稿(posts[0])と同じになっています。

object(WP_Query)#518 (51) { //WP_Query(クエリオブジェクト)
  ["query"]=>  //クエリ
  array(0) {
  }
  ["query_vars"]=>  //get_query_var() で取得できる値(URL から解析されたキーと値のペア)
  array(64) {
    ["error"]=>
    string(0) ""
    ["m"]=>
    string(0) ""
    ["p"]=>
    int(0)
    ["post_parent"]=>
    ・・・中略・・・
    ["paged"]=>  //アーカイブページ分割時のページ番号。
    int(0)  //最初のページの場合は 0。2ページ目以降はページの番号

    ・・・中略・・・

    ["request"]=>  //リクエスト(クエリの SELECT 文)
  string(182) "SELECT SQL_CALC_FOUND_ROWS  wp_posts.ID FROM wp_posts  WHERE 1=1  AND wp_posts.post_type = 'post' AND (wp_posts.post_status = 'publish')  ORDER BY wp_posts.post_date DESC LIMIT 0, 10"
  ["posts"]=>  //取得された投稿($wp_query->posts)
  &array(3) {  //この例の場合3件の投稿がある
    [0]=>  //[0]の投稿
    object(WP_Post)#4776 (24) {  //WP_Post(投稿オブジェクト)
      ["ID"]=>
      int(94)
      ["post_author"]=>
      string(1) "1"
      ["post_date"]=>
      string(19) "2019-03-10 13:21:15"
      ["post_date_gmt"]=>
      string(19) "2019-03-10 04:21:15"
      ["post_content"]=>
      string(70) "テストです。"
      ["post_title"]=>
      string(21) "テスト"
      ["post_excerpt"]=>
      string(0) ""
      ["post_status"]=>
      string(7) "publish"
      ["comment_status"]=>
      string(6) "closed"
      ["ping_status"]=>
      string(6) "closed"
      ["post_password"]=>
      string(0) ""
      ["post_name"]=>
      string(12) "test"

      ・・・中略・・・

    ["post_count"]=> //ループ関連関数で使用(投稿総数)
    int(3)
    ["current_post"]=> //ループ関連関数で使用(現在の投稿を識別する数値)
    int(-1)
    ["in_the_loop"]=> //ループ関連関数で使用(ループ内か否か)
    bool(false)
    ["post"]=> //現在の投稿($wp_query->post)$post
    object(WP_Post)#4773 (24) {  //WP_Post(投稿オブジェクト)
      ["ID"]=>
      int(94)
      ["post_author"]=>
      string(1) "1"
      ["post_date"]=>
      string(19) "2019-03-10 13:21:15"
      ["post_date_gmt"]=>
      string(19) "2019-03-10 04:21:15"
      ["post_content"]=>
      string(70) "テストです。"

      ・・・中略・・・

    ["is_home"]=>  //条件分岐タグ
    bool(true)
    ["is_404"]=>
    bool(false)
    ["is_embed"]=>
    bool(false)

      ・・・以下省略・・・

以下は投稿個別ページでの出力例です。個別ページなので取得される投稿は1件のみです。

object(WP_Query)#520 (51) {
  ["query"]=>  //クエリ
  array(3) {
    ["page"]=>
    string(0) ""
    ["name"]=>
    string(10) "first-post"
    ["category_name"]=>
    string(5) "music"
  }
  ["query_vars"]=>
  array(65) {
    ["page"]=>
    int(0)
    ["name"]=>
    string(10) "first-post"
    ["category_name"]=>
    string(5) "music"
    ["error"]=>
    string(0) ""

      ・・・中略・・・

   ["queried_object_id"]=>
  int(41)
  ["request"]=>  //リクエスト(クエリの SELECT 文)
  string(150) "SELECT   wp_posts.* FROM wp_posts  WHERE 1=1  AND wp_posts.post_name = 'first-post' AND wp_posts.post_type = 'post'  ORDER BY wp_posts.post_date DESC "
  ["posts"]=>  //取得された投稿($wp_query->posts)
  &array(1) {  //個別ページなので1件の投稿のみ
    [0]=>
    object(WP_Post)#4781 (24) {  //WP_Post(投稿オブジェクト)
      ["ID"]=>
      int(41)
      ["post_author"]=>
      string(1) "1"

      ・・・以下省略・・・

特定のプロパティを「$wp_query->プロパティ名」のように指定することもできます。以下は posts プロパティを調べる場合の例です。

<pre><?php var_dump($wp_query->posts); ?></pre>

以下はカスタム分類(タクソノミー)のアーカイブページでの出力結果です。

26、27行目は現在表示しているページ(リクエストされたページ)のオブジェクトの情報です。$wp_query には投稿の情報(オブジェクト)の他にもそのページに関する情報が格納されています。

このオブジェクトは get_queried_object() で取得することができます。このオブジェクトはリクエストされたページの種類により異なります。この例の場合、カスタム分類のアーカイブページなので、ターム(WP_Term クラス)オブジェクトですがフロントページではこの値は null になります。

関連項目:現在クエリされているオブジェクトの取得

object(WP_Query)#520 (52) {
  ["query"]=>  //クエリ
  array(1) {
    ["rental_cat"]=>
    string(6) "medium"
  }
  ["query_vars"]=>
  array(67) {
    ["rental_cat"]=>
    string(6) "medium"
    ["error"]=>
    string(0) ""
    ["m"]=>
    string(0) ""
    ["p"]=>
    int(0)
    ["post_parent"]=>
    string(0) ""
    ["subpost"]=>
    string(0) ""
    ["subpost_id"]=>
    string(0) ""

    ・・・中略・・・

  ["queried_object"]=>  //リクエストされたオブジェクト
  object(WP_Term)#4432 (10) {  //WP_Term(タームオブジェクト)
    ["term_id"]=>
    int(28)
    ["name"]=>
    string(6) "中型"
    ["slug"]=>
    string(6) "medium"
    ["term_group"]=>
    int(0)
    ["term_taxonomy_id"]=>
    int(28)
    ["taxonomy"]=>
    string(10) "rental_cat"
    ["description"]=>
    string(53) "中型車のカテゴリーです。"
    ["parent"]=>
    int(0)
    ["count"]=>
    int(1)
    ["filter"]=>
    string(3) "raw"
  }
  ["queried_object_id"]=>
  int(28)
  ["request"]=>
  string(381) "SELECT SQL_CALC_FOUND_ROWS  wp_posts.ID FROM wp_posts  LEFT JOIN wp_term_relationships ON (wp_posts.ID = wp_term_relationships.object_id) WHERE 1=1  AND (
  wp_term_relationships.term_taxonomy_id IN (28)
) AND wp_posts.post_type = 'rental' AND (wp_posts.post_status = 'publish' OR wp_posts.post_status = 'private') GROUP BY wp_posts.ID ORDER BY wp_posts.post_date DESC LIMIT 0, 10"
  ["posts"]=>
  &array(1) {
    [0]=>
    object(WP_Post)#4420 (24) {  //WP_Post(投稿オブジェクト)
      ["ID"]=>
      int(385)
      ["post_author"]=>
      string(1) "0"
      ["post_date"]=>
      string(19) "2019-05-06 15:02:22"
      ["post_date_gmt"]=>
      string(19) "2019-05-06 06:02:22"
      ["post_content"]=>
      string(64) "test rental1"
      ["post_title"]=>
      string(12) "Rental Test1"

      ・・・以下省略・・・
  }

日本語版 Codex:WP_Query

メインループ

リクエストされた URL を元に WordPress がデータベースから自動的に取得した記事やページのデータが 「メインクエリ」で、そのデータをテンプレートで表示するループが「メインループ」になります。

メインクエリの内容(メインループで使用できるデータ)は、WordPress がユーザーからリクエストされた URL を解析する時点で既に決定され、グローバル変数 $wp_query に格納されています。

WordPress が自動的に取得したデータとは異なる条件のデータを扱いたい場合は、pre_get_posts アクションフックやサブループを使います。

以下がループの一般的な形式です。

以下に記述されている条件や関数によりグローバル変数 $wp_query(WordPress がデータベースから自動的に取得したページのデータ)から値を取得する準備が整えられるようになっています。

グローバル変数 $wp_query を操作する記述はありませんが、the_post() などの関数が裏側で $wp_query を操作しています。

<?php if(have_posts()) : ?>
 <!-- 投稿(一覧)を出力する前に行う処理(もしあれば)-->
 <?php while(have_posts()): the_post(); ?>
 <!-- 個々の投稿を出力する処理(ループ内)-->
 <?php endwhile; ?>
 <!-- 投稿(一覧)を出力した後に行う処理(もしあれば)-->
<? else: ?>
 <!-- 投稿がないときに行う処理 -->
<?php endif; ?> 

上記のコードの「個々の投稿を出力する処理(ループ内)」となっている部分にページの種類(テンプレート)に応じて記事のコンテンツやタイトル、日付等を出力するテンプレートタグを記述します。

個別ページなどの場合は、ループで処理(表示)する記事の件数はそのページの記事のみなので1件ですが、アーカイブページやカテゴリーページなどの一覧ページの場合は処理(表示)する記事の件数は管理画面の「設定」→「表示設定」での「1ページに表示する最大投稿数」で設定した件数になります。

標準で取得されるデータ
生成するページ 標準で取得するデータ
ホームページ 最新の投稿と投稿に関するデータ(※)
※ 管理画面の「設定」→「表示設定」の「ホームページの表示」で「最新の投稿」が選択されている場合
カテゴリー別ページ 該当するカテゴリーに属する投稿と投稿に関するデータ
月別ページ 該当する年月に投稿された投稿と投稿に関するデータ
投稿の個別ページ 該当する投稿に関するデータ
固定ページ 該当する固定ページに関するデータ

以下はアーカイブ(一覧)ページでのループの例です。

ループ内で一覧表示するそれぞれの記事のタイトルや日付、抜粋を出力しています。

<?php if(have_posts()): ?>
  <?php while(have_posts()): the_post(); ?>
  <!-- ループ内(ここから) -->
  <div class="posts">
    <h3 class="title"><?php the_title(); ?></h3><!-- タイトルを出力 -->
    <p class="date"><?php echo get_the_date(); ?></p><!-- 日付を出力 -->
    <div class="excerpt"><!-- 抜粋を出力 -->
      <?php the_excerpt(); ?>
      <p><a href="<?php the_permalink(); ?>">続きを読む</a></p>
    </div>
  </div>
  <!-- ループ内(ここまで) -->
  <?php endwhile; ?>
<?php else: ?>
 <p>投稿がありません。</p>  //投稿がない場合
<?php endif; ?> 

例えばカテゴリーアーカイブ(一覧)ページ(category.php など)の場合、WordPress は自動的にそのカテゴリーに属する記事の情報をデータベースから取得してグローバル変数 $wp_query の posts と言うプロパティに格納しています。

そのカテゴリーに属する記事が複数あれば、posts の中にはそれぞれの記事の情報が順番に入っています。

posts は配列になっていて、最初の記事は posts[0] で次の記事は posts[1]、その次は posts[2] と言うように格納されています。

また、グローバル変数 $wp_query には post(最後に s が付かない)と言うプロパティがあり、ループの中で現在表示している投稿の情報が入るようになっています。post にはループの最初の繰り返しでは posts[0] が、次の繰り返しでは posts[1] がと言うように順番に中身が入れ替わるような仕組みになっています。

上記コードの「ループ内」では、これらの記事の情報が順番に呼び出されるようになっていて、ループ内で記事の情報を取得・出力する関数(テンプレートタグ)を記述することにより、その記事の情報(タイトルや本文など)を表示することができます。

そして該当する記事の数の分だけループされ(繰り返され)それぞれの記事の情報を表示できるようになっています。

以下は投稿の個別ページでのループの例です。

ループ内で表示する記事のタイトルや、その記事の属するカテゴリーやタグ、日付、コンテンツとその編集用リンクを出力しています。

個別ページの場合は、WordPress により取得される記事の情報は1件のみなので、グローバル変数 $wp_query の posts プロパティには posts[0] の1つだけが入っていることになります。そのためループも1回のみ実行されます。

<?php if(have_posts()): ?>
  <?php while(have_posts()): the_post(); ?>
  <!-- ループ内(ここから) -->
  <div class="post">
    <h2 class="single_title"><?php the_title(); ?></h2><!-- タイトルを出力 -->
    <p class="cat_tag">
      CATEGORY: <?php the_category(', '); ?><!-- カテゴリーを出力 -->
      <?php the_tags(' | TAG: ', ', '); ?><!-- タグを出力 -->
    </p>
    <p class="date"><?php echo get_the_date(); ?></p><!-- 日付を出力 -->
    <div class="single_content">
      <?php the_content(); ?><!-- コンテンツを出力 -->
      <?php edit_post_link('この記事を編集','<p class="editlink">','</p>'); ?>
    </div>
  </div>
  <!-- ループ内(ここまで) -->
  <?php endwhile; ?>
<?php endif; ?>

ループ内では、以下のようなテンプレートタグを使って表示したい情報を出力することができます。

ループ内で使用できるテンプレートタグの例
出力する内容 出力用テンプレートタグ 値を取得するテンプレートタグ
タイトル the_title get_the_title
抜粋 the_excerpt get_the_excerpt
本文 the_content get_the_content
投稿日 the_date get_the_date
投稿時刻 the_time get_the_time
更新日 the_modified_date get_the_modified_date
投稿者名 the_author get_the_author
カテゴリ名 the_category get_the_category
アイキャッチ画像 the_post_thumbnail get_the_post_thumbnail
ID the_ID get_the_ID
パーマリンク(URL) the_permalink get_permalink

関連ページ:ループで使うテンプレートタグや関数

ループの仕組み

ループの仕組みを少し詳しく見てみます。

以下が基本的なループの構造です。

<?php if(have_posts()) : ?>
 <!-- 投稿(一覧)を出力する前に行う処理(もしあれば)-->
 <?php while(have_posts()): the_post(); ?>
 <!-- 個々の投稿を出力する処理(ループ内)-->
 <?php endwhile; ?>
 <!-- 投稿(一覧)を出力した後に行う処理(もしあれば)-->
<? else: ?>
 <!-- 投稿がないときに行う処理 -->
<?php endif; ?> 

1行目は、if 文で have_posts() を使って表示すべきデータ(記事)があるかを判定しています。

have_posts() はグローバル変数 $wp_query を調べて、表示する記事があれば true を返し、なければ false を返す関数です。そのため have_posts() が true を返せば2行目移行に進みますが、false を返せばそこで終了します。

3行目は見易さのために1行にしてありますが、while(have_posts()): と the_post(); を分けることができます。while(条件) は「条件」が成立する限り endwhile までの間に記述された処理を繰り返します。

<?php while(have_posts()): ?>
  <!-- ループ内 -->
  <?php the_post(); ?>
  <!-- 個々の投稿を出力する処理 -->
<?php endwhile; ?>

ここでも have_posts() を使って条件を判定しています。

グローバル変数 $wp_query には現在の投稿を識別するための current_post(ループ内でのみ使用可能)と言うプロパティと投稿の総数を示す post_count と言うプロパティがあり、have_posts() はこれらのプロパティを調べて true または false を返しています。

また、current_post と言うプロパティはループ内で最初に記述する the_post() と言う関数によりその値が1ずつ増加するように設計されています。

このためループを繰り返して current_post の値(正確には current_post の値 +1)が post_count と同じになったら have_posts() が false を返してループが終了する仕組みになっています。

もし the_post() を記述しないと current_post の値が変わらない為、無限ループになってしまいます。

the_post() は current_post の値を増加させると共に、次の投稿が適切に表示されるように投稿のデータが入っているグローバル変数 $post を更新(書き換え)したり、表示に必要なプロパティをセットアップします。

以下のように記述すれば、current_post と post_count の値を確認することができます。

<?php if(have_posts()) : ?>
 <?php  echo "current_post: ".$wp_query->current_post;  //current_post: -1 ?>
 <?php while(have_posts()): the_post(); ?>
 <?php //current_post:0 post_count:3 ~ current_post:2 post_count:3
   echo "current_post:".$wp_query->current_post. " post_count:" .$wp_query->post_count;
  ?>
 <?php endwhile; ?>
 <?php  echo "current_post: ".$wp_query->current_post; //current_post: -1 ?>
<? else: ?>
<?php endif; ?> 

$post_count と $found_posts

現在表示しているページの投稿の件数(投稿の取得件数)は、$post_count プロパティで取得できます。

1ページに表示する投稿の件数は、管理画面の「設定」→「表示設定」での「1ページに表示する最大投稿数」で設定されていますが、最大投稿数に満たない場合など実際に表示されている(取得された)件数は $post_count で取得できます。

また、現在のクエリにマッチする投稿の合計数(全件数)は $found_posts で取得できます。

これらのプロパティを使うと、例えば以下のようにしてマッチする投稿の全件数や現在表示している投稿の件数を表示することができます。

$my_post_range は現在表示している最初の投稿が何件目かを示す値です。

<?php if(have_posts()) :?>
<?php
global $paged;  //ページ分割時のページ番号
if(empty($paged)) $paged = 1;

$my_post_count = $wp_query->post_count;  //投稿の取得件数
$my_found_posts = $wp_query->found_posts;  //投稿の全件数
$my_posts_per_page = get_query_var('posts_per_page');  //1ページに表示する最大投稿数
$my_post_range = ($paged -1) * $my_posts_per_page + 1 ;  //$paged は現在のページ番号

//例えば「全 24 件中 6 件目から 5 件を表示」と出力
echo "<p> 全 " . $my_found_posts . " 件中 " .
     $my_post_range . ' 件目から '. $my_post_count . " 件を表示</p>";
?>
<?php while(have_posts()) : the_post(); ?>
・・・
<?php endwhile; ?>
<?php endif; ?>
$wp_query 投稿数関連プロパティ
プロパティ 説明
post_count ループで表示される投稿の数(投稿の取得件数)。現在表示しているページに何件の投稿が存在するか
found_posts 現在のクエリ変数に一致する投稿の合計数(条件にマッチする投稿の合計数)
max_num_pages ページの合計数。found_posts を posts_per_page で割った結果

※ 上記はメインループの例です。

$wp_query には posts_per_page というプロパティがないようなので、1ページに表示する最大投稿数は get_query_var('posts_per_page') で取得しています。

但し、サブループでは1ページに表示する最大投稿数を get_query_var('posts_per_page') では取得できないようです。

1ページに表示する最大投稿数(設定→表示設定)は get_option( 'posts_per_page' ) で取得できます。

get_option()

have_posts() や the_post() の詳細は次項を参照ください。

have_posts()

以下はループで使用されている関数 have_posts() についての詳細です。

have_posts() は wp-includes/query.php で以下のように定義されていて、$wp_query のメソッド have_posts() の実行結果を返しています。

function have_posts() {
  global $wp_query; //グローバル変数 $wp_quer を関数内で使用するためのグローバル宣言
  return $wp_query->have_posts();
}

$wp_query(WP_Query クラス)のメソッド have_posts() は wp-includes/class-wp-query.php で以下のように定義されています。$this は $wp_query を指します。

例えば、$this->current_post は $wp_query の持つ current_post プロパティの値を意味します。

  • current_post:現在表示されている投稿を表す値(初期値は-1で次の投稿が表示される際に1増加)
  • post_count :表示される投稿の総数

3行目は「current_post + 1」と「post_count」の値を比較して「表示する投稿が残っているか」を判定していて、この判定で true が返る限りループは継続します。

「current_post + 1」と「post_count」が同じ値になれば、5行目の判定が適用され loop_end と言うアクションフックが実行され、rewind_posts() と言う関数が実行されることで current_post の値がリセットされ、$wp_query の post プロパティも初期の値(posts[0])に戻されて15行目へ移行します。

もし、「post_count」が 0 の場合(表示するデータがない場合)は、10行目の判定が適用され loop_no_results と言うアクションフックが実行され15行目へ移行します。

「ループ終了」または「表示するデータがない」場合は、$wp_query の in_the_loop プロパティの値 false に変更し、false を返します(ループ終了)。

//class WP_Query
public function have_posts() {
  if ( $this->current_post + 1 < $this->post_count ) {
    return true; // ループは継続
  } elseif ( $this->current_post + 1 == $this->post_count && $this->post_count > 0 ) {
    /* ループ終了した場合は以下のアクションを実行 */
    do_action_ref_array( 'loop_end', array( &$this ) );
    // ループ終了時の後始末
    $this->rewind_posts();
  } elseif ( 0 === $this->post_count ) {
    /* 表示するデータがない場合は以下のアクションを実行 */
    do_action( 'loop_no_results', $this );
  }
  //「ループ終了」または「表示するデータがない」場合
  $this->in_the_loop = false; //in_the_loop プロパティの値を false に変更
  return false;
}
rewind_posts()

rewind_posts() はループの投稿情報を巻き戻し、前回と同じ順序で先頭の投稿を取得できるようにする関数です。

以下は rewind_posts() の定義です(wp-includes/class-wp-query.php)。

3行目:current_post の値を -1(初期値)にリセット。

4~5行目:(current_post + 1 と post_count が等しく且つ)post_count > 0 であれば、$wp_query の post プロパティ($this->post)に $this->posts[0](全ての投稿 posts[] の中の最初の投稿)を代入して $this->post($wp_query の post プロパティ) を先頭の投稿に戻しています。

この操作によりループの投稿情報を巻き戻し、前回と同じ順序で先頭の投稿を取得できるようにしています。但し、the_post() によって書き換えられたグローバル変数 $post はそのままのように見えます。(グローバル変数 $post

//class WP_Query
public function rewind_posts() {
  $this->current_post = -1;
  if ( $this->post_count > 0 ) {
    $this->post = $this->posts[0];
  }
}
the_post()

以下はループで使用されている関数 the_post() についての詳細です。

the_post() は wp-includes/query.php で以下のように定義されていて、$wp_query のメソッド the_post() を実行しています。

function the_post() {
  global $wp_query; //グローバル変数 $wp_quer を関数内で使用するためのグローバル宣言
  $wp_query->the_post();
}

$wp_query(WP_Query クラス)のメソッド the_post() は wp-includes/class-wp-query.php で以下のように定義されています。$this は $wp_query を指します。

3行目:グローバル変数 $post を宣言。

4行目: $wp_query の in_the_loop プロパティの値 true に変更。

6~8行目:「current_post == -1」の場合は、ループが開始直後と判定し、loop_start と言うアクションを実行。

10行目:グローバル変数 $post に next_post() で取得する次の投稿のオブジェクトを代入(グローバル変数 $post が書き換えられる)

11行目:setup_postdata() を実行して投稿を表示できるようにグローバル変数 $post のデータを整える準備を実行

//class WP_Query
public function the_post() {
  global $post; //グローバル変数 $post を関数内で使用するためのグローバル宣言
  $this->in_the_loop = true; //in_the_loop プロパティの値を true に変更

  if ( $this->current_post == -1 ) // loop has just started
    /* ループを開始した直後の場合は以下のアクションを実行 */
    do_action_ref_array( 'loop_start', array( &$this ) );

  $post = $this->next_post(); //次の投稿をグローバル変数 $post に代入(書き換えられる!)
  $this->setup_postdata( $post ); //投稿を表示する準備を実行
}
setup_postdata()

$wp_query のメソッド setup_postdata() は wp-includes/class-wp-query.php で以下のように定義されています。

setup_postdata() は 現在処理されている投稿(ページ)を表示するために、その投稿のプロパティやグローバル変数を準備する関数です。

この関数の引数には $post(WP_Post のインスタンスか投稿オブジェクトまたは投稿 ID)を指定する必要があります。

Codex には「この関数は $post グローバル変数をセットしませんが、それへのリファレンスを引数とするつもりで設計されています。」とあるように(?)引数にはグローバル変数 $post が指定されていないとうまく機能しないようです。(

Function Reference/setup postdata
Code Reference/setup_postdata

//class WP_Query
/**
* Set up global post data.
*
* @since 4.1.0
* @since 4.4.0 Added the ability to pass a post ID to `$post`.
*
* @global int             $id
* @global WP_User         $authordata
* @global string|int|bool $currentday
* @global string|int|bool $currentmonth
* @global int             $page
* @global array           $pages
* @global int             $multipage
* @global int             $more
* @global int             $numpages
*
* @param WP_Post|object|int $post WP_Post instance or Post ID/object.
* @return true True when finished.
*/
public function setup_postdata( $post ) {
  global $id, $authordata, $currentday, $currentmonth, $page, $pages, $multipage, $more, $numpages; //これらのグローバル変数をセットアップするために宣言

  if ( ! ( $post instanceof WP_Post ) ) {
    $post = get_post( $post );
  }

  if ( ! $post ) {
    return;
  }

  $id = (int) $post->ID;

  $authordata = get_userdata( $post->post_author );

  $currentday   = mysql2date( 'd.m.y', $post->post_date, false );
  $currentmonth = mysql2date( 'm', $post->post_date, false );
  $numpages     = 1;
  $multipage    = 0;
  $page         = $this->get( 'page' );
  if ( ! $page ) {
    $page = 1;
  }

  /*
   * Force full post content when viewing the permalink for the $post,
   * or when on an RSS feed. Otherwise respect the 'more' tag.
   */
  if ( $post->ID === get_queried_object_id() && ( $this->is_page() || $this->is_single() ) ) {
    $more = 1;
  } elseif ( $this->is_feed() ) {
    $more = 1;
  } else {
    $more = 0;
  }

  $content = $post->post_content;  //コンテンツ
  if ( false !== strpos( $content, '<!--nextpage-->' ) ) {
    $content = str_replace( "\n<!--nextpage-->\n", '<!--nextpage-->', $content );
    $content = str_replace( "\n<!--nextpage-->", '<!--nextpage-->', $content );
    $content = str_replace( "<!--nextpage-->\n", '<!--nextpage-->', $content );

    // Remove the nextpage block delimiters, to avoid invalid block structures in the split content.
    $content = str_replace( '<!-- wp:nextpage -->', '', $content );
    $content = str_replace( '<!-- /wp:nextpage -->', '', $content );

    // Ignore nextpage at the beginning of the content.
    if ( 0 === strpos( $content, '<!--nextpage-->' ) ) {
      $content = substr( $content, 15 );
    }

    $pages = explode( '<!--nextpage-->', $content );
  } else {
    $pages = array( $post->post_content );
  }

  /**
   * Filters the "pages" derived from splitting the post content.
   *
   * "Pages" are determined by splitting the post content based on the presence
   * of `<!-- nextpage -->` tags.
   *
   * @since 4.4.0
   *
   * @param string[] $pages Array of "pages" from the post content split by `<!-- nextpage -->` tags.
   * @param WP_Post  $post  Current post object.
   */
  $pages = apply_filters( 'content_pagination', $pages, $post );

  $numpages = count( $pages );

  if ( $numpages > 1 ) {
    if ( $page > 1 ) {
      $more = 1;
    }
    $multipage = 1;
  } else {
    $multipage = 0;
  }

  /**
   * Fires once the post data has been setup.
   *
   * @since 2.8.0
   * @since 4.1.0 Introduced `$this` parameter.
   *
   * @param WP_Post  $post The Post object (passed by reference).
   * @param WP_Query $this The current Query object (passed by reference).
   */
  do_action_ref_array( 'the_post', array( &$post, &$this ) );

  return true;
}

※ 以下は、Function Reference/setup postdata からの引用です。

『setup_postdata() はグローバル変数 $post を割り当てない(更新しない)ので、自分でそれを行う必要があります。そうしないとグローバル変数 $post と関連する他のグローバル変数($id, $authordata など)を使うフックなどが正常に機能しなくなります』というような内容です。

このことは、逆に setup_postdata() を使う場合は、引数にグローバル変数 $post を指定すれば問題ないということになるかと思いますが、グローバル変数の扱いには注意が必要です。

setup_postdata() does not assign the global $post variable so it's important that you do this yourself. Failure to do so will cause problems with any hooks that use any of the above globals in conjunction with the $post global, as they will refer to separate entities.
グローバル変数 $post

global $post は現在の投稿情報を保持するグローバル変数です。

グローバル変数 $wp_query にはメインクエリ(ページ読み込み時に自動的に取得した表示する記事やページなどの全てデータ)が入っていますが、グローバル変数 $post には現在(表示対象)の投稿のデータが入っています。

主なグローバル変数

  • $wp_query:現在のメインクエリ(WP_Query オブジェクト)。
  • $wp_the_query:メインクエリのバックアップ用(WP_Query オブジェクト)。
  • $posts: クエリを実行して取得した記事(投稿)オブジェクトの配列。
  • $post: 現在の記事(投稿)オブジェクト。

グローバル変数 $post は the_post() 関数によって上書きされます。

以下のような記述で、ループの外側(開始前と終了後)及びループの内側でグローバル変数 $post の ID が変化するのを確認することができます。

$wp_query_post_id は $wp_query の post プロパティ($wp_query->post)の ID の値です。

<?php   //ループの外側(開始前)
  global $wp_query;
  $my_post = $wp_query->post;
  $wp_query_post_id = $my_post->ID; //$wp_query の post プロパティの ID
  echo '<p>$wp_query->post のID:'. $wp_query_post_id. '</p>';
  global $post;
  echo '<p>global $post のID:'. $post->ID. '</p>';//$post の ID
?>

<?php if(have_posts()) : ?>
  <?php while(have_posts()) : the_post(); ?>
   <?php  //ループの内側
    $my_post = $wp_query->post;
    $wp_query_post_id = $my_post->ID;
    echo '<p>$wp_query->post のID:'. $wp_query_post_id. '</p>';
    echo '<p>global $post のID:'. $post->ID. '</p>';
   ?>
  <?php endwhile; ?>
<?php endif; ?>

<?php  //ループの外側(終了後)
  $my_post = $wp_query->post;
  $wp_query_post_id = $my_post->ID;
  echo '<p>$wp_query->post のID:'. $wp_query_post_id. '</p>';
  echo '<p>global $post のID:'. $post->ID. '</p>';
?>

投稿の個別ページの場合(single.php)は、記事は一件だけなので出力される ID は全て同じになりますが、アーカイブページなどでは、ループ内ではそれぞれの ID は表示されるている記事により変化します。

<!-- 個別ページの場合の例(single.php) -->
<p>$wp_query->post のID:94</p><p>global $post のID:94</p>
<!-- ループ開始(個別ページ一件のみ) -->
<p>$wp_query->post のID:94</p><p>global $post のID:94</p>
<!-- ループ終了 -->
<p>$wp_query->post のID:94</p><p>global $post のID:94</p>

ループの前後でのグローバル変数 $post の中身

但し、アーカイブページなど複数の記事(投稿)がある場合のループの終了後では、$wp_query の post プロパティ($wp_query->post)の ID の値は初期値に戻ります(rewind_posts())が、グローバル変数 $post の ID はループの最後の ID となりこれらは一致しません。

このことから、ページの種類によってはグローバル変数 $post の中身はループの前後で異なる場合があることがわかります。

<!-- カテゴリーページの場合の例(category.php) -->
<p>$wp_query->post のID:94</p><p>global $post のID:94</p>
<!-- ループ開始(3件の投稿) -->
<p>$wp_query->post のID:94</p><p>global $post のID:94</p>
<p>$wp_query->post のID:72</p><p>global $post のID:72</p>
<p>$wp_query->post のID:41</p><p>global $post のID:41</p>
<!-- ループ終了 -->
<p>$wp_query->post のID:94</p><p>global $post のID:41</p>

wp_reset_postdata() 関数を使うとグローバル変数 $post を復元することができます。初期状態のグローバル変数 $post をテンプレートの先頭で別の変数名でコピーしておけば必要に応じてその変数から情報を取得することもできます。

グローバル変数 $post のプロパティ

グローバル変数 $post の中身(プロパティ)は var_dump() や print_r() を使って確認することができます。

<pre><?php var_dump($post); ?></pre>

以下は個別ページでのグローバル変数 $post の出力例です。

object(WP_Post)#4778 (24) {  //WP_Post(投稿オブジェクト)
  ["ID"]=>
  int(94)
  ["post_author"]=>
  string(1) "1"
  ["post_date"]=>
  string(19) "2019-03-10 13:21:15"
  ["post_date_gmt"]=>
  string(19) "2019-03-10 04:21:15"
  ["post_content"]=>
  string(70) "テストです。"
  ["post_title"]=>
  string(21) "コメントテスト"
  ["post_excerpt"]=>
  string(0) ""
  ["post_status"]=>
  string(7) "publish"
  ["comment_status"]=>
  string(6) "closed"
  ["ping_status"]=>
  string(6) "closed"
  ["post_password"]=>
  string(0) ""
  ["post_name"]=>
  string(12) "comment-test"
  ["to_ping"]=>
  string(0) ""
  ["pinged"]=>
  string(0) ""
  ["post_modified"]=>
  string(19) "2019-03-15 08:41:17"
  ["post_modified_gmt"]=>
  string(19) "2019-03-14 23:41:17"
  ["post_content_filtered"]=>
  string(0) ""
  ["post_parent"]=>
  int(0)
  ["guid"]=>
  string(26) "http://localhost/wp5/?p=94"
  ["menu_order"]=>
  int(0)
  ["post_type"]=>
  string(4) "post"
  ["post_mime_type"]=>
  string(0) ""
  ["comment_count"]=>
  string(1) "0"
  ["filter"]=>
  string(3) "raw"
}

以下はグローバル変数 $post のプロパティです。

グローバル変数 $post のプロパティ
プロパティ名 データ型 意味
ID int 投稿の ID
post_author string 投稿者の ID
post_date string 投稿日時
post_date_gmt string 投稿日時(GMT)
post_content string 投稿の内容(コンテンツ)
post_title string 投稿のタイトル
post_excerpt string 投稿の内容(抜粋)
post_status string 投稿の状態("publish"、"private"など)
comment_status string コメントの投稿状態("open"など)
ping_status string ピンバックの状態("open"など)
post_password string 投稿のパスワード(設定している場合)
post_name string スラッグ / 投稿名
to_ping string ピン通知 URL
pinged string トラックバックの送信先
post_modified string 更新日時
post_modified_gmt string 更新日時(GMT)
post_content_filtered string
post_parent int 親のポストID(0 は最上位の親)
guid string URL(パーマリンク)
menu_order int 表示順序
post_type string 投稿タイプ("post", "page", "attachment"など)
post_mime_type string 投稿(attachment の場合)の MIME タイプ(image/png等)
comment_count int コメント数
ancestors array 先祖のID(配列で保持)階層型ポストの場合
filter string 適用されたフィルター名/サニタイズの形式("raw"など)

pre_get_posts(メインクエリを変更)

メインクエリを変更する方法としては pre_get_posts フックを使う方法と query_posts() 関数を使う方法がありますが、query_posts() はテーマで使うことを想定されていないため(メインクエリ = $wp_query の値を書き換えてしまうため)、 query_posts() を使う方法は非推奨となっています。そのため現在は query_posts() を使用しません。

pre_get_posts は wp-includes/class-wp-query.php の WP_Query クラスのメソッド get_posts() の定義で設定されているフックで、WordPress がクエリーを取得する前に実行されるアクションフックです。

そのため pre_get_posts を使用すれば、それぞれのページに合ったクエリの結果をメインクエリとして扱うことができるようになります。

pre_get_posts は functions.php に記述します(テンプレートファイル内では動作しません)。

以下は、カテゴリーアーカイブページの場合に表示件数(posts_per_page)を15件に変更する例です。

1行目は pre_get_posts アクションフックの登録で、クエリを設定(メインクエリを変更)する関数を指定します。

2行目~11行目がクエリを設定する関数の定義です。

7行目はクエリの設定です。

add_action( 'pre_get_posts', 'my_pre_get_posts_category' );
function my_pre_get_posts_category( $query ) {
  if ( ! is_admin() && $query->is_main_query() ) {
    // 管理画面のページではなく、且つメインクエリの場合のみに実行
    if ( $query->is_category() ) {
      // カテゴリーアーカイブページの場合にクエリを変更
      $query->set( 'posts_per_page', 15 );
      // 表示件数を15件に変更
    }
  }
}

以下のように記述しても同じことになります。

add_action( 'pre_get_posts', 'my_pre_get_posts_category' );
function my_pre_get_posts_category( $query ) {
  if ( is_admin() || ! $query->is_main_query() ){  //条件を反転
    // 管理画面のページやメインクエリ以外の場合は何もしない
    return;
  }
  if ( $query->is_category() ) {
    // カテゴリーアーカイブページの場合に条件を変更
    $query->set( 'posts_per_page', 15 );
    // 表示件数を15件に変更
  }
}
pre_get_posts の注意点

以下は pre_get_posts を使用する際の注意点です。

変更するクエリを特定するために、is_admin() 条件分岐タグと $query->is_main_query() 関数を使用することが推奨されています。

上記の例では、「! is_admin() && $query->is_main_query()」で、「管理画面のページではなく、且つメインクエリの場合のみに実行」と言う条件を指定しています。

  • is_main_query():クエリがメインクエリかどうか(標準のクエリかサブクエリか)を判定する関数
    ※is_main_query() はグローバル変数 $wp_query に対して判定するので、pre_get_posts を使用する際は必ず $query->is_main_query() とする必要があります。
  • is_admin():管理画面の表示中かどうかを判定する関数

これらの記述がないと管理画面や他のカスタムループ(サブループの get_posts() など)に影響が出てしまうので注意が必要です。

上記の例の5行目 $query->is_category() のように条件分岐タグを使ってページを限定します。限定しない場合、全てのページで条件が変更されてしまいます。

固定ページでは使えない

pre_get_posts は単一の固定ページのページテンプレートに対するクエリを変更するのに用いるべきではないとされています。以下は WordPress のページからの抜粋です。

pre_get_posts は単一の固定ページのリクエスト(ページテンプレート)に対するクエリを変更するのに用いるべきではありません。なぜなら 'is_page'、'is_singular'、'pagename' および他のプロパティ(pretty パーマリンクを使っているかどうかによる)が parse_query() メソッドによってセットされた後だからです。詳しくは クエリ概要 を見てください。

これと同様に、pre_get_posts はテンプレートファイル(例えば archive.php)内では動作しません。なぜならクエリが完了した後だからです。

これらの設定に精通していて、かつ設定を調整しようと望むのでなければ、ページテンプレート内で new WP_Query を使って単一の固定ページのリクエスト(ページテンプレート)に対するクエリを変更するのを推奨します。

条件分岐関数に注意

pre_get_posts は WP_Query がセットアップされるより前に実行されるため、WP_Query に依存するいくつかのテンプレートタグや条件分岐関数は動作しないので注意が必要です。例えば、is_front_page() は動作しませんが、is_home() は動作します。

pre_get_posts フックに引数として渡されるクエリ変数($query)を var_dump() などで確認すると is_home の項目はありますが、is_front_page はありません。var_dump() で確認して使えそうな値を利用して条件分岐する必要があるかもしれません。例えば page_id は取得できるのでそれを使ってページを特定するなど。

以下は pre_get_posts フックで var_dump() で出力した WP_Query オブジェクト ($query) の例です。

クエリの設定方法

クエリの設定は以下のように指定します。$query->set は必要に応じて複数(好きなだけ)指定することができます。

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

パラメータには「WP_Query パラメータ」の値のほとんどを指定することができると思います(未確認)。

値が複数ある場合は、配列で指定します。

$query->set( 'post_type', array( 'post', 'news' ) )

以下はページの種類に応じて表示件数を変更する例です。

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

  if ( $query->is_home() ) {
    // ブログインデックスページでは1件のみ
    $query->set( 'posts_per_page', 1 );
    return;
  }
  if ( $query->is_post_type_archive( 'movie' ) ) {
    // カスタム投稿タイプ 'movie' の場合は50件表示
    $query->set( 'posts_per_page', 50 );
    return;
  }
}
add_action( 'pre_get_posts', 'hwl_home_pagesize', 1 );

以下のように elseif を使って記述しても同じです。

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

  if ( $query->is_home() ) {
    // ブログインデックスページでは1件のみ
    $query->set( 'posts_per_page', 1 );
  } elseif ( $query->is_post_type_archive( 'movie' ) ) {
    // カスタム投稿タイプ 'movie' の場合は50件表示
    $query->set( 'posts_per_page', 50 );
  }
}
add_action( 'pre_get_posts', 'hwl_home_pagesize', 1 );

また、is_main_query はメインクエリーを変更するための方法なので、1ページの中に複数のループを使う場合は何をメインにするかを決めて、メイン以外はサブループを使います。

サブループ

WordPress の動作シーケンスは大まかに以下の4つの処理順に分けることができます。

1~3 は、WordPress 本体の動作(テンプレートからページへの変換)で、4がテーマに関する部分です。

  1. パーマリンクの設定を元に URL の解析(ページ種類の決定)
  2. ページ種類により決定された投稿タイプの記事の抽出(グローバル変数 $wp_query の生成など)
  3. テンプレート階層によるテンプレートファイルの選択
  4. テンプレートファイル内のメインループの記述により 2.で抽出された記事の出力

複数の投稿タイプや異なる抽出条件が必要な場合は、サブループ(マルチループ)の手法を使います。

メインループで記事の抽出条件が足りない場合には、pre_get_posts フィルタを使ってメインループ用の抽出条件を変更することができます。

pre_get_posts フィルタは上記動作シーケンスの2.の段階に割り込んで抽出条件を変更し、その抽出結果を使ってメインループ処理を行うので、テンプレートファイルを変更することなく出力する記事を変更することができます。ページによって記事の取得件数や並び順を変更したりする場合などに有効です。

WordPress が自動的に取得したデータ(メインクエリ/$wp_query)とは異なる条件のデータ(サブクエリ)を使って表示するループをサブループや Secondary Loops と呼びます。

複数のループ(マルチループ)を使用する際に、メインループとは異なるループを処理するには WP_Query クラスget_posts() を使います。

WP_Query

WP_Query クラスから生成したクエリオブジェクトを利用してループを処理する方法です。

メインクエリも WP_Query オブジェクト($wp_query)を使いますが、それとは別に WP_Query オブジェクトを生成します。そのためメインクエリとほぼ同じようなことができますが、ループ関数の記述では生成したオブジェクト(インスタンス)のメソッドを使用します。

この方法はグローバル変数 $post を書き換えるので最後に wp_reset_post_data() を実行して書き換えられた $post の値を元に戻します。

以下が WP_Query クラスを使ったサブループの例です。

ループはメインループの場合とほぼ同じ処理になっていますが、WP_Query クラスのメソッドを使う必要があるので、 $インスタンスの変数名->have_posts(); のようにします。

  • 配列 $args に投稿を取得するための条件を指定
  • 配列 $args を引数に与えて WP_Query クラスのインスタンスを生成し変数($my_query)に代入
  • 生成した WP_Query クラスのインスタンスを使ってループを実行
  • ループでは have_posts()the_post() のループ関数は($wp_query が呼び出すのではなく)インスタンスのメソッドとして実行(例: $my_query->have_posts();)
  • ループ終了後の処理として wp_reset_postdata() を実行(必須)
    ※ $my_query->the_post() がグローバル変数 $post を上書きするのでリセットが必要
<?php
$args = array(
    'プロパティ' => 値,
    'プロパティ' => 値,
    // 取得する投稿の条件を指定
);
$my_query = new WP_Query( $args ); // WP_Query オブジェクト(クエリオブジェクト)の生成
?>

<?php if ( $my_query->have_posts() ) : ?>
    <?php while ( $my_query->have_posts() ) : $my_query->the_post(); ?>
    <!-- ループ内の処理-->
    <?php endwhile; ?>
<?php endif; ?>
<?php wp_reset_postdata(); //グローバル変数 $post をリセット(※必須) ?>

以下は「投稿タイプをカスタム投稿タイプ my-portfolio、1ページあたりに取得する記事数を 3」という条件を指定した WP_Query クラスのインスタンスを生成して、サブループでタイトルとそのリンクを出力する例です。

出力する内容を if ( $my_query->have_posts() ) : 以降に記述することで、もし該当する記事が無い場合は何も表示しないようにしています。

また、ページ送りを使用する場合は、posts_per_page と共に 'paged' パラメータを指定します。静的フロントページの場合は 'paged' の代わりに 'page' を使います。(ページ送りパラメータ

<?php
$paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
$args = array(
  'post_type' => 'my-portfolio',
  'posts_per_page' => '3',
  'paged' => $paged
);
$my_query = new WP_Query( $args );
?>

<?php if ( $my_query->have_posts() ) : ?>
<h2>最近のポートフォリオ</h2>
  <?php while ( $my_query->have_posts() ) : $my_query->the_post(); ?>
    <h3><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
  <?php endwhile; ?>
  <?php next_posts_link( '次の10件へ', $my_query->max_num_pages ); ?>
  <?php previous_posts_link( '前の10件へ' ); ?>
<?php endif; ?>
<?php wp_reset_postdata(); ?>

静的フロントページでページャーやページネーションなどページ送りの機能を使う場合は 'paged' の代わりに 'page' を使います。

<?php
//静的フロントページの場合は 'page' を使う
$paged = ( get_query_var('page') ) ? get_query_var('page') : 1;
$args = array(
  'post_type' => 'rental',
  'posts_per_page' => '3',
  'paged' => $paged,
);
$my_query = new WP_Query( $args );
?>

この方法(WP_Query を使ったサブループ)は the_post() を使うのでグローバル変数 $post(メインクエリの投稿オブジェクト)を書き換えるため、最後に wp_reset_post_data() を実行して書き換えられた $post の値を元に戻す必要があります。

wp_reset_postdata()

wp_reset_postdata() は wp-includes/query.php で定義されています。

wp_reset_postdata() はグローバル変数 $wp_query の reset_postdata() を呼び出しています。サブループの実行後この関数はグローバル変数の $post をメインクエリの現在の post に戻してリセットします(reset_postdata() で $GLOBALS['post'] に $wp_query の post を代入)。

/**
* After looping through a separate query, this function restores
* the $post global to the current post in the main query.
* @since 3.0.0
* @global WP_Query $wp_query Global WP_Query instance.
*/
function wp_reset_postdata() {
  global $wp_query;

  if ( isset( $wp_query ) ) {
    $wp_query->reset_postdata();  //WP_Query クラスの reset_postdata()
  }
}
reset_postdata()

WP_Query クラスの reset_postdata() は wp-includes/class-wp-query.php で定義されています。

/**
* After looping through a nested query, this function
* restores the $post global to the current post in this query.
* @since 3.7.0
* @global WP_Post $post
*/
public function reset_postdata() {
  if ( ! empty( $this->post ) ) {
    $GLOBALS['post'] = $this->post;
    $this->setup_postdata( $this->post );
  }
}
WP_Query パラメータ

クエリオブジェクトを生成する際に WP_Query や get_posts() の引数($args)に指定する(投稿を取得するための条件で指定できる)パラメータには以下のようなものがあります。

参考サイト:

WP_Query、get_posts() で指定できるパラメータ
キー キーと値の例 説明
p 投稿のID 'p'=>100 指定したIDの投稿を読み込む
name 投稿のスラッグ 'name'=>'wordpress' 指定したスラッグの投稿を読み込む
post__in 複数の投稿のIDからなる配列 'post__in'=>array(1,3) 指定したIDの投稿群を読み込む
post__not_in 複数の投稿のIDからなる配列 'post__not_in'=>array(1,3) 指定したIDの投稿を除外した残りの投稿群を読み込む
post_status 投稿の状態(デフォルト値は 'publish')
publish:公開
private:非公開
draft:下書き
future:予約
pending:レビュー待ち
trash:ゴミ箱
auto-draft:自動保存
inherit:継承(子の投稿(添付ファイルやリビジョン)に割り当てられ、実際のステータスは親投稿のステータスによって決まる)
'post_status'=>'draft" 指定した状態の投稿を読み込む
post_type 投稿タイプ(デフォルト値は 'post')
post: 投稿
page: 固定ページ
カスタム投稿タイプ名:カスタム投稿タイプ
revision:履歴 (リビジョン)
attachment:メディア
any:履歴と 'exclude_from_search' がセットされているのも以外すべて
'post_type' => 'page'
'post_type' =>array('foo', 'bar')
(複数の投稿タイプの指定の例)
指定した投稿タイプを読み込む
posts_per_page 整数(-1を指定するとすべてのページを読み込む) 'posts_per_page' => 3 読み込む件数を指定
指定しなければ「設定」→「表示設定」の指定によって決まる
paged 整数 'paged' => $paged ページ番号 next_posts_link() などのページ送り(ページネーションを含む)を使用する場合、ページ番号が入っていないと、常に1ページ目を取得しようとするので'paged' => $pagedとする
post_parent 親ページの投稿のID 'post_parent' => get_the_ID() 親ページの投稿のIDを指定してその子ページを読み込む
page_id 固定ページのID 'page_id'=>100 指定したIDの固定ページを読み込む
pagename 固定ページのスラッグ 'pagename'=>'wordpress'
'pagename'=>'parent_slug/child_slug'
指定したスラッグの固定ページを読み込む。親ページのスラッグと子ページのスラッグをスラッシュで区切って指定することもできる。
cat カテゴリーのID 'cat'=>100 指定したIDのカテゴリーに属する投稿を読み込む。IDをマイナスで指定すると、そのカテゴリーに属する投稿を除外することも可能
category_name カテゴリーのスラッグ 'category_name'=>'wordpress' 指定したスラッグのカテゴリーに属する投稿を読み込む。
category__in カテゴリーのIDの配列 'category__in'=>array(1,3) 指定したIDのカテゴリーのどれか1つに属する投稿を読み込む
category__not_in カテゴリーのIDの配列 'category__not_in'=>array(1,3) 指定したIDのカテゴリーのどれにも属さない投稿を読み込む
category__and カテゴリーのIDの配列 'category__and'=>array(1,3) 指定したIDのすべてのカテゴリーに属する投稿を読み込む
taxonomy カスタム分類名 'taxonomy' => 'カスタム分類名' 指定したカスタム分類名に属する投稿を読み込む
カスタムタクソノミー名 タクソノミースラッグ 'カスタムタクソノミー名' => 'タクソノミースラッグ'
'people' => 'bob'
'people' カスタムタクソノミーで 'bob' というスラッグのタグがつけられた投稿を表示
指定したカスタムタクソノミーのタクソノミースラッグに属する投稿を読み込む
tax_query 投稿情報を絞り込むための指定の配列 tax_query パラメータを参照 指定した投稿を読み込む
tag タグのスラッグ 'tag'=>'wordpress' 指定したスラッグのタグが付いている投稿を読み込む。
tag__in タグのIDの配列 'tag__in'=>array(1,3) 指定したIDのタグがどれか1つでも付いている投稿を読み込む
tag__not_in タグのIDの配列 'tag__not_in'=>array(1,3) 指定したIDのタグがどれも付いていない投稿を読み込む
tag__and タグのIDの配列 'tag__and'=>array(1,3) 指定したIDのタグがすべて付いている投稿を読み込む
tag_slug__in タグのスラッグの配列 'tag_slug__in'=>array('template', 'plugin') 指定したスラッグのタグがどれか1つ付いている投稿を読み込む
tag_slug__and タグのスラッグの配列 'tag_slug__and'=>array('template', 'plugin') 指定したスラッグのタグがすべてすべて付いている投稿を読み込む
author ユーザーのID 'author'=>1 指定したIDのユーザーが書いた投稿を読み込む。IDをマイナスで指定すると、そのユーザーが書いた投稿を除外できる。
author_name ユーザーのナイスネーム 'author_name'=>'taro' 指定したナイスネームのユーザーが書いた投稿を読み込む。
year 'year'=>2019 指定した年に書いた投稿を読み込む
monthnum 'monthnum'=>1 指定した月に書いた投稿を読み込む
day 'day'=>1 指定した日に書いた投稿を読み込む
w 週番号 'w'=>1 指定した週番号の週にに書いた投稿を読み込む
hour 'hour'=>1 指定した時に書いた投稿を読み込む
minute 'minute'=>1 指定した分に書いた投稿を読み込む
second 'second'=>1 指定した秒に書いた投稿を読み込む
order 'DESC' (デフォルト)、'ASC' 'order' => 'ASC' 投稿を読み込む際に、その並び順(昇順か降順か)を指定する
orderby author :ユーザーのID
date :日付(デフォルト)
title :タイトル
menu_order :「順序」欄で設定した値(固定ページ)
modified: 最終更新日時
parent :親固定ページのID
ID :投稿(ページ)のID
rand :ランダム
meta_value :カスタムフィールドの値
comment_count :コメント数
none :並べ替えなし
'orderby' => 'title' 投稿を読み込む際に、その並び順(ソートする項目)を指定する
offset (整数)- 投稿の先頭からスキップする件数を指定 'offset' => 1 表示される投稿の先頭から指定した件数をスキップする
meta_key カスタムフィールドのキー 'meta_key' => 'color' カスタムフィールドの特定のキーを持つ記事を取得するように指定
meta_value カスタムフィールドの値 'meta_value' => 'blue' カスタムフィールドの特定の値を持つ記事を取得するように指定
meta_compare カスタムフィールドを比較するための演算子(WP_Query 参照) 'meta_compare' => "!=' 「カスタムフィールドの値が○○より大きい」など、大小比較を行うことができる(数値の比較には使えない)
ignore_sticky_posts 真偽値。デフォルトは0(無視しない) 'ignore_sticky_posts' => 1 先頭固定投稿の設定を無視するかどうか
tax_query パラメータ
キー 説明
taxonomy 対象とするタクソノミー タクソノミー名('category'、'post_tag'、'post_format'なども指定可能)
field terms に指定するフィールド term_id(デフォルト), slug, name, term_taxonomy_id
terms 対象とするタームの値 ターム名、スラッグ、ターム id など 。複数ある場合は配列で指定。
include_children 階層化タクソノミーの場合、子供を含めて演算 するかどうか true(デフォルト), false
operator ターム間の演算子 IN(デフォルト), NOT IN, AND(大文字)
relation タクソノミー間の演算子 AND(デフォルト), OR(大文字)

以下は tax_query パラメータの指定例です。

$args = array(
  'tax_query' => array(
    array(
     'taxonomy' => 'people',
     'field' => 'slug',
     'terms' => 'bob'
    )
  )
);
$my_query = new WP_Query( $args );
投稿の並び順を変更

投稿の並び順を変更するには、投稿を読み込む際の並び順を指定するパラメータ(order や orderby)を使用します。

order(昇順か降順かを指定)
意味
ASC 小さい方から大きい方へ並べる(昇順:番号順、名前順、小さい順、古い順など)
DESC (デフォルト)大きい方から小さい方へ並べる(降順:10→1、Z→A、を→あ、日付の新しい順など)
orderby(ソートする項目を指定)※一部抜粋
意味
author 著者(ユーザーのID)で並び替え
comment_count コメント数で並び替え
date (デフォルト)日付で並び替え
ID 投稿 ID で並び替え
menu_order 「順序」欄で設定した値(固定ページや添付ファイル)で並び替え
meta_value カスタムフィールドで並び替え。meta_key=keyname がクエリに必要。
modified 最終更新日時で並び替え
title タイトルで並び替え

参考サイト:関数リファレンス/順序づけパラメータ

以下は、カテゴリーが events の投稿を5件、最終更新日時の新しい順で取得して表示する例です。

order(昇順か降順かを指定)は指定していないので、デフォルトの DESC(この場合は新しい順)が適用されます。

<?php
$args = array(
  'orderby' => 'modified',  //最終更新日時で並び替え
  'posts_per_page' => '5',  //5件表示
  'category_name'=>'events'  //カテゴリーが events
);
$my_query = new WP_Query( $args );
?>
<?php if ( $my_query->have_posts() ) : ?>
  <ul>
  <?php while ( $my_query->have_posts() ) : $my_query->the_post(); ?>
    <li><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></li>
  <?php endwhile; ?>
  </ul>
<?php endif; ?>
<?php wp_reset_postdata(); ?>  

以下は、カテゴリーが events の投稿を5件、一番古い投稿を除外して日付の古い順で取得して表示する例です。

<?php
$args = array(
  'order' => 'ASC',  //昇順(古い順)
  'posts_per_page' => '5',
  'category_name'=>'events',
  'offset' => 1 //一番古い投稿を除外
);
$my_query = new WP_Query( $args );
?>
<?php if ( $my_query->have_posts() ) : ?>
  <ul>
  <?php while ( $my_query->have_posts() ) : $my_query->the_post(); ?>
    <li><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></li>
  <?php endwhile; ?>
  </ul>
<?php endif; ?>
<?php wp_reset_postdata(); ?>  

最新の投稿を古い順に取得

最新の投稿を指定した件数、古い順に取得するには、以下のように2段階で取得することができます(もっと良い方法があるかも知れません)。

最初にすべての投稿を取得して found_posts で投稿の全件数を取得します。

そして一度 wp_reset_postdata() で $post の値を元に戻して、再度 WP_Query を使ったサブループの処理を実行します。

その際に、offset(投稿の先頭からスキップする件数)に「投稿の全件数から表示する件数を引いた値」を指定して 昇順(古い順)で投稿を取得します。

以下は最新の投稿を5件、古い順に表示する例です。

<?php
//すべての投稿を取得
$my_query = new WP_Query( array(
  'post_type' => 'post',
) );
//投稿の全件数
$my_found_posts = $my_query->found_posts;
//$post の値を元に戻す
wp_reset_postdata();

//表示する件数を指定
$my_posts_per_page = 5;

$args = array(
  //投稿の全件数から表示する件数を引いた値をオフセットに指定
  'offset' => $my_found_posts - $my_posts_per_page,
  'posts_per_page' => $my_posts_per_page,
  'order' => 'ASC',   //昇順(古い順)
);
$my_query = new WP_Query( $args );
?>
<?php if ( $my_query->have_posts() ) : ?>
  <ul>
  <?php while ( $my_query->have_posts() ) : $my_query->the_post(); ?>
    <li><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></li>
  <?php endwhile; ?>
  </ul>
<?php endif; ?>
<?php wp_reset_postdata(); ?>  

以下はカテゴリーが events の最新の投稿を5件、古い順に取得する例です。

<?php
//すべての投稿を取得
$my_query = new WP_Query( array(
  'post_type' => 'post',
  'category_name'=>'events', //カテゴリーを指定
) );
//カテゴリーが events の投稿の全件数
$my_found_posts = $my_query->found_posts;
//$post の値を元に戻す
wp_reset_postdata();

//表示する件数を指定
$my_posts_per_page = 5;

$args = array(
  //カテゴリーが events の投稿の全件数から表示する件数を引いた値をオフセットに指定
  'offset' => $my_found_posts - $my_posts_per_page,
  'posts_per_page' => $my_posts_per_page,
  'category_name'=>'events', //カテゴリーを指定
  'order' => 'ASC',  //昇順(古い順)
);
$my_query = new WP_Query( $args );
?>
get_posts()

get_posts() は、指定されたパラメータに基づいて投稿データの配列を読み込みます。複数の投稿を読み込むことができます。

投稿を1件だけ読み込む場合は、get_post() を使うことができます。

また、サブループを作成するのに get_posts() を使用する事もできます。

内部的には WP_Query オブジェクトを作成して投稿を読み込むので、パラメータの指定方法は、WP_Query オブジェクトの場合とほぼ同じですが、WP_Query オブジェクトにはないパラメータもいくつか用意されています。

get_posts( $args )

パラメータ
$args(配列):(オプション)引数の配列。
WP_Query のパラメータ及び get_posts 関数特有のパラメータ参照。
戻り値
投稿のオブジェクト(または投稿 ID)の配列
利用可能箇所
どこでも

以下は get_posts() 特有のパラメータです。

get_posts 関数特有のパラメータ
キー キーと値の例 動作
numberposts
または
posts_per_page
投稿の数 'numberposts'=>10 指定した件数の投稿を読み込む。-1を指定すると、すべての投稿を読み込む。デフォルトの値は5 (posts_per_page に対応)
category カテゴリーのID 'category'=> 1 指定したIDのカテゴリーに属する投稿を読み込む。IDにマイナスの数値を渡すと、そのIDのカテゴリーを除外した投稿を読み込む。(複数指定する場合はカンマ「,」で区切る) (cat に対応)
include 投稿のIDをコンマで区切った文字列か、IDの配列 'include'=> '1,2,3' 指定したIDの投稿のみを読み込む (post__in に対応)
exclude 投稿のIDをコンマで区切った文字列か、IDの配列 'exclude'=>array(1,2,3) 指定したIDの投稿を除外して読み込む (post__not_in に対応)
suppress_filters フィルタを無効にするかどうかの真偽値 'suppress_filters'=> false 初期値は null('suppress_filters' => true)フィルタリングが無効

get_posts() は wp-includes/post.php に以下のように定義されています。

WP_Query オブジェクトを生成して変数 $get_posts に入れて、その query() メソッドの実行結果(投稿オブジェクトの配列)だけを返しています。

このため get_posts() は記事データを扱うことはできますが、条件分岐タグやその他のパラメーターを扱うことはできません。より複雑な処理が必要な場合は、WP_Query でのサブループを使います。

/**
* Retrieves an array of the latest posts, or posts matching the given criteria.
* The defaults are as follows:
* @since 1.2.0
* @see WP_Query::parse_query()
* @param array $args {
*  Optional. Arguments to retrieve posts. See WP_Query::parse_query() for all available arguments.
*
*  @type int $numberposts  Total number of posts to retrieve. Is an alias of $posts_per_page in WP_Query. Accepts -1 for all. Default 5.
*  @type int|string $category  Category ID or comma-separated list of IDs (this or any children).Is an alias of $cat in WP_Query. Default 0.
*  @type array $include An array of post IDs to retrieve, sticky posts will be included. Is an alias of $post__in in WP_Query. Default empty array.
*  @type array $exclude An array of post IDs not to retrieve. Default empty array.
*  @type bool  $suppress_filters Whether to suppress filters. Default true.
* }
* @return WP_Post[]|int[] Array of post objects or post IDs.
*/
function get_posts( $args = null ) {
  $defaults = array(  //パラメータのデフォルト
    'numberposts'      => 5,  //取得件数
    'category'         => 0,
    'orderby'          => 'date',
    'order'            => 'DESC',
    'include'          => array(),
    'exclude'          => array(),
    'meta_key'         => '',
    'meta_value'       => '',
    'post_type'        => 'post',  //投稿タイプ
    'suppress_filters' => true,  //フィルタリング無効
  );

  $r = wp_parse_args( $args, $defaults ); //引数の配列とデフォルト値の配列を結合
  if ( empty( $r['post_status'] ) ) {
    $r['post_status'] = ( 'attachment' == $r['post_type'] ) ? 'inherit' : 'publish';
  }
  // get_posts() 特有のパラメータが指定されていればそれらを WP_Query 用に変換
  if ( ! empty( $r['numberposts'] ) && empty( $r['posts_per_page'] ) ) {
    $r['posts_per_page'] = $r['numberposts'];
  }
  if ( ! empty( $r['category'] ) ) {
    $r['cat'] = $r['category'];
  }
  if ( ! empty( $r['include'] ) ) {
    $incposts            = wp_parse_id_list( $r['include'] );
    $r['posts_per_page'] = count( $incposts );  // only the number of posts included
    $r['post__in']       = $incposts;
  } elseif ( ! empty( $r['exclude'] ) ) {
    $r['post__not_in'] = wp_parse_id_list( $r['exclude'] );
  }

  $r['ignore_sticky_posts'] = true;
  $r['no_found_rows']       = true;

  $get_posts = new WP_Query;  //WP_Query オブジェクトを生成
  return $get_posts->query( $r );  //query() メソッドの実行結果を返す

}

以下は get_posts() をパラメータなしで実行して取得した結果(オブジェクトの配列)を var_dump() で表示する例です。

<?php
$my_posts = get_posts();
var_dump($my_posts);
?>

以下は出力例です。$wp_query とは異なり、投稿のオブジェクトのみが取得されています。

(デフォルトのパラメータが適用され、投稿タイプが post のオブジェクトが5件)

array(5) {  //5つの投稿(WP_Post)オブジェクト
  [0]=>  // 1つ目の投稿オブジェクト
  object(WP_Post)#4499 (24) {
    ["ID"]=>int(307)
    ["post_author"]=>string(1) "1"
    ["post_date"]=>string(19) "2019-04-27 08:37:16"
    ["post_date_gmt"]=>string(19) "2019-04-26 23:37:16"
    ["post_content"]=>string(66) "投稿のコンテンツ容"
    ["post_title"]=>string(42) "投稿のタイトル"
    ・・・中略・・・
    ["post_type"]=>string(4) "post"  //投稿タイプ
    ・・・中略・・・
   }
  }
  [1]=>  // 2つ目の投稿オブジェクト
  object(WP_Post)#4545 (24) {
    ["ID"]=>int(230)
    ["post_author"]=>string(1) "1"
    ["post_date"]=>string(19) "2019-04-13 11:28:28"
    ・・・
    ["post_type"]=>string(4) "post"
    ・・・
  }
  [2]=>  // 3つ目の投稿オブジェクト
  object(WP_Post)#4560 (24) {
    ["ID"]=>int(221)
    ["post_author"]=>string(1) "1"
    ["post_date"]=>string(19) "2019-04-10 16:22:41"
    ・・・
    ["post_type"]=>string(4) "post"
    ・・・
   }
  }
  [3]=>  // 4つ目の投稿オブジェクト
  object(WP_Post)#4497 (24) {
    ["ID"]=>int(72)
    ["post_author"]=>string(1) "1"
    ["post_date"]=>string(19) "2019-01-11 10:16:37"
    ・・・
    ["post_type"]=>string(4) "post"
    ・・・
   }
  }
  [4]=>  // 5つ目の投稿オブジェクト
  object(WP_Post)#4556 (24) {
    ["ID"]=>int(41)
    ["post_author"]=>string(1) "1"
    ["post_date"]=>string(19) "2019-01-03 08:57:35"
    ・・・
    ["post_type"]=>string(4) "post"
    ・・・
  }
}

以下は get_posts() を使って「カテゴリー ID が1の記事3件のタイトルをリンクを付けて ul/li 要素のリストで出力する」サブループの例です。

get_posts() の戻り値(投稿のオブジェクトの配列)を変数に代入する際は、グローバル変数の「$posts」にしないように変数名を $posts 以外にします。

foreach 文で配列から投稿を1つずつ読み込んで投稿の数だけ処理を繰り返します(ループ)。

その際に setup_postdata() を使って投稿のプロパティ(抜粋やコンテンツを取得するテンプレートタグ)が使えるようにしています。

但し、この場合 setup_postdata() には引数としてグローバル変数 $post を指定する必要があるので、「foreach($my_posts as $post) : setup_postdata($post); 」としています。

そのため、グローバル変数 $post が上書きされるので終了後に wp_reset_postdata() でグローバル変数 $post を復元します。

<ul>
<?php
  $args = array(
    'category' => 1,
    'numberposts' => 3,
    //または 'posts_per_page' => 3,
  );

  $my_posts = get_posts($args);
  //$posts = とは書かない(グローバル変数 $posts を上書きしない)
  foreach($my_posts as $post) : setup_postdata($post); // ループ開始
  //グローバル変数 $post が上書きされるので終了後復元
?>
    <li><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></li>
  <?php endforeach; ?><!-- ループ終了 -->
</ul>
<?php wp_reset_postdata(); //$post を復元 ?>