Think in WP-CLI: The loop

Prerequisites

If you want to follow these examples in your local environment, you’ll need a theme with PHP template files, a hybrid theme, and, of course, WP-CLI installed. I’m using Twenty Twenty-One as the last default hybrid theme.

HINT: quickly download and activate Twenty Twenty-One theme

wp theme install --activate twentytwentyone

A problem/use case

When you start writing PHP code in a WordPress theme or plugin, learning the Loop is one of the most important things. It is the base of every template file. Similarly, to perform many highly useful actions with WP-CLI, you need to learn and understand loops in CLI tools.

Examples

List all the posts

This snippet will give different results depending on the template. You’ll get a list of posts in all categories on archive templates, as many as you set in reading options; you’ll get only one on singular templates. Different archives will have different lists. The most simple loop, in the most generic template, the index.php, will just list all posts (limited by the number set in options) in descending order, ordered by published date.

HINT: quickly generate 100 posts

wp post generate
PHP

if ( have_posts() ) :
    while ( have_posts() ) : the_post();
        // Display post content
    endwhile;
endif;

WP-CLI:

wp post list

If we want to go beyond these default templates and queries, which we often do, we must create a custom query. That means modifying arguments for the WP_Query object.

List all posts in category

For example, we want to see all the posts from the Terry Pratchett category. On the category.php template, we would use the first PHP example. But maybe we want to list all these posts on a single.php template as a part of the “Also in this category” section.

HINT: quickly create a new category

wp term create category "Terry Pratchett" --description="The best reporter on the Discworld."

The loop from the first example will give the current post in this template, so we need to create another loop with a custom query to get all the other posts from the same category.

HINT: quickly move all posts from Uncategorized category to Terry Pratchett

for x in $(wp post list --cat=1 --field=ID) ; do wp post term add $x category terry-pratchett && wp post term remove $x category uncategorized ; done
PHP

// We want to get category ID dynamically.
$category = get_the_category( get_the_ID() );

$args = array(
    'post_type'      => 'post',
    'posts_per_page' => 1000,                  // Never use -1, it's very bad for performance.
    'cat'            => $category[0]->term_id, // Assuming the post has only one category.
);

$in_this_category = new WP_Query( $args );

if ( $in_this_category->have_posts() ) :
    while ( $in_this_category->have_posts() ) : $in_this_category->the_post();
        // Display post content
    endwhile;
endif;

// Always reset post data after the custom loop to avoid unexpected results on the rest of the page.
wp_reset_postdata(); 
HINT: quickly find category term ID

wp term list category --fields=term_id,slug

WP-CLI:

wp post list --cat=2

List all posts in the category but exclude the current post

With the previous example, we’re listing all the posts in the category, including the current one. That’s usually not desired, and, more often than not, you’ll want to exclude it. You might be tempted to use post__not_in argument, like this:

PHP

// Get current post ID.
$current_post = get_the_ID();

$args = array(
    'post_type'      => 'post',
    'posts_per_page' => 1000,                  // Never use -1, it's very bad for performance.
    'cat'            => $category[0]->term_id, // Assuming the post has only one category.
    'post__not_in'   => $current_post,         // Also very bad for performance.
);

Even though this example fits very well with my point, which we will come to later, I can not, in my good conscience, leave it here as an example of how to exclude the current post. What you really want to do in this situation is to get all posts with the query and skip the current post later on inside the loop.

PHP

// Get current post ID.
$current_post = get_the_ID();

// We want to get category ID dynamically.
$category = get_the_category( $current_post );

$args = array(
    'post_type'      => 'post',
    'posts_per_page' => 1000,                  // Never use -1, it's very bad for performance.
    'cat'            => $category[0]->term_id, // Assuming the post has only one category.
);

$in_this_category = new WP_Query( $args );

