What methods are available for making a custom post query, and when are they best to use? This post aims to establish a solid understanding of two methods of querying posts, how to access the results, how to build arguments, and finally how to clean up after it. The two methods we’ll look at are get_posts() and WP_Query.

When it comes to make a new post query, there are really two options (at the time of writing). The choice really just depends on your preference (and some minor performance effect). One option has the potential to mess up the global loop/query that you are currently in, unless you handle it correctly. With one option you handle objects and in the other option you handle an array. The arguments for customizing the post query are however identical.

Global query?

If you are unsure what I mean with “messing up the global query”, it’s this. WordPress always make one global query, depending on which page you are at. If a visitor visits a category archive, WordPress has made a post query for this. The theme would normally access posts in the query using “the loop”. When we make a new query with its own loop inside this loop, we need to make sure the WordPress’ global query and our query are handled separately.

However if you want to modify WordPress’ post query, that’s another story. I have another post that goes into depth about how to do that.

Let’s look at the two options we have and how we handle them. After that we’ll look at the arguments for customizing the query. Keep in mind that the arguments are identical for both.

The two methods of querying posts

You can query posts with either the function get_posts() or making a new instance of WP_Query. The first option returns an array of posts and in the second you handle an object. Because get_posts() returns an array of just the posts it’s usually simpler to use this anywhere you’d like. However if you want to paginate your query, you should definitely go for creating a WP_Query instance.

The function get_posts is a wrapper function for WP_Query which means it accepts the same arguments, but get_posts has a few additional “alias” arguments. WordPress’ documentation page for get_posts does not list out possible arguments (except the alias arguments), but refers to the documentation page for WP_Query for the arguments. We’ll look closer at the arguments later on.

If performance is an issue (i.e. the site has a lot of posts), get_posts is faster than using WP_Query because it skips the calculation for pagination.

The method of looping through your custom post query differs depending on which method you choose. You should be familiar with the common WordPress loop used in almost all theme templates:

if (have_posts()) {
	while (have_posts()) : the_post();
		// Access to each post; you can use template tags here
	endwhile;
}

Looping using WP_Query

Looping the results from using WP_Query is exactly the same, except that we specifically refer to the instance object in the loop. We also need to remember to “reset the state” after we are done looping so that the global post object gets set back to what it was before. To do that we use wp_reset_postdata().

$custom_query = new WP_Query([/* Arguments here */]);
if ($custom_query->have_posts()) {
	while ($custom_query->have_posts()) : $custom_query->the_post();
		// Access to each post; you can use template tags here
	endwhile;
	wp_reset_postdata();
}

If you dump the object instantiated by WP_Query ($custom_query in the above example) you will find the full query and arguments used. The interesting parts here are the properties ‘found_posts‘ and ‘posts‘. The ‘posts‘ property contains the result of post objects which the loop will run through. Number of posts that matched your query is returned in ‘found_posts‘ and is useful if you want to make a custom pagination. Divide this value wtih WordPress’ setting for number of posts per page to figure out how many pages you need for your query, or simply refer to the property ‘max_num_pages‘.

Note: When you use get_posts WordPress returns only the ‘posts‘ property (which is an array) from the WP_Query object.

Looping using get_posts

By using get_posts we don’t use the usual “WordPress loop”, instead we use a normal PHP array loop. Each element in the array are post objects, and no resetting is necessary after you are done looping. Keep in mind that template tags (such as the_title(), the_permalink() etc) are not available inside this loop. You’ll need to refer to the post object properties (e.g. $custom_post->ID).

$custom_query = get_posts([/* Arguments here */]);
foreach ($custom_query as $custom_post) {
	// Template tags are not available here, refer to the post object properties, for example:
	echo $custom_post->post_title;
}

I recommend that you name your post objects something different than $post. You might encounter issues when trying to access post properties (it might refer to the global post object and not the post in the loop).

If you want to use template tags for easier accessing post information (such as the_title() and the_permalink()), you can do so. Do this by telling WordPress to set up the global post object inside the loop with setup_postdata(). If you do this, you will need to reset the state with wp_reset_postdata() after the loop.

$custom_query = get_posts([/* Arguments here */]);
foreach ($custom_query as $post) {
	setup_postdata($post);
	// Template tags are available here, for example:
	the_title();
}
wp_reset_postdata();

