I’ve got one last WordPress post in me for now. Today we’ll be taking a quick look at what WordPress’s “The Loop“™ and its related functions are actually doing behind the scenes.
Per the docs, a basic The Loop™ loop looks like this
if ( have_posts() ) :
while ( have_posts() ) : the_post();
// Display post content
endwhile;
endif;
Looking at this as a general PHP programmer (vs. as a WordPress theme developer), we see a have_posts()
guard clause, a while
loop that uses that same have_posts
function, and then inside the while
loop we make a call to a function named the_post
, after which we can display post content.
This sample code uses PHP’s alternative syntax for control structures. It could also be written like this
if ( have_posts() ) :
while ( have_posts() ) :
the_post();
// Display post content
endwhile;
endif;
or like this
if ( have_posts() ) {
while ( have_posts() ) {
the_post();
// Display post content
}
}
All the above loops are functionally equivalent.
The have_posts Function
The have_posts
function calls a global object’s have_posts()
method.
# File: /wp-includes/query.php
function have_posts() {
global $wp_query;
return $wp_query->have_posts();
}
This $wp_query
global is the global WP_Query
object. WordPress creates this object in the wp-settings.php
include file.
# File: wp-settings.php
$GLOBALS['wp_the_query'] = new WP_Query();
/* ... */
$GLOBALS['wp_query'] = $GLOBALS['wp_the_query'];
The code in wp-settings
uses PHP’s $GLOBALS
array/dictionary to define global variables. Setting a value in this array will also set a corresponding global variable. ($GLOBALS['foo'] = 'bar';
means that global $foo
will also equal 'bar'
). This $GLOBALS
array is also a PHP super global, which means you can reference it without saying global $GLOBALS
.
The have_posts
method of the WP_Query
object reveals that WP_Query
does more than just query the database.
# File: wp-includes/class-wp-query.php
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;
}
WP_Query
objects also contain counter variables that track where in “A Loop™” the current query is and issues the loop_end
event/action-hook if there’s no posts left to loop over.
The the_post Function
If we look at the definition of the_post
, we see that it’s calling the the_post
method on the global WP_Query
object.
# File: wp-includes/class-wp-query.php
function the_post() {
global $wp_query;
$wp_query->the_post();
}
If we look at the method definition
# File: wp-includes/class-wp-query.php
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 );
}
We see some in_the_loop
bookkeeping, an event/action-hook firing if this is the first item in the loop, and then a call the next_post
and setup_post_data
functions. We also see that the call to next_post
populates the $post
variable, which is the global posts variable.
# File: wp-includes/class-wp-query.php
global $post;
/* ... */
$post = $this->next_post();
If we look at the next_post
method
# File: wp-includes/class-wp-query.php
public function next_post() {
$this->current_post++;
$this->post = $this->posts[$this->current_post];
return $this->post;
}
We see some more bookkeeping on post counting, and that we’re setting a current value for $this->post
by pulling a value from $this->posts
. If we peek at setup_postdata
this appears to be a function that sets a number of global variables and fires the the_post
event/action-hook.
# File: wp-includes/class-wp-query.php
public function setup_postdata( $post ) {
global $id, $authordata, $currentday, $currentmonth, $page, $pages, $multipage, $more, $numpages;
/* ... */
$elements = $this->generate_postdata( $post );
/* ... */
$id = $elements['id'];
$authordata = $elements['authordata'];
$currentday = $elements['currentday'];
$currentmonth = $elements['currentmonth'];
$page = $elements['page'];
$pages = $elements['pages'];
$multipage = $elements['multipage'];
$more = $elements['more'];
$numpages = $elements['numpages'];
/* ... */
do_action_ref_array( 'the_post', array( &$post, &$this ) );
return true;
}
We’ll stop at the generate_postdata
method and just say that it extracts and formats information from the fetched post object.
What Runs the Query?
This line from next_posts
raises a question
# File: wp-includes/class-wp-query.php
$this->post = $this->posts[$this->current_post];
What exactly populates $this->posts
? If we’ve followed our execution correctly, it seems like “The Loop™” expects this value to be already populated.
The posts
array will be populated by the WP_Query->get_posts
method, which is called by the query
method. Both of these methods are public, which means they could be called at anytime by code outside of the WP_Query
class.
That said, there’s two scenarios where ->query
gets called that seem like the most common ones. First, if you create a WP_Query
object with query arguments, the constructor will immediately call query and fetch the posts.
# File: wp-includes/class-wp-query.php
public function __construct( $query = '' ) {
if ( ! empty( $query ) ) {
$this->query( $query );
}
}
The other scenario we’re interested in is the one that’s relevant for the global WP_Query
object. This object is instantiated without query arguments. The global query object has its query method called as part of the call to the global wp()
function. The wp()
function calls the main
method of the WP
object. This global WP
object is a class responsible for bootstrapping various parts of the WordPress environment. Its main method calls its query_posts
method which is what calls the WP_Query
object’s query
method and populates the posts variables.
# File: wp-includes/class-wp.php
public function query_posts() {
global $wp_the_query;
$this->build_query_string();
$wp_the_query->query( $this->query_vars );
}
Display the Post
If we jump back to our sample code
if ( have_posts() ) :
while ( have_posts() ) : the_post();
// Display post content
endwhile;
endif;
We still haven’t fully answered the question of why, after calling the_post
, we can “display post content”.
This comment means that we can call functions like the_title
to output information about the post. If we take a look at the definition of the_title
and its call to get_the_title
, we see WordPress fetches the title from a post populated by a call to get_post
#File: wp-includes/post-template.php
function get_the_title( $post = 0 ) {
$post = get_post( $post );
$title = isset( $post->post_title ) ? $post->post_title : '';
/* ... */
}
The get_post
method can fetch posts in a lot of different ways, but in the general instance it’s pulling data from the global $post
variable. This is the same variable we saw set earlier in the the_post
method.
Backwards Compatibility
All of this is pretty complex and almost certainly not how you’d build a similar system today. It is a good example of the work involved in maintaining a backwards compatible system. WordPress APIs come from the PHP3/PHP4 era — functions, global variables, etc. Rather than evolve the system to pick up the latest fashion in PHP development, the work of a core WordPress systems engineer appears to be to maintain these older APIs as time moves on while still adding new features to the system.
The main trade-off is developers who have already adopted WordPress have a stable system that continues to behave the way it’s always behaved, at the cost of new developers facing a steep learning curve if they want to go beyond the surface details of these older APIs.