An in depth look into the WP_Query and a WordPress loop

A comprehensive look into the WordPress loop.

The Do’s and Don’ts of looping

The Do’s:

  • The good old default loop is always at your service.
  • pre_get_posts filter to modify the home, archive, category, search, and tag page main loops.
  • WP_Query for completely custom loops.
  • get_posts to get a singular post in the sidebar, for example.

The Don’t:

  • Don’t ever use query_posts for anything, it’s not recommended anymore, use WP_Query instead.

For explanation why is that so, please scroll down to the Takeaways and conclusions section of the article, I’ve embedded a slideshow and a diagram there that’ll sheds more light to it.

Lets go these through one by one.

Default loop

If the default loop is clear to you, please skip to the next section.

First things first, the basic loop that get’s it’s content depending on what page it is, if it’s home it get’s the latest posts, or if in category archive it gets the posts related to that category and so on.

<?php if (have_posts()) : while (have_posts()) : the_post(); ?>
    <article>
        <h2>
            <?php the_title(); ?>
        </h2>
        <?php the_content(); ?>
    </article>
<?php endwhile; ?>
    <div class="nav">
        <div class="nav__next"><?php next_posts_link(); ?></div>
        <div class="nav__prev"><?php previous_posts_link(); ?></div>
    </div>
<?php else : ?>
    <div>
        <h1>Not Found</h1>
    </div>
<?php endif; ?>

I personally hate that syntax a lot, I prefer the following:

<?php
if (have_posts()) :
    while (have_posts()) :
        the_post();
        ?>
        <article>
            <h2><?php the_title(); ?></h2>
            <?php the_content(); ?>
        </article>
        <?php
    endwhile;
    ?>
    <div class="nav">
        <div class="nav__next"><?php next_posts_link(); ?></div>
        <div class="nav__prev"><?php previous_posts_link(); ?></div>
    </div>
    <?php
else :
    ?>
    <article>
        <h1>Not Found</h1>
    </article>
    <?php
endif;
?>

It’s still spaghetti code, but we can’t help that can we, unless using a templating system like Blade or Twig. Anyway, it’s still much nicer IMO.

Syntax aside, lets examine all the constituents of the default loop:

if (have_posts()) :
I quote the bible: “This function checks to see if the current WordPress query has any results to loop over. This is a boolean function, meaning it returns either TRUE or FALSE.” In essence, this just checks if there are any posts, simple as.
while (have_posts()) :
I quote: “As a side effect, have_posts starts, steps through, or resets The Loop.” At this point, have_posts() will contain all the posts that are needed on the requested page. while is a PHP control structure that runs a loop as long as a condition is true, in this case, as long as there are posts.
the_post()
Iterate the post index in The Loop. Retrieves the next post, sets up the post, sets the ‘in the loop’ property to true.” —Codex This just sets up things for us, after calling the_post user has access to functions like the_title() and the_content().
the_title()
Get’s the title of the post. In the world of WordPress these are called template tags.
the_content()
Gets the content of the post.
endwhile
This ends the loop, at this point the loop should be done completely. After this, though, there is the pagination (if it’s needed). The pagination is only wanted if there are posts, so it’s inside the if statement, and it’s only wanted to be shown once, so it’s outside the while loop.
else :
Stuff after this runs only if there simply aren’t any posts to display. This is more of a precautionary measure, most likely there always are posts, but you know, a lot of programming is there just to cast a “safety net”, so to speak.
endif
Signifies the end of the if statement and the final resting point of the default WordPress loop.

Where does the loop get its contents then? This content is called Query, like I stated before, the content of the loop depends on what page the user is requesting, i.e. what the current URL is. There might be a time when this pattern is wanted to be broken, to achieve completely custom behaviour. That’s why there are so many different ways to manipulate loop. Read on if interested.

pre_get_posts filter

pre_get_posts is a fairly new invention. It’s a filter used via the function.php file rather than from the templates. It requires the default loop in the template, though. In essence, pre_get_posts edits the behaviour of the default loop.

Here’s the basics:

// functions.php
function cm_custom_loop($query) {
    if ($query->is_home() && $query->is_main_query()) {
        $query->set('post_type', 'project');
        $query->set('orderby', 'menu_order');
        $query->set('order', 'ASC');
    }
}
add_action('pre_get_posts', 'cm_custom_loop');