Keep in mind that setup_postdata requires the objects you are looping through (the “as” part in the foreach loop) to be named $post! In the first example I named the post objects $custom_post and this would not work with setup_postdata().

However if you just need access to basic post information you might as well skip setting up global post object and rather use the corresponding “get_“-template tags and the post ID. For example the tag the_permalink() only works correctly if the global post object is set up, but you can request the post permalink without the global post object simply by using echo get_the_permalink($custom_post->ID).

Query arguments

You can find the complete list of all possible arguments at WP_Query’s documentation page. Examples of parameters are posts with specific term(s) from a taxonomy, post meta values, post types, inclusion or exclusion of specific posts, and a whole range of options for ordering the results. There’s too many to go through each in detail, but here are some common examples of arguments for querying posts.

Example 1: Related posts from same category

Let’s say you want to show a “related posts” at the end of a single post. It should show a random selection of 3 posts that are in the same category as current post, and it should exclude the current post from the result.

$post_id = get_the_ID();  // current post ID
$custom_query = new WP_Query([
	'post_type' => 'post',
	'posts_per_page' => 3,
	'category__in' => wp_get_post_categories($post_id),
	'post__not_in' => [$post_id],
	'orderby' => 'rand'
]);

The arguments are pretty self-explanatory. I ask for only ‘post‘ in ‘post_type‘ and a maximum of 3 posts in ‘posts_per_page'.

For querying posts in categories, you can build a tax_query or use the simpler ‘category__in‘ (NB: Only works for post category). In the example above I use wp_get_post_categories() to get an array of term IDs assigned to the provided post, and use this for the argument ‘category__in‘.

You can exclude post IDs with ‘post__not_in‘ whereas I provided the current post ID. Finally I asked for a random order of posts by setting ‘rand‘ in ‘orderby‘. You could provide e.g. ‘title‘ or ‘date‘ to order them differently. Take a look at the documentation for ordering to see what is possible.

Example 2: All posts from a custom post type with multiple ordering arguments

In this example we assume that you have a custom post type ‘book‘ and in a custom page template you wish to display all published books. You wish to order the posts primarily by menu_order (the page attribute, it’s a number you can set per post), and secondly post title.

$custom_query = new WP_Query([
	'post_type' => 'book',
	'posts_per_page' => -1,
	'orderby' => ['meny_order' => 'ASC', 'title' => 'DESC']
]);

Again, the arguments are pretty self-explanatory. I request post type ‘book‘ as ‘post_type‘. When you set ‘posts_per_page‘ to -1 it will fetch all (published posts, unless you specify something different in ‘post_status‘ argument). Finally I provide an array to ‘orderby‘ to tell WordPress to sort the posts primarily by menu order in ascending order, and secondly post title in descending order.

Example 3: Posts with custom meta data

Let’s assume you have a custom post type ‘book‘ and you wish to query all books that either is unpublished or a book published between the year 1990 and 2019.

$custom_query = new WP_Query([
	'post_type' => 'book',
	'posts_per_page' => -1,
	'meta_query' => [
		'relation' => 'OR',
		[
			'key' => 'book_status',
			'value' => 'unpublished',
			'compare' => '='
		],
		[
			'key' => 'year_published',
			'value' => [1990, 2019],
			'type' => 'numeric',
			'compare' => 'BETWEEN'
		]
	]
]);

Building a query by using post meta is best done with meta_query (for very simple meta arguments you can use meta_key and meta_value directly). The ‘meta_query‘ argument requires an array, where each argument is an array. You can control the relation between each argument with ‘relation‘, which I set to ‘OR‘ in the above example.

I provide two meta data arguments to ‘meta_query‘. The first simply compares the meta key ‘book_status‘ to the text ‘unpublished‘, and if it is equal it will be included. In the second argument I tell WordPress to get any value in the meta key ‘year_published‘ that is between the numbers 1990 and 2019.

Conclusion

You should now have a basic understanding of the two methods of querying posts. There is not a big difference as you can build the same query using both, but the way of handling the two are different. The three provided examples of query arguments only touch the surface of what queries you can build. Refer to WP_Query documentation for a full overview with plenty of examples.

If you are curious about how to modify the global query that WordPress perform, I have a separate post going into depth about that.