The Loop

This article is part of the WordPress guide. Read the introduction.

The Loop is one of the most famous and fundamental WordPress concepts. It’s how you display the content from the database in each template. Let’s cut to the chase.

The Loop is just an iterator pattern. It’s not something you can directly touch. There’s no the_loop() function. In this case, it’s best to start with a code snippet:

PHP
if ( have_posts() ) :
    while ( have_posts() ) : the_post();
        the_content();
    endwhile;
else :
    echo 'No posts to display';
endif;

That’s it. It’s a while loop. The function have_posts() checks if there are any posts left to iterate over in the global $wp_query variable. The function the_post() sets the global $post variable to the next post in order. You display the contents of each post inside the while loop (in whatever way you want to display it).

By convention, the_post() is called on the same line as the while loop. Functionally, it’s the same as calling it on the first line of the loop. You should also use the endwhile syntax instead of curly braces. The reason is simple – you’re going to be putting HTML inside that loop, and it’s way easier to find where the loop ends with an endwhile instead of a closing brace (the same rule applies to conditionals).

Let’s now imagine we create an archive-thm_book.php file inside our theme. According to the template hierarchy, this file will be used to display the archive page for our custom book post type. The global $wp_query object will hold n posts of type book (up to 10 by default). We can now create the HTML of the “book card” inside The Loop, just like product cards in e-commerce stores. By iterating over this HTML in The Loop, we’ll display a list of books on our books archive page. The user will be able to see it once they head over to the example.com/books/ page.

$post & Template Tags

WordPress defines an important global variable – $post. It’s a variable of type WP_Post. It holds the currently iterated over post in The Loop, and the first post in $wp_query before The Loop is run. It is populated by the the_post() function during each iteration. You can interact with it directly, but you shouldn’t. You should use template tags instead.

Template tags are just php functions meant to be used in templates to get dynamic data. Fancy name, simple idea. Some template tags are not based on $post, e.g., bloginfo( $param ), get_header(), get_footer(), etc., but most are, e.g., the_title(), the_content(), get_the_author(), get_the_date(), etc. One of the most important template tags is the_content(), which renders the contents of the post’s post_content column in the database.

Template tags prefixed with ‘get_the_’ return the requested data (usually as a string). You can store this data in a variable and do something with it before displaying it. Template tags prefixed with ‘the_’ display the data for you. They are, in essence, just wrappers echoing their ‘get_the_’ equivalents.

As already noted, before running The Loop, the global $post contains the first post returned by $wp_query. That is convenient for single posts (such as the single.php template), as you don’t have to run The Loop. You can just use template tags directly, because $post is set to the first (and only) post in $wp_query. This behavior can seem counter-intuitive for templates such as archive.php, where the_title() would not return the title of the archive, but of its first post.

Secondary Loops & WP_Query

Sometimes you need a different Loop than the one created automatically by WordPress. Some potential cases with that requirement are:

  • Showing 5 most recent news articles on an about us page.
  • Displaying related products at the bottom of a product page.
  • Creating a complex front page with different sections for products, featured posts, categories, etc.

You can’t get that data from The Loop, as those posts aren’t part of the query run by WordPress for the given page. In that case, you need to create a custom query. You can use the WP_Query class to achieve that.

To run a custom query, you need to create an object of this class and pass an $args array to its constructor. The query will be run, and the posts will be available in the object. You can fetch them and run a Loop the same way you do in The Loop – with have_posts() and the_post() (but called on the query object). As a matter of fact, the global $wp_query object is an object of this class, and the main query is created in the same way.

Code Example

In this case, it’s best to start with a code example and explain it later.

PHP
<h3>Latest News Stories</h3>
<ul>

<?php
// Step 1: Define the arguments for our custom query.
$args = [
    'post_type'      	=> 'post', // We want standard posts.
    'posts_per_page' 	=> 5,      // We want 5 of them.
    'category_name'  	=> 'news', // Only from the 'news' category.
    'orderby'       	=> 'date', // Order them by publication date.
    'order'     	   => 'DESC',  // The newest ones first.
];

// Step 2: Create a new WP_Query object.
$news_query = new WP_Query( $args );

// Step 3: Run a new Loop using our query's methods.
if ( $news_query->have_posts() ) :
    while ( $news_query->have_posts() ) :
        $news_query->the_post(); // This sets up the global $post.
        ?>

        <li>
            <a href="<?php the_permalink(); ?>">
                <?php the_title(); ?>
            </a>
        </li>

    <?php
    endwhile;
endif;

$args

This is the array of arguments that controls what the final SQL query looks like. It provides a lot of control. Some of the most commonly used options are:

  • post_type
  • post_status
  • name (retrieves a post by its slug)
  • post__in (retrieves only posts with provided IDs)
  • post__not_in (excludes posts with provided IDs)
  • posts_per_page (number of posts for pagination)
  • paged (the page number – if posts_per_page is 10 and paged is 3, posts 21-30 will be returned)
  • orderby
  • order (DESC or ASC)
  • cat (retrieves posts from a specific category by its ID)
  • category_name (same as cat but by slug)
  • tax_query (complex taxonomy query, described in detail later)
  • meta_key (retrieves posts that have a specified meta key)
  • meta_value (the value to match to the meta_key)
  • meta_compare (comparison operator, e.g, =, !=, >, <, LIKE, IN, etc.)
  • meta_query (complex meta query, described in detail later)

This list is nowhere near complete. Go read the official WP_Query Documentation if you need a list of all parameters along with instructions on how to use them.

tax_query

The tax_query parameter allows you to filter the query based on multiple taxonomies with different relations. tax_query is an array. This array can have a ‘relation’ parameter and other arrays. The actual filtering is done in the inner arrays. It’s hard to explain, just look at the code example:

PHP
// [...]
'tax_query' => array(
	'relation' => 'AND',
	array(
		'taxonomy' => 'category',
		'field' => 'slug',
		'terms' => 'technology',
		'operator' => 'IN',
	),
	array(
		'taxonomy' => 'tag',
		'field' => 'slug',
		'terms' => 'opinion',
		'operator' => 'NOT IN',
	),
),
// [...]

This query will make it so that all the posts returned are in the technology category AND do not have the opinion tag. The ‘relation’ parameter controls the relation between the inner arrays. The inner array has 5 options:

  • taxonomy – the name of the taxonomy being searched.
  • field – what the term is selected by. Possible values are ‘term_id’, ‘name’, ‘slug’, or ‘term_taxonomy_id’.
  • terms – taxonomy term(s).
  • operator – operator to test. Possible values are ‘IN’, ‘NOT IN’, ‘AND’, ‘EXISTS’, and ‘NOT EXISTS’.
  • include_children – whether or not to include children for hierarchical taxonomies.

You can also nest outer arrays in the inner arrays to create more complex queries. For example, you could have: “return posts that are in the technology category OR are in the business category AND have an AI tag”. The logical expression would be: (technology || (business && AI)). I’m not going to include a code example as that would be too long. Go read the documentation.

meta_query

meta_query is like tax_query but for metadata instead. It queries the wp_postmeta table. The most typical use case is to filter by data added with custom fields. The structure is the same as with tax_query. You have outer and inner arrays. You can have OR or AND relationships. You can nest outer arrays inside inner arrays, creating more complex queries.

The parameters in the inner array are:

  • key – meta_key in the database.
  • value – meta_value in the database.
  • compare – operator to test. Possible values are ‘=’, ‘!=’, ‘>’, ‘>=’, ‘<‘, ‘<=’, ‘LIKE’, ‘NOT LIKE’, ‘IN’, ‘NOT IN’, ‘BETWEEN’, ‘NOT BETWEEN’, ‘EXISTS’, ‘NOT EXISTS’, ‘REGEXP’, ‘NOT REGEXP’, and ‘RLIKE’.
  • type – the value type. Possible values are ‘NUMERIC’, ‘BINARY’, ‘CHAR’, ‘DATE’, ‘DATETIME’, ‘DECIMAL’, ‘SIGNED’, ‘TIME’, ‘UNSIGNED’. This parameter is important as if you were to sort numbers with type ‘CHAR’, you’d get: 1, 2, 3, 32, 4, 49, 5, etc.

Running the Loop & wp_reset_postdata()

Mind the lowercase ‘t’ in ‘the Loop’. The name ‘The Loop’ is by convention the main loop based on the global $wp_query. Here we’re talking about a secondary loop based on our custom query.

To run the main Loop, we could’ve just used the have_posts() and the_post() functions. These functions are nothing more than wrappers over $wp_query->have_posts() and $wp_query->the_post(). Running a Loop based on a different query is as simple as calling those methods on the other query’s object, which is what we’re doing here.

It’s important to note that $news_query->the_post() sets the global $post variable to the current post, just like The Loop does. This is good, because it means you can use Template Tags. The side effect is that when your loop ends, the global $post holds the last post from your secondary Loop. If you were in the single.php template, and were to now call the_content() outside of any loops, the content of the ‘rogue’ post would be rendered instead of the content of the main post the user wants to see. This problem is solved by the wp_reset_postdata() function.

wp_reset_postdata() resets the global $post variable to the one from the main $wp_query. If you run The Loop before your secondary Loop, then after you call this function, the $post will be the last post from the main query. If you call it before the main Loop, or you don’t have The Loop at all (like in single.php), the $post will be restored to the first post in $wp_query. Bottom line – the $post goes back to whatever it was before you ran your custom loop. The rule is very simple. Always call wp_reset_postdata() after executing a secondary Loop. Not doing so can cause many very confusing bugs.

get_posts()

The WP_Query class is a powerful tool, but with great power comes great complexity. There’s a much simpler function that suffices in many situations: get_posts( $args ). This function is just a wrapper over WP_Query. It is supposed to be used with just a few parameters, and it returns an array of posts (theoretically you could supply any parameter you can to WP_Query).

The only arguments advertised by the documentation are:

  • numberposts – number of posts to be returned (alias of posts_per_page in WP_Query).
  • category – category or categories specified by ID (alias of cat in WP_Query).
  • include – array of post IDs to retrieve (alias of post__in in WP_Query).
  • exclude – array of post IDs to exclude (alias of post__not_in in WP_Query).
  • suppress_filters – if true, the query will not be passed through filters, such as pre_get_posts, posts_where, etc. Be careful, this argument is set to true by default (WP_Query also accepts suppress_filters, but it defaults to false there).

As already noted, this function returns an array of WP_Post objects (or post IDs if ‘fields’=>’ids’). I’m not going to tell you what to do with that data. You should know what you’re trying to achieve. But I’m going to show you how you can run a secondary Loop with that:

PHP
$my_posts = get_posts( $args );
if ( $my_posts ) {
    foreach ( $my_posts as $post ) { // Use a standard foreach loop
        setup_postdata( $post ); // Manually set up post data
        // ... do stuff with template tags ...
    }
}
wp_reset_postdata(); // Still need to reset!

This is the same thing as running a Loop directly on a WP_Query object. You can see we use setup_postdata() instead of the_post(). This is a more low-level function. It’s actually used by WP_Query::the_post() to set the global $post variable. This means you still have to call wp_reset_postdata() after running such a loop.

If you want to run a simple loop where you don’t need to use template tags, and you don’t want to modify the global $post, you can do that as well. In that case just don’t call setup_postdata(). You can then access post properties directly, such as: $post->ID, $post->post_title, etc.