Categories


Archives


Recent Posts


Categories


WordPress WP_Query and The Loop™

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Updated for Magento 2! No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

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.

Copyright © Alan Storm 1975 – 2021 All Rights Reserved

Originally Posted: 14th September 2021