Show hierarchical relationship between to custom post types on admin list

The way WordPress works for hierarchical post types is that post parent should always be the same post type of children.

For that reason, in admin screens, the post type in edit.php is pretty hardcoded.

However, what WordPress do is to run a WP_Query where ‘post_type’ argument is the current one.

So, what you can do?

First of all, if you want to merge the 2 cpt admin screens, you can hide the admin screen for ‘quote’. It is very simple: when you register the post type, simply set ‘public’ argument to false:

$args = array(
   // all other args here
  'hierarchical' => TRUE
  'public' => FALSE
);
register_post_type( 'quote', $args );

Doing so, the quote admin screen is not shown, so you have only the ‘request’ one.

What you have to do is intercept the query and set 'post_type' argument to array containg both ‘request’ and ‘quote’.

The problem doing so is that the global ‘post_type’ variable will be set to that array, but WordPress expects ‘post_type’ a string ending up in some errors. To avoid that errors, we should find a hook to force the global 'post_type' to be the string ‘request’.

After a quick look at the file responsible to output the admin posts table: 'wp-admin/includes/class-wp-posts-list-table.php' I found that a good hook for the scope can be the filter hook 'edit_posts_per_page'.

So the code:

add_action( 'pre_get_posts', function( $query ) {
  if ( is_admin() ) { // do nothing if not is admin
    $scr = get_current_screen();
    // if in the right admin screen
    if ( $scr->base === 'edit' && $scr->post_type === 'request' ) {
      // force query to get both 'request' and 'quote' cpt
      $query->set( 'post_type', array( 'request', 'quote' ) );
      // force global $post_type to be="request" if is an array 
      add_filter( 'edit_posts_per_page', function( $perpage ) {
        global $post_type;
        if ( is_array( $post_type ) && in_array( 'request', $post_type ) ) {
          $post_type="request";
        }
        return $perpage; // we don't want to affect $perpage value 
      } );
    }
  }
  return $query;
} );