Lets examine the ingredients more carefully:

function cm_custom_loop($query)
This just makes a basic PHP functions and gives it one parameter $query.
if ($query->is_home() && $query->is_main_query())
Check if home page and if the query is the main query. If either one of them is false, the statement returns false and stops.
$query->set('post_type', 'project')
Here starts the loop modification, this shows only posts from the custom post type called “projects”. All the same parameters as in WP_Query are available here. See a comprehensive codex article on it.
$query->set('orderby', 'menu_order')
This is another modification to the loop, it changes how the posts are ordered. We can have these mods as many as we like, just consult the WP_Query documentation page for parameters.
add_action('pre_get_posts', 'cm_custom_loop');
This is where we apply the new loop function.

PHP nowadays has anonymous functions, since we don’t need that loop function to anything else than this, it could be written more concisely:

add_action('pre_get_posts', function($query) {
    if ($query->is_home() && $query->is_main_query()) {
        $query->set('post_type', 'project');
    }
});

That looks a bit like JavaScript :)

pre_get_posts more in depth

Every time user requests a page, WP creates a Query based on the URL. Now, if we would use a custom loop in our templates, like a query_posts loop, we would only edit that Query we already have, hence doing things twice. pre_get_posts edits the main query that is created for each and every page, that’s why the name: pre.

Here’s a little diagram to clear it up a bit:

┌──────────────────────────┐
│  User requesting a page  |
└────────────┬─────────────┘
┌────────────┴─────────────┐
│    Query is generated    |
└────────────┬─────────────┘
┌────────────┴─────────────┐
│          Loop            |
└──────────────────────────┘

Here’s what pre_get_posts comes in:

┌──────────────────────────┐
│  User requesting a page  |
└────────────┬─────────────┘
┌────────────┴─────────────┐
│   Modify original Query  |
|    with `pre_get_posts`  |
└────────────┬─────────────┘
┌────────────┴─────────────┐
│ Query is generated based |
|  on the `pre_get_posts`  |
└────────────┬─────────────┘
┌────────────┴─────────────┐
│          Loop            |
└──────────────────────────┘

This great Stack Exchange answer explains it nicely:

…the main query executes on every page load and returns an array of posts regardless. Removing the loop does not stop the main query from being executed. The loop just displays what is being retrieved by the main query.

Always use pre_get_posts to modify the main Query, the Query that is automatically generated on every page load by WordPress. For crating custom queries, read on.

WP_Query loop

WP_Query is great for those completely custom loops that in no way tied to the main query.

Here’s the basics:

<?php
// Configure the query, in essence, this returns and object that we then iterate
// with a loop later on
$my_query = new WP_Query('post_type=project');

// Iterate the newly created Query object
while($my_query->have_posts()) :
    $my_query->the_post();
    ?>
    <article>
        <h1><?php the_title(); ?></h1>
        <?php the_content(); ?>
    </article>
    <?php
endwhile;
wp_reset_postdata();

Let’s break that down:

$my_query = new WP_Query('post_type=foo');
This configures the query. All the parameters are listed in the Codex WP_Query article. In essence, $my_query now contains an object that contains all the posts requested.
while($my_query->have_posts()) :
Start the while loop with the help of the have_posts() helper function.
$my_query->the_post()
Then make it possible to access the custom template tags like the_title() inside the loop.
endwhile
End the while loop.
wp_reset_postdata()
It’s better to be safe than sorry and reset the query after using it.

For me, the whole business of looping became much more approachable when I learned how the actual Query object looks like, let’s jump to that next.

The anatomy of the Query Object

WP_Query($args) essentially gets a PHP Object, below is an example object that displays all the posts from custom post type named “project”. It’s quite long indeed, but that’s how all data is transported in WP. Seeing it, hopefully strips the magic out of the loop. In essence, it’s everything you ever need to display anything in WordPress. When you got the Query, it’s like having a hydrogen bomb, you got no problems.

Objects can be printed out in human readable form with the print_r() PHP funciton, like so:

<pre><code>
<?php print_r($my_query); ?>
</code></pre>

In my case, the Query Object would look like this, it’s quite large:

WP_Query Object
(
    [query] => Array
        (
            [post_type] => project
        )

    [query_vars] => Array
        (
            [post_type] => project
            [error] => 
            [m] => 
            [p] => 0
            [post_parent] => 
            [subpost] => 
            [subpost_id] => 
            [attachment] => 
            [attachment_id] => 0
            [name] => 
            [static] => 
            [pagename] => 
            [page_id] => 0
            [second] => 
            [minute] => 
            [hour] => 
            [day] => 0
            [monthnum] => 0
            [year] => 0
            [w] => 0
            [category_name] => 
            [tag] => 
            [cat] => 
            [tag_id] => 
            [author] => 
            [author_name] => 
            [feed] => 
            [tb] => 
            [paged] => 0
            [comments_popup] => 
            [meta_key] => 
            [meta_value] => 
            [preview] => 
            [s] => 
            [sentence] => 
            [fields] => 
            [menu_order] => 
            [category__in] => Array
                (
                )

            [category__not_in] => Array
                (
                )

            [category__and] => Array
                (
                )

            [post__in] => Array
                (
                )

            [post__not_in] => Array
                (
                )

            [tag__in] => Array
                (
                )

            [tag__not_in] => Array
                (
                )

            [tag__and] => Array
                (
                )

            [tag_slug__in] => Array
                (
                )

            [tag_slug__and] => Array
                (
                )

            [post_parent__in] => Array
                (
                )

            [post_parent__not_in] => Array
                (
                )

            [author__in] => Array
                (
                )

            [author__not_in] => Array
                (
                )

            [ignore_sticky_posts] => 
            [suppress_filters] => 
            [cache_results] => 1
            [update_post_term_cache] => 1
            [update_post_meta_cache] => 1
            [posts_per_page] => 10
            [nopaging] => 
            [comments_per_page] => 50
            [no_found_rows] => 
            [order] => DESC
        )

    [tax_query] => WP_Tax_Query Object
        (
            [queries] => Array
                (
                )

            [relation] => AND
            [table_aliases:protected] => Array
                (
                )

            [queried_terms] => Array
                (
                )

            [primary_table] => wp_joh5_posts
            [primary_id_column] => ID
        )

    [meta_query] => WP_Meta_Query Object
        (
            [queries] => Array
                (
                )

            [relation] => 
            [meta_table] => 
            [meta_id_column] => 
            [primary_table] => 
            [primary_id_column] => 
            [table_aliases:protected] => Array
                (
                )

        )

    [date_query] => 
    [request] => SELECT SQL_CALC_FOUND_ROWS  wp_joh5_posts.ID FROM wp_joh5_posts  WHERE 1=1  AND wp_joh5_posts.post_type = 'project' AND (wp_joh5_posts.post_status = 'publish')  ORDER BY wp_joh5_posts.post_date DESC LIMIT 0, 10
    [posts] => Array
        (
            [0] => WP_Post Object
                (
                    [ID] => 43
                    [post_author] => 1
                    [post_date] => 2015-03-19 16:37:18
                    [post_date_gmt] => 2015-03-19 16:37:18
                    [post_content] => 
                    [post_title] => Opus
                    [post_excerpt] => 
                    [post_status] => publish
                    [comment_status] => open
                    [ping_status] => open
                    [post_password] => 
                    [post_name] => 43-2
                    [to_ping] => 
                    [pinged] => 
                    [post_modified] => 2015-04-13 12:53:20
                    [post_modified_gmt] => 2015-04-13 12:53:20
                    [post_content_filtered] => 
                    [post_parent] => 0
                    [guid] => http://example.dev/?p=43
                    [menu_order] => 2
                    [post_type] => project
                    [post_mime_type] => 
                    [comment_count] => 0
                    [filter] => raw
                )

            [1] => WP_Post Object
                (
                    [ID] => 37
                    [post_author] => 1
                    [post_date] => 2015-03-19 15:59:30
                    [post_date_gmt] => 2015-03-19 15:59:30
                    [post_content] => 
                    [post_title] => Näytös
                    [post_excerpt] => 
                    [post_status] => publish
                    [comment_status] => open
                    [ping_status] => open
                    [post_password] => 
                    [post_name] => naytos
                    [to_ping] => 
                    [pinged] => 
                    [post_modified] => 2015-04-13 12:53:20
                    [post_modified_gmt] => 2015-04-13 12:53:20
                    [post_content_filtered] => 
                    [post_parent] => 0
                    [guid] => http://example.dev/?p=37
                    [menu_order] => 1
                    [post_type] => project
                    [post_mime_type] => 
                    [comment_count] => 0
                    [filter] => raw
                )

            [2] => WP_Post Object
                (
                    [ID] => 22
                    [post_author] => 1
                    [post_date] => 2015-03-17 15:17:17
                    [post_date_gmt] => 2015-03-17 15:17:17
                    [post_content] => 
                    [post_title] => Pile of Shit
                    [post_excerpt] => 
                    [post_status] => publish
                    [comment_status] => open
                    [ping_status] => open
                    [post_password] => 
                    [post_name] => pile-of-shit
                    [to_ping] => 
                    [pinged] => 
                    [post_modified] => 2015-04-13 12:23:11
                    [post_modified_gmt] => 2015-04-13 12:23:11
                    [post_content_filtered] => 
                    [post_parent] => 0
                    [guid] => http://example.dev/?p=22
                    [menu_order] => 3
                    [post_type] => project
                    [post_mime_type] => 
                    [comment_count] => 0
                    [filter] => raw
                )

        )

    [post_count] => 3
    [current_post] => -1
    [in_the_loop] => 
    [post] => WP_Post Object
        (
            [ID] => 43
            [post_author] => 1
            [post_date] => 2015-03-19 16:37:18
            [post_date_gmt] => 2015-03-19 16:37:18
            [post_content] => 
            [post_title] => Opus
            [post_excerpt] => 
            [post_status] => publish
            [comment_status] => open
            [ping_status] => open
            [post_password] => 
            [post_name] => 43-2
            [to_ping] => 
            [pinged] => 
            [post_modified] => 2015-04-13 12:53:20
            [post_modified_gmt] => 2015-04-13 12:53:20
            [post_content_filtered] => 
            [post_parent] => 0
            [guid] => http://example.dev/?p=43
            [menu_order] => 2
            [post_type] => project
            [post_mime_type] => 
            [comment_count] => 0
            [filter] => raw
        )

    [comment_count] => 0
    [current_comment] => -1
    [found_posts] => 3
    [max_num_pages] => 1
    [max_num_comment_pages] => 0
    [is_single] => 
    [is_preview] => 
    [is_page] => 
    [is_archive] => 1
    [is_date] => 
    [is_year] => 
    [is_month] => 
    [is_day] => 
    [is_time] => 
    [is_author] => 
    [is_category] => 
    [is_tag] => 
    [is_tax] => 
    [is_search] => 
    [is_feed] => 
    [is_comment_feed] => 
    [is_trackback] => 
    [is_home] => 
    [is_404] => 
    [is_comments_popup] => 
    [is_paged] => 
    [is_admin] => 
    [is_attachment] => 
    [is_singular] => 
    [is_robots] => 
    [is_posts_page] => 
    [is_post_type_archive] => 1
    [query_vars_hash:WP_Query:private] => 298e5e8168ac1e986f3e96c14d61c221
    [query_vars_changed:WP_Query:private] => 
    [thumbnails_cached] => 
    [stopwords:WP_Query:private] => 
)

