The main problem is a misunderstanding of PHP objects and the 2 methods you’re using:
$a = get_posts( ... );
$b = new WP_Query( ... );
$a
is an array ofWP_Post
objects- e.g.
foreach ( $a as $p ) { ... }
- e.g.
$b
is an object of typeWP_Query
- e.g.
while ( $b->have_posts() ) { $b->the_post(); .... }
- e.g.
Most importantly, get_posts
is a wrapper around WP_Query
! So much so that the array of posts is $b->posts
, but beware that by accessing it you skip any lazy loading optimisations in place. The same is somewhat true of using get_posts
, and you would still need to call setup_postdata
inside the loop for each post to set the current post, followed by wp_reset_postdata
at the end.
A Note on wp_reset_query
This function is not for cleaning up after get_posts
or WP_Query
. Though it will appear to work, it does more than that. This is because it’s intended to reset the query itself after using query_posts(...)
to overwrite the main query. Since query_posts
is a major mistake in 99% of cases it’s safe to say you should never use it, and neither should you use wp_reset_query
.
Instead, this is how you perform a standard post loop using a query:
$args = [ ... ];
$query = new WP_Query( $args );
if ( $query->have_posts() ) {
while( $query->have_posts() ) {
$query->the_post();
// output inner loop, get_the_ID and standard functions such as `the_title()` will work in here
}
wp_reset_postdata();
}
Notice I did not do foreach ( $query->posts as $post ) {
as this would not set the current post globals, and it would not perform lifecycle filters and hooks. I also only call wp_reset_postdata
at the very end after the loop, but, I do it inside the if condition so it does not happen if there are no posts. I also checked if the query had posts, this gives an opportunity to print out things before/after the loop such as <ul>
and </ul>
etc
Further reading: