It's hard to believe, I know, but despite its massive list of available parameters, WP_Query
somehow doesn't seem to have the tools available to look for grandchildren (and beyond) when querying hierarchical post types. Due to it only supporting the post_parent
field, you end up having to nest queries, use custom SQL, or use post__in
with a secondary call of some sort to fill the "in" set of posts through which WP_Query
will look.
What's the use case here? Well, say you want to perform a text search but only across some nested subset of pages. These pages don't all have a common parent page, because of all the nesting, so WP_Query
won't find any past the first level down. When checking for a parent, it's literally comparing against the post_parent
field in the posts database.
The solution I found to this silly problem was in get_pages()
⸺ the hierarchical brother of the tried and true get_posts()
function. It offers two main parameters, child_of
and post_type
, and that's exactly what we need! See, while the function does contain "pages" in its name, because it's ancient and predates custom post types, it's not limited to literal pages. It actually supports any hierarchical post type thanks to that post_type
parameter.
The key difference is that while WP_Query
compares against the post_parent
database field, the child_of
parameter of get_pages()
instead dives down the hierarchy. In fact, if you check its source, it overrides the results of WP_Query
, which is otherwise used for all types of queries except this one, and it does so by using another ancient function called get_page_children()
.
// […]
$pages = new WP_Query();
$pages = $pages->query( $query_args );
if ( $child_of || $hierarchical ) {
$pages = get_page_children( $child_of, $pages );
}
// […]
To combine these two and get our search running over a subset of nested pages, we use the post__in
parameter of WP_Query
as mentioned above.
new WP_Query([
'orderby' => 'title',
'order' => 'ASC',
'paged' => $paged,
'post_status' => 'publish',
'post_type' => $type,
'post__in' => get_pages(['child_of' => 42]),
's' => $search
]);
This is just a snippet; you'll have to fill in the variables and whatnot yourself as relevant, of course. But in using get_pages()
and post__in
like this, we expand the scope of posts WP_Query
will consider compared to its post_parent
parameter.
I called this problem "silly" above, and that's because it seems odd to me that this child_of
parameter doesn't exist in WP_Query
. Both get_pages()
and WP_Query
were introduced way back in v1.5.0, and get_pages()
now uses WP_Query
within itself ⸺ most of the time. I'm not sure why the core developers haven't added true child_of
functionality to WP_Query
yet. Maybe this issue is so niche no one's had reason to notice.