All the data we need is there! Notice roughly in half way of the Object the [posts] array, that is of great interest to us, lets drill into it:

print_r($my_query->posts);

That narrows the massive object down to an array of post objects:

Array
(
    [0] => WP_Post Object
        (
            [ID] => 43
            [post_author] => 1
            [post_date] => 2015-03-19 16:37:18
            [post_date_gmt] => 2015-03-19 16:37:18
            [post_content] => 
            [post_title] => Opus
            [post_excerpt] => 
            [post_status] => publish
            [comment_status] => open
            [ping_status] => open
            [post_password] => 
            [post_name] => 43-2
            [to_ping] => 
            [pinged] => 
            [post_modified] => 2015-04-13 12:53:20
            [post_modified_gmt] => 2015-04-13 12:53:20
            [post_content_filtered] => 
            [post_parent] => 0
            [guid] => http://example.dev/?p=43
            [menu_order] => 2
            [post_type] => project
            [post_mime_type] => 
            [comment_count] => 0
            [filter] => raw
        )

    [1] => WP_Post Object
        (
            [ID] => 37
            [post_author] => 1
            [post_date] => 2015-03-19 15:59:30
            [post_date_gmt] => 2015-03-19 15:59:30
            [post_content] => 
            [post_title] => Something
            [post_excerpt] => 
            [post_status] => publish
            [comment_status] => open
            [ping_status] => open
            [post_password] => 
            [post_name] => naytos
            [to_ping] => 
            [pinged] => 
            [post_modified] => 2015-04-13 12:53:20
            [post_modified_gmt] => 2015-04-13 12:53:20
            [post_content_filtered] => 
            [post_parent] => 0
            [guid] => http://example.dev/?p=37
            [menu_order] => 1
            [post_type] => project
            [post_mime_type] => 
            [comment_count] => 0
            [filter] => raw
        )

    [2] => WP_Post Object
        (
            [ID] => 22
            [post_author] => 1
            [post_date] => 2015-03-17 15:17:17
            [post_date_gmt] => 2015-03-17 15:17:17
            [post_content] => 
            [post_title] => Something Else
            [post_excerpt] => 
            [post_status] => publish
            [comment_status] => open
            [ping_status] => open
            [post_password] => 
            [post_name] => pile-of-shit
            [to_ping] => 
            [pinged] => 
            [post_modified] => 2015-04-13 12:23:11
            [post_modified_gmt] => 2015-04-13 12:23:11
            [post_content_filtered] => 
            [post_parent] => 0
            [guid] => http://example.dev/?p=22
            [menu_order] => 3
            [post_type] => project
            [post_mime_type] => 
            [comment_count] => 0
            [filter] => raw
        )

)

