【WordPressの動き方シリーズ】(5)メインクエリがテンプレートでどのように働いているか

【WordPressの動き方シリーズ】(4)メインクエリができるまで | WordPressのプラグインを作ろうでメインクエリのデータが$wp_the_queryに保存されることを学びました。この結果、おなじWP_Queryオブジェクトを参照渡しをされている$wp_queryからもメインクエリを利用できるようになりました

このメインクエリを利用するには、テンプレートでメインループを利用することになります。

メインループでメインクエリを扱う時には、$wp_queryを利用することになります。

メインループ

WordPress公式テンプレートでよく見かけるかと思いますが、メインループは以下の様な形をしています。

if(have_posts()):
    while(have_posts()):
         the_post();

//略

    endwhile;
endif;
wp_reset_query();

メインクエリはこのメインループ内で処理されるのですが、テンプレート上でサブクエリを利用したいと考えて、以下のようにサブクエリで使用する形式の書き方を冒頭に付け加えても、if(have_posts()): while(have_posts()):~~~~endwhile;endif;内ではそのままではサブクエリはループとして利用できません。

//サブクエリを利用しようとしてもこのままでは利用できない例

$args = array( 
    'post_type' => 'information',
    'posts_per_page' => 5
);
$information = get_posts($args);
//$informationにサブクエリのデータを保存する

if(have_posts()):
    while(have_posts()):
         the_post();

//この中はメインクエリが使われるので、そのままではサブクエリのループは効かない。

    endwhile;
endif;
wp_reset_query();

ここに出てくる、have_posts(), the_post()関数は、wp-includes/query.phpに記載されています。WP_Queryクラス内部にも同名のメソッドがありますが、この二つの関数は「メインクエリを利用するため専用の関数」です。具体的にはこれから見ていきますが、各関数の中身は、WP_Queryクラスのインスタンスオブジェクトである$wp_queryからそれぞれ同名のメソッドが実行されています。

have_posts()関数

/**
* Whether current WordPress query has results to loop over.
*
* @since 1.5.0
*
* @global WP_Query $wp_query Global WP_Query instance.
*
* @return bool
*/
function have_posts() {
    global $wp_query;
    return $wp_query->have_posts();
}

特に何も解説する必要ないですね。上にも書きましたがWP_Queryクラスのインスタンスオブジェクトである$wp_queryの同名メソッドが実行されています。

WP_Queryクラスのhave_posts()メソッドを見て見ましょう。

    /**
     * Determines whether there are more posts available in the loop.
     *
     * Calls the {@see 'loop_end'} action when the loop is complete.
     *
     * @since 1.5.0
     * @access public
     *
     * @return bool True if posts are available, false if end of loop.
     */
    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 ) {  //・・・②
            /**
             * Fires once the loop has ended.
             *
             * @since 2.0.0
             *
             * @param WP_Query &$this The WP_Query instance (passed by reference).
             */
            do_action_ref_array( 'loop_end', array( &$this ) );  //・・・⑤
            // Do some cleaning up after the loop
            $this->rewind_posts();  //・・・⑥
        }

        $this->in_the_loop = false;  //・・・③
        return false;  //・・・④
    }

① if ( $this->current_post + 1 < $this->post_count )

ループを進めてよいかどうか(表示する記事が$wp_queryに残っているかどうか)判定しています。

$this->current_postプロパティは、メインクエリの記事のindex(記事の順番号)です。(WP_Queryクラスがインスタンス化された時(=初期状態)、自動的に-1の値が設定されます。)

メインループ処理の各ステップにおいて、$this->current_postの値は以下のように設定されています。

if( have_posts() ) : // <= ここでは、初期値の -1
    while( have_posts() ) :  // <= ここでは、「一つ前の記事のindex」。初回ループなら初期値の -1
        the_post(); // <=この関数でようやく「一つ前の記事index」に1を加えたものが設定される
    endwhile;
endif;

次(あるいは初回)のループが始まる前に表示する記事が終わってないかを判定する必要があるため、if(have_posts()), while(have_posts())の段階では、一つ前の記事のindexがこのプロパティに保存されています。そのため$this->current_postに1を加えた【次の記事index】を使って比較することになります。ループが始まることが確実になった段階(the_post()関数)でindexの番号を一つ増やすことになります(後述)。

② elseif ( $this->current_post + 1 == $this->post_count && $this->post_count > 0 )

最終記事を表示したループあと、while(have_posts())に戻ったとき、①の判定でfalseとなり、こちらの判定が行われます。

