Return parent post with its children using WP_Query?

The question:

In some cases it might be useful to use multiple post & page parameters in your WP_Query object. In my case, I would like to display children of a parent page including the parent page itself.

Visualization of what I want to achieve. Imagine the following pages hierarchically sorted as following:

  • page A
  • page B
    • Child page A
    • Child page B
    • Child page C
  • page C

The bold list items are the posts/pages I want to retrieve.

My first thoughts go out using these two parameters for WP_Query:

$args = array(
   'post_id' => $parent->ID,
   'post_parent' => $parent->ID,
);

Unfortunately, here it will only use one parameter. With the $args above (correct me if I’m wrong) it will output all children posts of the parent post and not the parent post itself as well.

This problem might be solved by gathering all posts needed and putting them in parameter post__in like so:

$args = array(
   'post__in' => $children_and_parent_ids,
);

However there is wp_list_pages() allowing you to include a post(s) and specify the post where you want to include the children of (child_of). Why is this not possible with WP_Query?

An example of what I’m trying to achieve using wp_list_pages():

wp_list_pages(array(
    'include' => $parent->ID,
    'child_of' => $parent->ID,
));

Have a look at the documentation of WP_Query.

The Solutions:

Below are the methods you can try. The first solution is probably the best. Try others if the first one doesn’t work. Senior developers aren’t just copying/pasting – they read the methods carefully & apply them wisely to each case.

Method 1

We can filter the posts_where clause of the generated SQL to also return the parent post/page and not just the parent’s children. Here we will set our own custom argument called wpse_include_parent, which, when set to true, will alter the generated SQL accordingly.

All we need to do inside our posts_where filter is to check if our custom argument is set and that the post_parent argument is set. We then get that value and pass it to the filter to extend our SQL query. What is nice here, post_parent excepts a single integer value, so we only need to validate the value as an integer.

THE QUERY

$args = [
    'wpse_include_parent' => true,
    'post_parent'         => 256,
    'post_type'           => 'page'
    // Add additional arguments
];
$q = new WP_Query( $args );

As you can see, we have set 'wpse_include_parent' => true to “activate” our filter.

THE FILTER

add_filter( 'posts_where', function ( $where, WP_Query $q ) use ( &$wpdb )
{
    if ( true !== $q->get( 'wpse_include_parent' ) )
        return $where;

    /**
     * Get the value passed to from the post parent and validate it
     * post_parent only accepts an integer value, so we only need to validate
     * the value as an integer
     */
    $post_parent = filter_var( $q->get( 'post_parent' ), FILTER_VALIDATE_INT );
    if ( !$post_parent )
        return $where;

    /** 
     * Lets also include the parent in our query
     *
     * Because we have already validated the $post_parent value, we 
     * do not need to use the prepare() method here
     */
    $where .= " OR $wpdb->posts.ID = $post_parent";

    return $where;
}, 10, 2 );

You can extent this as you need and see fit, but this is the basic idea. This will return the parent passed to post_parent and it’s children

Method 2

If all you want is results from the “page” post_type then do as @birgire suggested.

Alternatively you can adapt the following to give you a similar result for not only the page post_type but any custom post type.

$parent = 2;      //change as desired
$type   = 'page'; //change as desired

$child_args = array( 
    'post_type'   => $type, 
    'post_parent' => $parent 
);

$ids = array($parent);
$ids = array_merge($keys, array_keys( get_children( $child_args ) ));

$query = new WP_Query( 
    array( 
        'post_type'      => 'page', 
        'post_status'    => 'publish', 
        'post__in'       => $ids, 
        'posts_per_page' => -1 
    ) 
);

The above is essentially the same thing as hooking onto the posts_where filter and parsing the SQL clause however, this achieves exactly the same thing.

Method 3

    $args = array(
        'post_type' => 'tribe_events',
        'posts_per_page' => '-1',
        'orderby' => 'ID',
        'order' => 'ASC',
        'post_parent' => $postID,
    );

    $children = new WP_Query($args);
    $parent[] = get_post($postID);
    $family = array_merge($parent, $children->get_posts());

This seems to work. Comments?

Method 4

Using global $wpdb combined with [get_results()][1] is an option as well. Performance wise I think this is the best solution since it only runs one query.

Here is my final code.

<ul class="tabs"><?php

    global $wpdb, $post;

    $parent = count(get_post_ancestors($post->ID))-1 > 0 ? $post->post_parent : $post->ID;

    $sql = "SELECT ID FROM `{$wpdb->prefix}posts`";
    $sql.= " WHERE ID='{$parent}' OR post_parent='{$parent}' AND post_type='page'";
    $sql.= " ORDER BY `menu_order` ASC";

    $tabs = $wpdb->get_results($sql);

    $output = '';
    foreach ($tabs as $tab) {
        $current = $post->ID == $tab->ID ? ' class="active"' : '';

        $output .= '<li'.$current.'>';
        $output .= empty($current) ? '<a href="'.get_permalink($tab->ID).'" rel="nofollow noreferrer noopener">' : '';
        $output .=   get_the_post_thumbnail($tab->ID, 'menu-24x24');
        $output .=   '<span>'.get_the_title($tab->ID).'</span>';
        $output .= empty($current) ? '</a>' : '';
        $output .= '</li>';
    }
    print $output;

?></ul>

Method 5

If I understand you correctly you want to get the ID’s of both the parent and any subsequent children pages. WordPress has functions that fetch the children of pages, such as this one:

https://codex.wordpress.org/Function_Reference/get_page_children

It is my understanding, that since you are performing a WP_Query, you are already fetching the ID’s of the parent pages, so all you would need to do is pass in the relevant ID to the the above function to get what you desire.

Note: I should point out that this function doesn’t do a DB query, so better performance wise as you are only making on query to the DB.

Method 6

You can use get_pages() with child_of parameter like below :

<?php
   get_pages( array(
     'child_of' => $parent_page_id;
   ) );
?>


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Comment