WordPressの表示フローとLoopとthe_title()〜いろいろ考えたメモ〜

いきなりわけの分からないタイトルですが、色々考えたたり、教えていただいたりしたので記録です。
いろいろ考えてる過程のメモ的な感じなので、あんまり役に立たないと思います。ごめなさい

発端はループの説明

WordPressのループについて説明しようとして、うまく説明出来ない事がありました。

ループ無しでthe_title()とthe_content() を書くとthe_title()の結果は表示されるのですが、the_content() の結果は全く表示されません。the_title()もthe_content() もループ内で使うテンプレートタグですよね。

アーカイブ系のページだと、ループがなければthe_title()の結果も一番新しい記事しかでないので、結果としておかしいってわかります。なのでなんとなくあたりまえだと思ってました。

でも固定ページとかだと、タイトルは普通に取得できてるかのように見えちゃいます。

WordPressの動作原理解説とかWordPress の表示ロジックを理解する – Reloaded – とかを何度も読みなおしたり、ループの内外で$postsの中身をみたりとか、ソースを追ってみようとしたんです。

でも、the_title()はとりあえず表示されちゃうのか、またthe_content()はなぜ全く何も表示されないのか、悩んで悩んで、我らが黄色い人まがりんさん@jim0912 にヒントをいっぱい教えていただきました!

テンプレートが呼ばれた時

この時は、すでにアクセスされたURLをもとに、表示の条件が抽出されて(実行フローの「解析」)、パラメータを元に条件にあう記事が取得(実行フローの「抽出」)されています。

表示する記事情報は $wp_query->posts に格納されています。

flow

WP_Queryのプロパティを復習

ここでWP_Queryのプロパティを復習しつつ$wp_queryをvar_dumpしてじっくりみてみました。

スクリーンショット 2013-05-22 15.56.42

まぁ。こんな感じ。もちろんソースでみたほうがわかりやすいけど。こんなにあるよって感じで。

WP_Queryのプロパティとメソッド

これを理解するにはWP_Queryのプロパティとメソッドを見るといい感じ。

WP_Queryのプロパティとメソッドはここ

この時点でタイトルも記事内容ももちろん取得できてるわけですね。

メソッドのnext_post()、the_post()、have_posts() はループの中で使えって書いてあります。

ここまではよし。

確かめたいのはとりあえず出ちゃうのと全く何にも出てこないというthe_title()とthe_content()の挙動の違い。

いわく、

the_title は、グローバル変数の $post さえ取れれば扱えるのですが、the_content は、ページ分割への対応に必要となるグローバル変数が、the_post で生成される…

とのこと。

もう一度ループを復習

ということでもう一度ループを復習してみることに。

have_posts()

have_posts()は表示すべき投稿があるかどうか、という事なんだけど、$wp_query->have_posts()を呼び出してるんですね。

ループの始めで$wp_query->have_posts()を呼び出して、表示すべき投稿があるどうか、という条件としてつかって、表示すべき投稿がある限り、ループが繰り返される。

という感じ。

そして繰り返すごとに、 the_post()が呼び出される。the_post()は$wp_query->the_post()を呼び出している。

the_post()

the_post() についてはWordPressの the_post() ってそもそも何者? もわかりやすかったです。
再度読みなおしました。ありがとございます。

codex見ると

投稿の生成

the_post() 関数は投稿リストの中の現在の投稿を取得し、それをループの繰り返しの内部で使えるようにします。 the_post() 関数がないとテーマで使用されているほとんどのテンプレートタグは正しく機能しません。
投稿データが利用できる状態になれば、テンプレートは投稿データを訪問者に表示しはじめることができます。

ってなってる。

wp-includes/query.php の the_post() をみてみる
$wp_query->the_post()を呼び出してる

775行め

/**
 * Iterate the post index in the loop.
 *
 * @see WP_Query::the_post()
 * @since 1.5.0
 * @uses $wp_query
 */
function the_post() {
	global $wp_query;

	$wp_query->the_post();
}

2848 行め  

setup_postdata($post) 

have_postsもあったのでついでに。

	/**
	 * 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
	 * @uses $post
	 * @uses do_action_ref_array() Calls 'loop_start' if loop has just started
	 */
	function the_post() {
		global $post;
		$this->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();
		setup_postdata($post);
	}

	/**
	 * Whether there are more posts available in the loop.
	 *
	 * Calls action 'loop_end', when the loop is complete.
	 *
	 * @since 1.5.0
	 * @access public
	 * @uses do_action_ref_array() Calls 'loop_end' if loop is ended
	 *
	 * @return bool True if posts are available, false if end of loop.
	 */
	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));
			// Do some cleaning up after the loop
			$this->rewind_posts();
		}

		$this->in_the_loop = false;
		return false;
	}