To get any usable data (strings) out of it, more deeper probing is needed:

// This would return array with only the first post in it
print_r($my_query->posts[0]);

// This would get a string, the title of the first post
$first_post_title = $my_query->posts[0]->post_title;

With the same methodology anything can be accessed in the Object. Usually this is done, you guessed it, with a loop. Below is a simplified example:

$my_query = new WP_Query('post_type=project');

foreach($my_query->posts as $post) {
    ?>
    <h1>
        <?php echo $post->post_title; ?>
    </h1>
    <?php
    echo $post->post_content;
}

That’s just PHP, nothing to do with WordPress really, and a valid way to use the Query Object if so desired. But WordPress provides, the earlier mentioned, very handy helper functions: the_post() and have_posts(). Those enable us to access the data in the post more easily with, for example, the_content() and the_title() template tags.

There’s also a more coherent way to access the posts, and that’s the aptly names get_posts() function.

get_posts loops

The most appropriate use for get_posts is to create an array of posts based on a set of parameters. It retrieves a list of recent posts or posts matching this criteria. —Codex

This is really handy in many places!

The basics:

<?php
// Get the object of posts
$my_posts = get_posts('post_type=project');
// Show its content
print_r($my_posts);

That gets us exactly the same as the before mentioned:

$my_query = new WP_Query('post_type=project');
print_r($my_query->posts);

An array of WP_Post objects, like so:

Array
(
    [0] => WP_Post Object
        (
            [ID] => 43
            [post_author] => 1
            [post_date] => 2015-03-19 16:37:18
            [post_date_gmt] => 2015-03-19 16:37:18
            [post_content] => 
            [post_title] => Opus
            [post_excerpt] => 
            [post_status] => publish
            [comment_status] => open
            [ping_status] => open
            [post_password] => 
            [post_name] => 43-2
            [to_ping] => 
            [pinged] => 
            [post_modified] => 2015-04-13 12:53:20
            [post_modified_gmt] => 2015-04-13 12:53:20
            [post_content_filtered] => 
            [post_parent] => 0
            [guid] => http://example.dev/?p=43
            [menu_order] => 2
            [post_type] => project
            [post_mime_type] => 
            [comment_count] => 0
            [filter] => raw
        )

    [1] => WP_Post Object
        (
            [ID] => 37
            [post_author] => 1
            [post_date] => 2015-03-19 15:59:30
            [post_date_gmt] => 2015-03-19 15:59:30
            [post_content] => 
            [post_title] => Something
            [post_excerpt] => 
            [post_status] => publish
            [comment_status] => open
            [ping_status] => open
            [post_password] => 
            [post_name] => naytos
            [to_ping] => 
            [pinged] => 
            [post_modified] => 2015-04-13 12:53:20
            [post_modified_gmt] => 2015-04-13 12:53:20
            [post_content_filtered] => 
            [post_parent] => 0
            [guid] => http://example.dev/?p=37
            [menu_order] => 1
            [post_type] => project
            [post_mime_type] => 
            [comment_count] => 0
            [filter] => raw
        )

    [2] => WP_Post Object
        (
            [ID] => 22
            [post_author] => 1
            [post_date] => 2015-03-17 15:17:17
            [post_date_gmt] => 2015-03-17 15:17:17
            [post_content] => 
            [post_title] => Something Else
            [post_excerpt] => 
            [post_status] => publish
            [comment_status] => open
            [ping_status] => open
            [post_password] => 
            [post_name] => pile-of-shit
            [to_ping] => 
            [pinged] => 
            [post_modified] => 2015-04-13 12:23:11
            [post_modified_gmt] => 2015-04-13 12:23:11
            [post_content_filtered] => 
            [post_parent] => 0
            [guid] => http://example.dev/?p=22
            [menu_order] => 3
            [post_type] => project
            [post_mime_type] => 
            [comment_count] => 0
            [filter] => raw
        )

)

