WordPressのテーマを作ってる人は、テーマファイル内でいろんな独自の「ループ」を作っていることだろう。

ループとは、条件を指定してクエリから取得した複数の投稿をループ処理で表示する仕組みのことで、主にアーカイブページで使われる。

そんなループを作るとき、条件に合った投稿を取得するのに使うのが、クラス WP_Query と関数 get_posts() だ。

これらは相互に置き換えることができるとされているが、ループの作り方に違いがある。

今回は WP_Queryget_posts() の使い方の違いと、他に俺が気付いた違いについて紹介する。

ループの作り方の違い

WordPressにおいて投稿を取得するのは WP_Query の仕事だ。

get_posts() も内部では WP_Query を呼び出している。

しかし get_posts() が投稿データ(オブジェクト)の配列を返すのに対し、 WP_Query はクエリを実行するクラスを直接操作する。

もちろん扱いやすいのは get_posts() のほうだが、メインループ以外の独自のループ(サブループと呼ぶ)を作るときは WP_Query のほうが処理に無駄がないので、好みに合ったほうを使えばいい。

実際にループ処理を行うコードを比べてみよう。

まずは get_posts() から。

$args = array( /* 省略 */ );
$my_posts = get_posts( $args );
if ( $my_posts ) {
    foreach ( $my_posts as $post ) {
        setup_postdata( $post );
        the_title();
        the_content();
    }
    wp_reset_postdata();
}

取得する投稿の条件を指定する引数 $argsget_posts() でも WP_Query でも共通だ。

get_posts() では foreach でループを作り、 setup_postdata() で投稿データをセットする。

そうそう、ループ内でグローバル変数 $post を書き換えているので、ループ後に wp_reset_postdata() するのをお忘れなく(特に $post を再び使用する場合)。

次に WP_Query のループを見てみよう。

$args = array( /* 省略 */ );
$my_query = new WP_Query( $args );
if ( $my_query->have_posts() ) {
    while ( $my_query->have_posts() ) {
        $my_query->the_post();
        the_title();
        the_content();
    }
    wp_reset_postdata();
}

WP_Query では foreach の代わりに while でループを作る。

その評価式には WP_Query のメソッドである have_posts() を使用する。

また、投稿データをセットするのも setup_postdata() ではなく the_post() メソッドだ。

have_posts()the_post() も、メインループで使う関数としておなじみだが、そもそもこれらの関数は内部的にはメインクエリ $wp_query の同名メソッドを呼び出しているにすぎない。

すなわち WP_Query で独自のループを操作するのはメインループと同じ仕組みということだ。

一方 get_posts() では取得した投稿を配列として、要素ごとに処理を処理を行う。

見比べてみるとわかる通り、これら二つのループは何行かのコードを書き換えることで、相互に置き換えられることがわかる。

余談:the_post() と setup_postdata() の違い

the_post() には setup_postdata() が含まれるが、他の処理も含まれるので、この二つの関数(およびメソッド)は同じ処理ではない。

特に重要な違いは、 the_post() と違って setup_postdata() は現在の投稿に関する多くのグローバル変数を書き換えるが、肝心の $post を書き換えない点だ。

これは $post が汚染されないという長所でもあるが、 $post 以外の変数名を与えて setup_postdata( $my_post ) のように呼び出すと、以後 the_title() のような関数を使用した場合、 $my_post ではなく元の $post の投稿データが使用されてしまう。

the_permalink() なら the_permalink( $my_post ) のように引数を与えればいいが、 the_title() は投稿オブジェクトを引数に取れない(常にグローバルの $post が使用される)ので問題となる。

この問題を回避するには、 $post$my_post を代入するか、最初から $post の変数名で setup_postdata( $post ) と呼び出すかしなければならない、つまり結局 $post を書き換えることになる。

また同じ理由で、 the_post()have_posts() を使わないと、内部的にはループに入った(またはループから出た)扱いがされないので、 in_the_loop() のようなループ関連の関数やフックが使えないという違いもある。

ちなみに「ループ内」とは the_post() の時点から while 文の評価式にある have_posts() までの間を指す。

引数の違い

上では引数 $args は共通だと述べたが、確かにその形式は同じなのだが、初期値にはわずかに違いがある。

引数のキー WP_Query での初期値 get_posts() での初期値
'ignore_sticky_posts' false / 0 true / 1
'no_found_rows' false / 0 true / 1
'suppress_filters' false / 0 true / 1

しかもこれらのうち 'ignore_sticky_posts''no_found_rows' は、 get_posts() では設定しても強制的に初期値に上書きされて WP_Query に渡される。

また、以下の表の左側のキーの値は get_posts() でしか使えず、対応する共通のキーにコピーされて WP_Query に渡される。

get_posts() 専用のキー 対応するキー
'numberposts' 'posts_per_page'
'category' 'cat'
'include' 'post__in'
'posts_per_page' も上書き
'exclude'
'include' が存在すると無効
'post__not_in'

これから get_posts() を使う場合は、最初から WP_Query にも対応するキーで指定しておくことをオススメする。

一部の状況で WP_Query がおかしい件

さて、このブログで使用しているWordPressテーマは俺の完全な自作だが、このあいだ、これまで get_posts() を使用していた部分を片っ端から WP_Query に置き換える作業をしていた。

すると、うまく動かない現象を確認。

それは自作のショートコードの処理を行う関数でのこと。

get_posts() では問題なく動いていて、同じ引数のまま WP_Query に変更したところ、投稿を正常に取得できなくなった。

上記のような引数の初期値の違いを揃えてみても変化なし。

不思議に思って WP_Query について調べまくった成果がこの記事なのだが……

残念ながら、原因を突き止めることも、問題を回避することもできず、あえなく get_posts() に戻すことに。

ただ一つ、WordPress公式ドキュメントに以下の記述を発見した。

Note: Ticket #18408 For querying posts in the admin, consider using get_posts() as wp_reset_postdata() might not behave as expected.

引用元:WP_Query | Class | WordPress Developer Resources

「管理ページでクエリを実行するなら get_posts() を使うことも考えよう、 wp_reset_postdata() が期待通りに動かないかもしれないから」?

ショートコードだから管理ページではないし、 wp_reset_postdata() とは関係ないから俺の状況とは違うけど、要するに WP_Query だとうまく動かない状況もあるってことだと理解した。

今回、2種類のショートコードで問題を確認したが、そのうちの一方は、そもそも投稿を全く取得できていないようだった(メソッド have_posts()false )。

しかしもう一方では、投稿は取得できているものの、投稿オブジェクトのプロパティのうち $post->post_date のデータが欠落しているという、これまた不可思議な現象に遭遇。

しかも、それならばと $my_query->the_post(); のあとで $post = $my_query->post; と、クエリのプロパティを投稿オブジェクトに書き戻してみると、 $post->post_date がちゃんと存在するという。

the_post() に問題があるのか、そもそも WP_Query に原因があるのか、はたして……

ともかく、ショートコードの関数で WP_Query を使うのは避けたほうがよさそうだ。

この件について、何か情報をお持ちの方がいれば、ぜひ情報提供を求めたい。

まとめ

今回は WP_Queryget_posts() のいろいろな違いを紹介した。

ほぼ同じ、とはいったものの、こうして挙げてみると意外にも違うものだ。

みなさんにはこれらの違いに注意して、ぜひより良いループを作ってほしい。