WordPress内部では、あらゆる投稿の検索に、WP_Queryオブジェクトを使用しています。
WordPressによる投稿の検索をカスタマイズする、最も最良の方法は、pre_get_postsフィルターを使用することです。
この手法を使いこなすためには、WP_Queryクラスの仕様を理解することが重要です。
WP_Queryクラス
WP_Queryクラスは、検索するための各種パラメータ(投稿タイプ、投稿名、カテゴリやターム、投稿者、取得件数など、あらゆる条件)と、その検索結果をプロパティとして持っています。
(その他多くのプロパティがあります。)
URLへのアクセス時には、URLを解析してWP_Queryオブジェクトが自動的に生成され、検索条件パラメータ($query_varsプロパティ)がセットされます。
そして、query() メソッドによって、投稿データが取得され、検索結果(postsプロパティ)にセットされます。
この時自動的に作成される WP_Query オブジェクトは、グローバル変数 $wp_query に格納されています。
テンプレートファイル内で、ループ処理内に必ず記述される、the_post() メソッドが実行されると、postsプロパティに格納された検索結果の配列がポップされ、グローバル変数である $post に格納されます(厳密にはpopではなくindexを進めてコピー)。
テンプレートタグである the_title() や the_content() は、このグローバル変数 $post から値を取得します。
テンプレートによるWP_Queryオブジェクトの操作
pre_get_posts の挙動を説明する前に、WordPressコーディングで、よくpre_get_postsフィルターと対比される、各手法について解説しておきます。
今や事実上の非推奨となったquery_posts() 関数は、グローバル変数 $wp_query を直接書き換えます。
(query_varsプロパティを書き換えてquery() メソッドを実行。)
これにより、自動生成されたWP_Queryオブジェクトの内容が失われるため、それに依存する処理が不具合を起こしたりするなどの難点があります。
それに変わって普及したのが、テンプレートタグ内で、新たにWP_Queryオブジェクトを作成する方法。
WP_Queryクラスのコンストラクタは、引数でquery_varsプロパティに任意の値をセットできるため、query_posts() と同様のことが実現できます。
デフォルトのWP_Queryオブジェクト(グローバル変数 $wp_query)は上書きされません。
グローバル変数 $post は上書きされていますが、再利用が必要な際は、wp_reset_postdata() 関数をコールすれば、元々の $post を復元できます。
しかし、query_posts() 同様、テンプレートに記述する以上、デフォルトの検索が既に終わっているため、無駄に2回DBにクエリを投げることになる、テンプレートの決定などの後続処理が既に終わっているなどの、根本的な問題は解消されません。
例えば、20投稿ある投稿タイプに対して、デフォルトでは1ページ10件のところ、テンプレートで1ページ5件に変更したWP_Queryオブジェクトを生成してループ処理したとします。
このとき、アーカイブの3ページ目のURLにアクセスすると、デフォルトの検索処理では21件目からを探しに行くためNOT FOUNDとなり、404テンプレートなどが選択されてしまいます。
そのため、ページのメインループの変更には、やはり適しません。
そもそもメインループにはあまり使用しませんが、get_posts() という関数もあります。
これは、検索結果を直接配列で返すため、デフォルトのWP_Queryオブジェクトはもちろん、グローバル変数 $post にも影響しないという点で、ページ内での副次的な投稿取得には非常に手軽です。
内部的には、もちろんWP_Queryクラスを使用していますが、WP_Queryオブジェクトが隠蔽されるため、ループ処理や出力処理は、全て自前で書く必要があります。
pre_get_postsフィルター
さて、本題のpre_get_postsフィルターですが、WP_Queryクラスが生成された際、DBにクエリを投げる前に、通過するフィルターです。
コールバック関数の引数にはWP_Queryオブジェクトそのものが渡されます。
コールバック関数内では、is_archive() 等のメソッドによって、query_varsプロパティの中身を評価し、set() メソッド等によって、query_varsプロパティの値を書き換えることができます。
注意すべき点は、このフィルターは、当然のことながら、デフォルトのWP_Queryオブジェクト生成時のみならず、テンプレート内で新たにWP_Queryオブジェクトを作成した時はもちろん、get_posts() 内部でWP_Queryオブジェクトが生成された時や、管理画面の表示時などにも通過するということです。
そのため、is_admin() メソッドや、is_main_query() メソッドの戻り値を評価することによって、意図したケースのみにquery_varsプロパティが書き換えられるようにする必要があります。
余談:WordPressの優しさ〜WP_Queryクラスのメソッドとテンプレートタグ
テンプレートのループ処理内で使用されるhave_posts() や、the_posts() は、元々、WP_Queryクラスのメソッドです。
そのため、$wp_query->have_posts() などと記述すべきなのですが、WordPressは、関数として、同名の have_posts() や the_post() を用意してくれています。
これはもちろん、内部的には、グローバル変数 $wp_query に格納されている、WP_Query オブジェクトのメソッドをコールしています。
なんと冗長な・・・と思わなくもありませんが、クラスやオブジェクトのことがよくわからないPHP初心者でも扱えるようにという、WordPressの優しさでしょう。