set a CPT as parent of a page

The challenge we try to take on here is how to:

  • not remove or replace the current Page Attribute meta box

  • keep the hierarchical information in the drop-down.

We assume we’re on the single page edit screen, and want to add the event post type to the page parent drop-down.

The drop-down is displayed using the wp_dropdown_pages() function, that calls the get_pages() function. It only supports a single post type.

Here are two ways to do this:

Method #1 – Modify SQL

Here’s an experimental way, since it deals with SQL modifications.

Since there’s no obvious way to filter the generated SQL query in get_pages(), we can use the general query filter to our needs.

To make the dropdown more user friendly, when it contains titles from multiple post types, we use the list_pages filter, to prepend the post type info.

Example:

So instead of displaying the options like:

"(no parent)"
"About the Music Hall"
    "History"
    "Location"
"Concerts"
    "Of Monsters and Men"
    "Vienna Philharmonic Orchestra"
"Tickets"

we get:

"(no parent)"
"page - About the Music Hall"
    "page  - History" 
    "page  - Location"
"event - Concerts"
    "event - Of Monsters and Men"
    "event - Vienna Philharmonic Orchestra"
"page - Tickets"

Demo plugin:

Here’s the code snippet that could be placed in a plugin for testing:

/**
 * Add the hierarchical 'event' post type, to the 'page' parent drop-down.
 *
 * @link http://wordpress.stackexchange.com/a/204439/26350
 */ 
is_admin() && add_filter( 'page_attributes_dropdown_pages_args', function( $dropdown_args, $post )
{
     // Only do this for the 'page' post type on the single edit 'page' screen
     if( 'page' === get_current_screen()->id && 'page' === $post->post_type )
     {
        // Modify the options' titles
        add_filter( 'list_pages', 'wpse_add_post_type_info_in_options', 10, 2 );

        // Modify the get_pages() query
        add_filter( 'query', function( $sql )
        {
            // Only run this once
            if( ! did_action( 'wpse_change_cpt' ) )
            {
                do_action( 'wpse_change_cpt' );

                // Adjust the post type part of the query - add the 'event' post type
                $sql = str_replace( 
                    "post_type="page" ", 
                    "post_type IN ( 'event', 'page' ) ", 
                    $sql 
                );
            }
            return $sql;
        } );
    }
    return $dropdown_args;
}, 10, 2 );

where:

function wpse_add_post_type_info_in_options ( $title, $page )
{
    return $page->post_type . ' - ' . $title;
}

and a little bit of clean up:

add_filter( 'wp_dropdown_pages', function( $output )
{
    if( did_action( 'wpse_change_cpt' ) )
        remove_filter( 'list_pages', 'wpse_add_post_type_info_in_options', 10, 2 );

    return $output;
} );

Method #2 – Only use wp_dropdown_pages()

Here we letwp_dropdown_pages() run twice and then merge it into a single drop-down; once for the page post type and again for the event post type:

/**
 * Add the hierarchical 'event' post type, to the 'page' parent drop-down.
 *
 * @link http://wordpress.stackexchange.com/a/204439/26350
 */ 
is_admin() && add_filter( 'page_attributes_dropdown_pages_args', 'wpse_attributes', 99, 2 );

function wpse_attributes( $dropdown_args, $post )
{
    // Run this filter callback only once
    remove_filter( current_filter(), __FUNCTION__, 99 );

    // Only do this for the 'page' post type on the edit page screen
    if( 'page' === get_current_screen()->id && 'page' === $post->post_type )
    {
        // Modify the input arguments, for the 'event' drop-down
        $modified_args = $dropdown_args;
        $modified_args['post_type'] = 'page';
        $modified_args['show_option_no_change'] = __( '=== Select Events here below: ===' );            
        $modified_args['show_option_none'] = false;

        // Add the 'event' drop-down
        add_filter( 'wp_dropdown_pages', function( $output ) use ( $modified_args )
        {
            // Only run it once
            if( ! did_action( 'wpse_dropdown' ) )
            {
                do_action( 'wpse_dropdown' );

                // Create our second drop-down for events
                $output .= wp_dropdown_pages( $modified_args );

                // Adjust the output, so we only got a single drop-down
                $output = str_replace( 
                    [ "<select name="parent_id" id='parent_id'>", "</select>"],
                    '', 
                    $output 
                );
                $output = "<select name="parent_id" id='parent_id'>" . $output . "</select>";
            }
            return $output;
        } );
    }
    return $dropdown_args;
}

Here the two hierarchical drop-downs are separated by the empty === Select Events here below: === option.

Example:

"(no parent)"
"About the Music Hall"
    "History"
    "Location"
"Tickets"
"=== Select Events here below: ==="
"Concerts"
    "Of Monsters and Men"
    "Vienna Philharmonic Orchestra"

Adjusting the main query

Assume now that we create a page called Of Monsters And Men – Info with the omam-info slug and select the event Of Monsters And Men as the parent, with the omam slug.

Then the path would be

example.tld/omam/omam-info

but this gives a 404 error. The reason is that the get_page_path() check inside the \WP_Query class, for the main query, fails:

if ( '' != $qv['pagename'] ) {
    $this->queried_object = get_page_by_path($qv['pagename']);
    if ( !empty($this->queried_object) )
        $this->queried_object_id = (int) $this->queried_object->ID;
    else
        unset($this->queried_object);

This is because here get_page_by_path() only checks for the page and attachment post types, not the event post type. Unfortunately there’s no explicit filter to change that.

We could of course use the query filter, like we did above, but let’s try another workaround.

We can try to adjust the un-assigned properties queried_object_id and queried_object_id of the \WP_Query object with:

/**
 * Support for page slugs with any kind event parent hierarchy
 *
 * @link http://wordpress.stackexchange.com/a/204439/26350
 */
add_action( 'pre_get_posts', function( \WP_Query $q )
{
    if( 
            ! is_admin()               // Front-end only
        &&  $q->is_main_query()        // Target the main query
        &&  $q->get( 'pagename' )      // Check for pagename query variable
        &&  ! $q->get( 'post_type' )   // Target the 'page' post type
        && $page = get_page_by_path( $q->get( 'pagename' ), OBJECT, [ 'page', 'event', 'attachment' ] ) 
    ) {
        if( is_a( $page, '\WP_Post' ) )
        {                
            $q->queried_object_id = $page->ID;
            $q->queried_object = $page;
        }
    }
} );

This should also support any number of event parents, like the following hierarchy:

ecample.tld/event-grandparent/event-parent/event-child/page-slug

Note

For the parent drop-downs in the edit.php screen, we might use the quick_edit_dropdown_pages_args filter, instead of the page_attributes_dropdown_pages_args filter we used above.

Leave a Comment