Below is and example how to pull in 5 latest post from “project” post type, into a list:

<?php
$args = array(
    'posts_per_page' => 5,
    'post_type'      => 'projects'
);
$myposts = get_posts($args);
?>
<ul class="latest-posts">
    <?php
    foreach($myposts as $post) :
        setup_postdata($post);
        ?>
        <li>
            <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
        </li>
        <?php
    endforeach;
    wp_reset_postdata();
    ?>
</ul>

Let’s boil that down:

$args = array(...
Set the parameters as an array this time, they can be passed in the query parameter format also (more below).
$myposts = get_posts($args)
Gets the array of posts into a variable called $myposts.
foreach($myposts as $post) :
Start a standard PHP foreach loop.
endforeach
End the loop.
wp_reset_postdata()
Reset post data so that the loop won’t interrupt any future loops.

Functionify it

The previous example can be abstracted into a function easily, to keep the templates clean and spaghetti code to minimum. That’s would go into the themes functions.php file:

/**
 * Gets 5 latest posts from a wanted post type
 * @param  array   $post_type Array fo post types
 * @param  integer $amount    How many posts
 * @return string             A HTML list
 */
function cm_get_latest_posts($post_type = array('post'), $amount = 5) {
    // The global $post object is needed
    global $post;

    // Configure the object
    $args = array(
        'post_type'      => $post_type,
        'posts_per_page' => $amount
    );

    // Get the posts
    $myposts = get_posts($args);
    $output = "";

    // Loop the posts
    foreach ($myposts as $post) {
        setup_postdata($post);
        // Put stuff in variables so it's easier to compose the <li> element
        $title = get_the_title();
        $permalink = get_the_permalink();
        $output .= "<li><a href='$permalink'>$title</a></li>";
    }
    wp_reset_postdata();

    // Finally return it
    return $output;
}

Then call it in a template, like sidebar.php:

<ul>
    <?php echo cm_get_latest_posts('project'); ?>
</ul>

That’ll get 5 posts from the “project” post types. Great use for get_posts().

2 ways to configure the Query

There’s few different ways to pass the Query some parameters:

  1. The query parameter method
  2. and the array method.
// Query parameter method, good if there are very few parameters
$my_query = new WP_Query('post_type=foo');

// Array method is easier to read when there are a lot of parameter
$args = array(
    'post_type' => 'foo',
    'order'     => 'ASC',
    'order_by'  => 'menu_order'
);
$my_query = new WP_Query($args);

See all the parameters in the Codex WP_Query article.

Takeaways and conclusions

  • pre_get_posts for editing the main loop.
  • WP_Query for sideloops.
  • get_posts makes using WP_Query a bit easier.

Here’s a cool diagram of the Query (loving the typography):

Is that Comic Sans?
Is that Comic Sans?

Andrew Nacin’s slides “You don’t know Query” slides, very enlightening:

See the talk on wordpress.tv.

Hopefully this was helpful. I’m just drafting post with more higher lever looping tricks. Meanwhile you might be interested to read: “Advanced WordPress loop techniques“.

Club-Mate, the beverage → club-mate.fi