loop

こんな感じでいいのかな?

the_post でやっていること = wp-includes/query.php のsetup_postdata の内容

というのがこのことか?

ということで

setup_postdataも見てみる

3675行目あたり

/**
 * Set up global post data.
 *
 * @since 1.5.0
 *
 * @param object $post Post data.
 * @uses do_action_ref_array() Calls 'the_post'
 * @return bool True when finished.
 */
function setup_postdata($post) {
	global $id, $authordata, $currentday, $currentmonth, $page, $pages, $multipage, $more, $numpages;

	$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;
	$page = get_query_var('page');
	if ( !$page )
		$page = 1;
	if ( is_single() || is_page() || is_feed() )
		$more = 1;
	$content = $post->post_content;
	if ( strpos( $content, '<!--nextpage-->' ) ) {
		if ( $page > 1 )
			$more = 1;
		$multipage = 1;
		$content = str_replace("\n<!--nextpage-->\n", '<!--nextpage-->', $content);
		$content = str_replace("\n<!--nextpage-->", '<!--nextpage-->', $content);
		$content = str_replace("<!--nextpage-->\n", '<!--nextpage-->', $content);
		$pages = explode('<!--nextpage-->', $content);
		$numpages = count($pages);
	} else {
		$pages = array( $post->post_content );
		$multipage = 0;
	}

	do_action_ref_array('the_post', array(&$post));

	return true;
}

なんか3.6から変わるっぽい。paginate_content、get_paged_contentというfunctionが出来て、
setup_postdataに投稿フォーマットの処理がはいってるみたい。

どんな影響あるのかわかんないけど。

3.6(beta2)のsetup_postdata まわり

/**
 * Split the passed content by <!--nextpage-->
 *
 * @since 3.6.0
 *
 * @param string $content Content to split.
 * @return array Paged content.
 */
function paginate_content( $content ) {
     $content = str_replace( "\n<!--nextpage-->\n", '<!--nextpage-->', $content );
     $content = str_replace( "\n<!--nextpage-->",   '<!--nextpage-->', $content );
     $content = str_replace( "<!--nextpage-->\n",   '<!--nextpage-->', $content );
     return explode( '<!--nextpage-->', $content );
}

/**
 * Return content offset by $page
 *
 * @since 3.6.0
 *
 * @param string $content
 * @param int $paged
 * @return string
 */
function get_paged_content( $content = '', $paged = 0 ) {
     global $page;
     if ( empty( $page ) )
          $page = 1;

     if ( empty( $paged ) )
          $paged = $page;

     if ( empty( $content ) ) {
          $post = get_post();
          if ( empty( $post ) )
               return '';

          $content = $post->post_content;
     }

     $pages = paginate_content( $content );
     if ( isset( $pages[$paged - 1] ) )
          return $pages[$paged - 1];

     return reset( $pages );
}

/**
 * Set up global post data.
 *
 * @since 1.5.0
 *
 * @param object $post Post data.
 * @uses do_action_ref_array() Calls 'the_post'
 * @return bool True when finished.
 */
function setup_postdata($post) {
     global $id, $authordata, $currentday, $currentmonth, $page, $pages, $format_pages, $multipage, $more, $numpages;

     $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;
     $page = get_query_var('page');
     if ( !$page )
          $page = 1;
     if ( is_single() || is_page() || is_feed() )
          $more = 1;
     $split_content = $content = $post->post_content;
     $format = get_post_format( $post );
     if ( $format && in_array( $format, array( 'image', 'audio', 'video', 'quote' ) ) ) {
          switch ( $format ) {
          case 'image':
               get_the_post_format_image( 'full', $post );
               if ( isset( $post->split_content ) )
                    $split_content = $post->split_content;
               break;
          case 'audio':
               get_the_post_format_media( 'audio', $post, 1 );
               if ( isset( $post->split_content ) )
                    $split_content = $post->split_content;
               break;
          case 'video':
               get_the_post_format_media( 'video', $post, 1 );
               if ( isset( $post->split_content ) )
                    $split_content = $post->split_content;
               break;
          case 'quote':
               get_the_post_format_quote( $post );
               if ( isset( $post->split_content ) )
                    $split_content = $post->split_content;
               break;
          }
     }

     if ( strpos( $content, '<!--nextpage-->' ) ) {
          if ( $page > 1 )
               $more = 1;
          $multipage = 1;
          $pages = paginate_content( $content );
          $format_pages = paginate_content( $split_content );
          $numpages = count( $pages );
     } else {
          $pages = array( $post->post_content );
          $format_pages = array( $split_content );
          $multipage = 0;
     }

     do_action_ref_array('the_post', array(&$post));

     return true;
}

こういう処理がループ外ではできてないから、なんにも表示されないってこと、なのかな?

そんでもってthe_content()

the_content()は wp-includes/post-template.php 156行目あたり

get_the_contentもその後にありました。get_the_contentをapply_filters、str_replace して echoしてるんですねー。へぇー。

/**
 * Display the post content.
 *
 * @since 0.71
 *
 * @param string $more_link_text Optional. Content for when there is more text.
 * @param bool $stripteaser Optional. Strip teaser content before the more text. Default is false.
 */
function the_content($more_link_text = null, $stripteaser = false) {
	$content = get_the_content($more_link_text, $stripteaser);
	$content = apply_filters('the_content', $content);
	$content = str_replace(']]>', ']]&gt;', $content);
	echo $content;
}

/**
 * Retrieve the post content.
 *
 * @since 0.71
 *
 * @param string $more_link_text Optional. Content for when there is more text.
 * @param bool $stripteaser Optional. Strip teaser content before the more text. Default is false.
 * @return string
 */
function get_the_content($more_link_text = null, $stripteaser = false) {
	global $post, $more, $page, $pages, $multipage, $preview;

	if ( null === $more_link_text )
		$more_link_text = __( '(more...)' );

	$output = '';
	$hasTeaser = false;

そんでもってthe_title()

the_title

/**
 * Display or retrieve the current post title with optional content.
 *
 * @since 0.71
 *
 * @param string $before Optional. Content to prepend to the title.
 * @param string $after Optional. Content to append to the title.
 * @param bool $echo Optional, default to true.Whether to display or return.
 * @return null|string Null on no title. String if $echo parameter is false.
 */
function the_title($before = '', $after = '', $echo = true) {
	$title = get_the_title();

	if ( strlen($title) == 0 )
		return;

	$title = $before . $title . $after;

	if ( $echo )
		echo $title;
	else
		return $title;
}

あれ?get_the_title もみなきゃ?

get_the_title

/**
 * Retrieve post title.
 *
 * If the post is protected and the visitor is not an admin, then "Protected"
 * will be displayed before the post title. If the post is private, then
 * "Private" will be located before the post title.
 *
 * @since 0.71
 *
 * @param int $id Optional. Post ID.
 * @return string
 */
function get_the_title( $id = 0 ) {
	$post = &get_post($id);

	$title = isset($post->post_title) ? $post->post_title : '';
	$id = isset($post->ID) ? $post->ID : (int) $id;

	if ( !is_admin() ) {
		if ( !empty($post->post_password) ) {
			$protected_title_format = apply_filters('protected_title_format', __('Protected: %s'));
			$title = sprintf($protected_title_format, $title);
		} else if ( isset($post->post_status) && 'private' == $post->post_status ) {
			$private_title_format = apply_filters('private_title_format', __('Private: %s'));
			$title = sprintf($private_title_format, $title);
		}
	}
	return apply_filters( 'the_title', $title, $id );
}

ループ書かないで var_dumpしたら $post は最新一件入ってたから、
$post->post_title
タイトルはとりあえず表示されちゃう…… ってこと?

違ってたらがんがんツッコミ下さい。

そんでもって感想

これを人に説明できるか?っていうと消化出来てないし、ソースも読み切れてないし、多分もっと見なきゃいけないとこたくさんありそう。

それにしても、ひとことでループ内でないと使えないよ!って言ってるテンプレートタグにも随分違いがあるんだなぁ。

このあと

query_postsを捨てよ、pre_get_postsを使おう
3.3の新しい関数 is_main_query を使おう

このへんもう一回じっくり読んだら、理解が変わるかな?

ひょんなところからツッコミもらって、色々調べて、ちょっとつらかったけど、いろいろ追っかけてくのは面白かったです。

まとまりないけどこれで。

コメントをどうぞ♪