if ( $in_this_category->have_posts() ) :
    while ( $in_this_category->have_posts() ) : $in_this_category->the_post();
        if ( get_the_ID() === $current_post ) {
            continue;
        }
        // Display post content
    endwhile;
endif;

// Always reset post data after the custom loop to avoid unexpected results on the rest of the page.
wp_reset_postdata();

For the sake of the example, let’s say the current post ID is 82.

WP-CLI:

wp post list --cat=2 --post__not_in=82

The thing to note here are parameter names. WP-CLI uses the same parameter names as WP_Query: cat and post__not_in. The only difference is that WP_Query needs them in an array, while WP-CLI prefixes them with --.

Return only post IDs

Let’s say you want to list only post titles as links. You don’t need the whole post object being returned by the query. You’ll only need post IDs and can get them with the fields parameter. You can switch to foreach a loop as well.

PHP

// Get current post ID.
$current_post = get_the_ID();

// We want to get category ID dynamically.
$category = get_the_category( $current_post );

$args = array(
    'post_type'      => 'post',
    'posts_per_page' => 1000,                  // Never use -1, it's very bad for performance.
    'cat'            => $category[0]->term_id, // Assuming the post has only one category.
    'fields'         => 'ids',
);

$in_this_category = new WP_Query( $args );

foreach ( $in_this_category->posts as $id ) {
    if ( $id === $current_post ) {
        continue;
    }

    $permalink = get_the_permalink( $id );
    $title     = get_the_title( $id );
    
    echo '<h3><a href="' , esc_url( $permalink ) , '">' , esc_html( $title ) , '</a></h3>';
}

// Always reset post data after the custom loop to avoid unexpected results on the rest of the page.
wp_reset_postdata();

WP-CLI:

wp post list --cat=2 --post__not_in=82 --fields=ID

This is the only place the WP-CLI parameter has a different name than the WP_Query one.

You may have never thought about what the query returned to you because it was never on your way – you would use only what you needed. Might as well get the whole post object because you never know what the future brings.

However, by default, you’ll get post ID, title, post name, published data, and status fields in WP-CLI. If you’re using the loop to perform some action other than listing all posts, chances are you don’t need all those parameters.

+-----+--------------+-------------+---------------------+-------------+
| ID  | post_title   | post_name   | post_date           | post_status |
+-----+--------------+-------------+---------------------+-------------+
| 105 | Post 101     | post-101    | 2024-01-13 12:02:38 | publish     |
| 85  | Post 81      | post-81     | 2024-01-13 12:02:38 | publish     |
+-----+--------------+-------------+---------------------+-------------+

Because of this, you can really fine-tune what fields you’ll get back from the WP-CLI query. WP_Query has only a few options for the fields argument: allids, and id=>parent; while WP-CLI can use ALL query arguments.

I’ll repeat this: WP-CLI can use any query argument for returning values.

For example, return post title, published and modified date for all posts and order descending by modified date:

wp post list --fields=post_title,post_date,post_modified --order=DESC --orderby=post_modified

Go ahead, try it.

Conclusion

WP-CLI queries are template-agnostic and treat every query as a custom one. Listing posts in WP-CLI has a different purpose than loops in PHP files. Other than informational value, just listing posts is not very useful in the terminal. But performing and understanding loops in WP-CLI is crucial for doing other actions that would take much time in PHP (or dashboard) and only one command in WP-CLI (e.g. deleting tens of thousands of posts).

Helpful commands

Download and activate Twenty Twenty-One theme

wp theme install --activate twentytwentyone

Generate 100 posts

wp post generate

Create a new category

wp term create category "Terry Pratchett" --description="The best reporter on the Discworld."

Find category terms IDs

wp term list category --fields=term_id,slug

Move all posts from Uncategorized category to Terry Pratchett

for x in $(wp post list --cat=1 --field=ID) ; do wp post term add $x category terry-pratchett && wp post term remove $x category uncategorized ; done

Resources

Leave a comment

Your email address will not be published.
You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.