$this->current_postは、「前のループの記事index = 最終記事index」となっているので、$this->current_post + 1 == $this->post_count 式はtrueとなり、⑤、⑥の「最後の記事を表示させた後の処理」が進められ、その後に③、④が実行されます。

⑤ do_action_ref_array( 'loop_end’, array( &$this ) );

このloop_endアクションフックは、メインクエリの記事データがすべて処理されメインループが終わる時に処理を行うために利用されるアクションフックです。

WP_Queryのインスタンスオブジェクトの$wp_queryを利用できます。

⑥ $this->rewind_posts();

rewind_posts()メソッドは、


/**
 * Rewind the posts and reset post index.
 *
 * @since 1.5.0
 */ 
public function rewind_posts() { 
    $this->current_post = -1; 
    if ( $this->post_count > 0 ) { 
        $this->post = $this->posts[0]; 
    } 
}

メインクエリの記事データがすべて処理されメインループが終わる時に色々なパラメーターを初期状態に戻すために実行されます。

具体的には、$this->current_postの値を初期状態(現在の記事indexではなく、初期状態)に。$this->postプロパティにメインクエリの最初の記事のデータを代入します。(※記事データは、オブジェクト型なので、「参照渡し」となっています。)

つまり、この二つのプロパティを初期状態にする関数(巻き戻し関数)というわけです。

以上を簡単にまとめると、

/*have_posts()判定:
    メインクエリに記事データはあるか=>データがあるならwhile()構文へ移動
                                 =>データが無ければ、$this->in_the_loop => falseにしてループを抜ける
*/
if( have_posts() ) : 
    /*
    have_posts()判定:
        最後の記事を表示させた後=>loop_endアクション実行後、rewind_posts()メソッドで色々初期化
                              =>$this->in_the_loop => falseにしてループを抜ける
        まだ最後の記事を表示させていない=>while():endwhile;ループ内に進む
    */
    while( have_posts() ) : 
    
    endwhile;
endif;

となります。

the_post()関数

次は、while()ループ内の最初の関数the_post()です。

/**
* Iterate the post index in the loop.
*
* @since 1.5.0
*
* @global WP_Query $wp_query Global WP_Query instance.
*/
function the_post() {
    global $wp_query;
    $wp_query->the_post();
}

この関数もメインクエリのデータが保存されている$wp_queryのthe_post()メソッドを実行します。

WP_Queryクラスのthe_post()メソッドの内容を見て見ましょう。

WQ_Queryクラスのthe_post()メソッド

 

/**
     * Sets up the current post.
     *
     * Retrieves the next post, sets up the post, sets the 'in the loop'
     * property to true.
     *
     * @since 1.5.0
     * @access public
     *
     * @global WP_Post $post
     */
    public function the_post() {
        global $post;  //・・・①
        $this->in_the_loop = true;  //・・・①

        if ( $this->current_post == -1 ) // loop has just started  ・・・②
            /**
             * Fires once the loop is started.
             *
             * @since 2.0.0
             *
             * @param WP_Query &$this The WP_Query instance (passed by reference).
             */
            do_action_ref_array( 'loop_start', array( &$this ) );  //・・・③

        $post = $this->next_post();  //・・・④
        $this->setup_postdata( $post );  //・・・⑤
    }

① global $post; $this->in_the_loop = true;

今進行しているループ内で利用するための記事データを保存するグローバル変数$postを呼び出すと同時に、ループ内であるフラグとして$this->in_the_loopプロパティをtrueに変更しています。

② if ( $this->current_post == -1 )

WP_Queryがインスタンス化された直後の$wp_queryは$this->current_postの値は-1となっています。

そのためこの判定は「1回目ループが始まってすぐ」に、③が実行されるようになっています。2回目以降のループでは実行されません。

③ do_action_ref_array( 'loop_start’, array( &$this ) );

$wp_queryのhave_posts()でメインループが終わった後に実行されるloop_endアクションフックがありましたが、ここでは、メインループの最初のループの先頭で実行されるloop_startアクションフックが設定されています。

④ $post = $this->next_post();

$wp_queryのnext_post()メソッドは以下の内容になっています。

/**
     * Set up the next post and iterate current post index.
     *
     * @since 1.5.0
     * @access public
     *
     * @return WP_Post Next post.
     */
    public function next_post() {

        $this->current_post++;

        $this->post = $this->posts[$this->current_post];
        return $this->post;
    }        

メインループの記事indexの$this->current_postはここに来るまで「前のループのindex」のままでした。(最初のループでは-1)

その値を、ループの先頭のここで「現在のループの記事index」に巻き上げ、メインクエリの$wp_query->postプロパティに「現在のループ記事オブジェクト」を代入しています。

⑤ $this->setup_postdata( $post );

WP_Queryのsetup_postdata()メソッドが実行されます。その中身を見て見ましょう。

/**
     * 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 );

            // 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 array   $pages Array of "pages" derived from the post content.
         *                       of `<!-- 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;
    }

この関数では大きく分けて3種類の事をしています。

$this->post に納められているデータの型をWP_Postのオブジェクトに保証

②の判定式でWP_Postのインスタンス化判定し、異なっている場合には、$post = get_post( $post );で再度記事データをWP_Post形式で取得することになります。

現在のループでの記事データにあったグローバル変数の設定

①で

global $id, $authordata, $currentday, $currentmonth, $page, $pages, $multipage, $more, $numpages;

といくつかのグローバル変数が宣言されていますが、setup_postdata()メソッドでは、それぞれの変数に現在のループにおける値を設定しています。

$id :投稿ID
$authordata:記事を書いた執筆者に関する情報
$currentday:記事が公開された日付
$currentmonth:記事が公開された月
$numpages:記事を「<!–nextpage–>」などで分割した際の分割ページ数
$multipage:記事が「<!–nextpage–>」などで分割されているかの判定フラグ
$page:記事が「<!–nextpage–>」などで分割されていた場合の現在の表示ページ
$pages:記事が「<!–nextpage–>」などで分割されていた場合、分割したそれぞれのページのコンテンツが配列として納められている

the_postアクションフックの実行

③でthe_postくションフックを実行しまし。このアクションフックでは、$wp_queryと$postが参照渡しされていますので、それらを使って内容の改変等を行う事が出来ます。

以上の3つを実行することで、the_post()関数以降、そのループで使用される記事にあったグローバル変数を利用して、様々な関数(the_title(), the_content()など)をループ内で実行することができるようになっています。

wp_reset_query();

最後の記事を表示させ、while(the_posts()):でfalse判定が起こると、whileループを抜けてwp_reset_query()関数が実行されます。

/**
* Destroys the previous query and sets up a new query.
*
* This should be used after query_posts() and before another query_posts().
* This will remove obscure bugs that occur when the previous WP_Query object
* is not destroyed properly before another is set up.
*
* @since 2.3.0
*
* @global WP_Query $wp_query     Global WP_Query instance.
* @global WP_Query $wp_the_query Copy of the global WP_Query instance created during wp_reset_query().
*/
function wp_reset_query() {
    $GLOBALS['wp_query'] = $GLOBALS['wp_the_query'];
    wp_reset_postdata();
} 

この関数は、改変された恐れのあるメインクエリ$wp_query変数の中身を、オリジナルとしてとっておいた$wp_the_queryの中身を使ってリセットし、その後でwp_reset_postdata()関数を実行します。

/**
* 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の同名メソッドを実行しますのでそちらを見て見ましょう。

/**
     * 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 Global post object.
     */
    public function reset_postdata() {
        if ( ! empty( $this->post ) ) {
            $GLOBALS['post'] = $this->post;
            $this->setup_postdata( $this->post );
        }
    }

$this->postは、while(have_posts())の中で、rewind_posts()メソッドどによりメインクエリの最初の記事データに戻されています。グローバル変数の$postをその値にリセットしています。

また、そして、最初の記事データを利用し、$wp_queryのsetup_postdataメソッドを実行することで、

$id :投稿ID
$authordata:記事を書いた執筆者に関する情報
$currentday:記事が公開された日付
$currentmonth:記事が公開された月
$numpages:記事を「<!–nextpage–>」などで分割した際の分割ページ数
$multipage:記事が「<!–nextpage–>」などで分割されているかの判定フラグ
$page:記事が「<!–nextpage–>」などで分割されていた場合の現在の表示ページ
$pages:記事が「<!–nextpage–>」などで分割されていた場合、分割したそれぞれのページのコンテンツが配列として納められている

これらのグローバル変数を「最初の記事」の内容に戻しています。

つまり、wp_reset_query()の「query」は、「メインクエリ」の事なので、サブクエリを使う時にはこの関数を使う必要はありません

 

以上が、メインクエリがメインループで利用されるステップとなります。