4

I am looking for a method to get the registered metaboxes for a specific post type (or post ID) when on a separate, unrelated post type. I am working on making a custom "editing" post type for a specific client need and want the ability to load the metaboxes without having them be registered to that 'editable' type.

Clarification:

What I am trying to accomplish is the actual display of the metaboxes from another post type. So if my "events" post type has a metabox with date, time, and location, I would want to show those same boxes on a separate post type without actually registering the metabox again.

2
  • I'm going to immediately guess that modifying the global $wp_meta_boxes would do the trick. Jun 18, 2015 at 19:20
  • @DanCameron sadly it does not. it doesn't recognize the metaboxes for other post types.
    – Norcross
    Jun 18, 2015 at 19:29

4 Answers 4

3

Wrong Assumptions

@Rarst or @Dan-Cameron answers may work, but assume that when you are viewing a post type editing page, the metaboxes for the other post type are all registered.

There are different case in which that won't happen:

  • If metaboxes are registered using "add_meta_boxes_{$post_type}" hook, instead of the more common, generic "add_meta_boxes" they won't be available in the admin page for the stealer post type
  • Even the generic post hook "add_meta_boxes" pass to hooking callbacks the current post type and the current post object as arguments. It means that registering callbacks may use those arguments in some if conditions, so is possible that we are in same situation of previous point

In short, unless you have control on how the metaboxes for some post type A are registered, you can't be sure they will be able in post editing page for some post type B.

In case you have that control, then @Rarst answer would do the trick.

In case you have not that control, the only way would be a bit tricky.

IMO your only chance, in that case, is to send an HTTP request (AJAX or via WP HTTP API) to the post edit page of the CPT you want to steal the boxes from. You have to add some request variable that will make the page return the boxes array (probaly serialized or JSON-enconded).

After that, you can merge the boxes returned with the boxes for current post, and you are done.

The point is that such code

  • is slow (because additional request)
  • is not trivial

So, if you have control on which are all the boxes registered for post a certain post type, is much simpler and faster just register those boxes again for the stealer post type.

If you can't be sure which are all the boxes for that post type, then the additional HTTP request is your only chance.

I'll post here a class, that implements that workflow (here as Gist for better readability):

class MetaboxStealer
{
    private static $stealing;

    private $post_type;
    private $boxes = [];

    /**
     * When the request contain special variable, this function will make it
     * return a serialized version of $wp_meta_boxes array and die.
     */
    public static function init()
    {
        add_filter('post_updated_messages', function ($messages) {
            if (MetaboxStealer::stealing()) {
                ob_start();
                return [];
            }
            return $messages;
        });

        add_action('do_meta_boxes', function () {
            if (MetaboxStealer::stealing()) {
                ob_end_clean();
                global $wp_meta_boxes;
                echo serialize($wp_meta_boxes);
                die();
            }
        });
    }

    /**
     * Checks that the request contain a special variable that will make request
     * return a serialized version of $wp_meta_boxes array and die.
     *
     * @return bool
     */
    public static function stealing()
    {
        if (is_null(self::$stealing)) {
            $screen = function_exists('get_current_screen') ? get_current_screen() : null;
            $stealing = filter_input(INPUT_GET, 'stealing-boxes', FILTER_SANITIZE_STRING);
            self::$stealing =
                $screen instanceof \WP_Screen
                && $stealing
                && wp_verify_nonce($stealing, $screen->post_type);
        }
        return self::$stealing;
    }

    /**
     * @param string $post_type Current post type
     */
    public function __construct($post_type)
    {
        $this->post_type = $post_type;
    }

    /**
     * Send a HTTP request to post edit page of a given CPT setting a special
     * variable that will make that page return serialized $wp_meta_boxes array.
     * After that, so obtained boxes are merged with the boxes for current post type.
     *
     * @param string $cpt CPT to steal metaboxes from
     */
    public function steal($cpt)
    {
        $vars = [
            'post_type'      => $cpt,
            'stealing-boxes' => wp_create_nonce($cpt),
        ];
        $url = add_query_arg($vars, admin_url('/post-new.php'));
        $cookies = [];
        foreach ($_COOKIE as $name => $value) {
            if ('PHPSESSID' !== strtoupper($name)) {
                $cookies[] = new \WP_Http_Cookie([
                    'name'  => $name,
                    'value' => $value,
                ]);
            }
        }
        $response = wp_remote_get($url, ['cookies' => $cookies]);
        if (! is_wp_error($response)) {
            $body = wp_remote_retrieve_body($response);
            if (is_serialized($body)) {
                $boxes = unserialize($body);
                $this->boxes = isset($boxes[$cpt]) ? $boxes[$cpt] : [];
                empty($this->boxes) or $this->merge();
            }
        }
    }

    /**
     * Merge metaboxes for current post type with boxes obtained with 
     * a HTTP request to another CPT post edit page.
     */
    private function merge()
    {
        global $wp_meta_boxes;
        isset($wp_meta_boxes[$this->post_type]) or $wp_meta_boxes[$this->post_type] = [];
        foreach ($this->boxes as $context => $priorities) {
            foreach ($priorities as $priority => $boxes) {
                if (! isset($wp_meta_boxes[$this->post_type][$context])) {
                    $wp_meta_boxes[$this->post_type][$context] = [];
                }
                if (! isset($wp_meta_boxes[$this->post_type][$context][$priority])) {
                    $wp_meta_boxes[$this->post_type][$context][$priority] = [];
                }
                $wp_meta_boxes[$this->post_type][$context][$priority] = array_merge(
                    $wp_meta_boxes[$this->post_type][$context][$priority],
                    $boxes
                );
            }
        }
    }
}

Notes:

  • the method merge() is highly derived from Rarst answer
  • I've used WP HTTP API to send additional request to keep all in one place, but an AJAX implementation would be nicer

How To Use

Pretty simple.

// init the class
add_action('admin_init', ['MetaboxStealer', 'init']);

// use the class to merge boxes for current CPT with boxes for another CPT
add_action('edit_form_after_editor', function ($post) {
    $stealer_cpt = 'stealer-cpt';
    $steal_from = 'events';
    if ($post->post_type === $stealer_cpt) {
        $stealer = new MetaboxStealer($post->post_type);
        $stealer->steal($steal_from);
        // note that you can steal from different CPTs
        // $stealer->steal($another_steal_from);
    }
});

The Saving Issue

No matter how do you manage to show boxes from one CPT in the post edit page of another CPT, is possible that the saving routine checks the post type before saving post meta in it.

In that case, metaboxes from the other CPT, even if showed, will not be saved and you'll probably need to write another saving routine, if you have no access on the original one.

1
  • this is the best answer for a question that doesn't really have one. for some reason I felt this was easier to accomplish but it appears I was wrong. thanks for outlining this.
    – Norcross
    Jun 19, 2015 at 3:36
0

Create an additional metabox (checkbox, I'm thinking) that provides the option to create an 'edit' post type from this post. For the sake of this code, we'll call it 'transfer_data'.

Change the value of $post_type to the post type of the original post that we will be retrieving the data from.

I've added the functionality to save the original post ID intoback a meta value for the 'edit' post type. In order to save the 'edit' metaboxes to the original post, you should be able to use that value and this function ( with update_post_meta() instead, of course) to reverse engineer another function to hook into.

function create_edit_page($data){
    $post_type = 'the_post_type_to_work_with';
    // Grab this post's ID
    $orig_id = $_POST['post_ID'];
    // Grab the value of the 'transfer_data' field
    $is_transfer_val = get_post_meta( $orig_id, 'transfer_data');
    if($data['post_type'] == $post_type && $is_transfer_val == TRUE && 
            isset($data['guid']) && strlen($data['guid'])>0 ){

        $post_id = wp_insert_post(
          array(
            'comment_status'  => 'closed',
            'ping_status'   => 'closed',
            'post_author'   => $data['post_author'],
            'post_name'   => $slug,
                'post_content'  =>  $data['post_content'],
            'post_title'    => $data['post_title'],
            'post_status'   => 'publish',
                // The custom post type 'editing'
            'post_type'   => 'editing'
          )
        );

        // create the meta fields
        $all_meta_boxes = get_post_meta( $orig_id );
        if(isset( $all_meta_boxes ) && is_array( $all_meta_boxes )){
            foreach($all_meta_boxes as $metakey => $metavalue){
                add_post_meta($post_id, $metakey, $metavalue);
            }
        }
        // add a meta field that points to original post (for editing purposes, etc.)
        add_post_meta($post_id, 'original_post_id', $orig_id);

        // If you want to redirect the user after saving use the filter below
        // add_filter('redirect_post_location', 'my_post_redirect_filter', '99');

        return $data;
    }
    return $data;
}
add_action( 'wp_insert_post', 'create_edit_page', '99' );
// Or, call BEFORE updating the database with below action
//add_action( 'wp_insert_post_data', 'create_edit_page', '99' );
1
  • not quite what I had in mind. the issue I'm trying to solve is the actual display of the meta boxes. Will update question to clarify.
    – Norcross
    Jun 18, 2015 at 19:16
0

I will make no claim this is reliable, but this is about how far I got:

add_action( 'add_meta_boxes', function () {
    global $wp_meta_boxes;

    foreach ( $wp_meta_boxes['steal-from'] as $context => $priorities ) {

        foreach ( $priorities as $priority => $boxes ) {

            if ( ! isset( $wp_meta_boxes['metabox-stealer'][ $context ] ) ) {
                $wp_meta_boxes['metabox-stealer'][ $context ] = [ ];
            }

            if ( ! isset( $wp_meta_boxes['metabox-stealer'][ $context ][ $priority ] ) ) {
                $wp_meta_boxes['metabox-stealer'][ $context ][ $priority ] = [ ];
            }

            $wp_meta_boxes['metabox-stealer'][ $context ][ $priority ] = array_merge(
                $wp_meta_boxes['metabox-stealer'][ $context ][ $priority ],
                $boxes
            );
        }
    }
}, 11 );

Where steal-from and metabox-stealer are post types to operate on.

1
  • 1
    hmm...no dice on this either. it appears as though the metaboxes in the global aren't available when you're on a separate post type
    – Norcross
    Jun 18, 2015 at 23:13
0

This will insert the metaboxes manually in close enough locations to where the meta boxes should go. The problem is that they'll be inserted before the standard mboxes for that post type.

I'm sure you could hack with it more if necessary but this at least gets the basics of what you need.

function monkey_advanced_meta_boxes() {
    $post = get_post( 2457 );
    do_meta_boxes( 'post_type_to_take_mboxes_from', 'normal', $post );
    do_meta_boxes( 'post_type_to_take_mboxes_from', 'advanced', $post );
}
add_action( 'edit_form_advanced', 'monkey_advanced_meta_boxes' );

function monkey_sidebar_meta_boxes() {
    $post = get_post( 2457 );
    do_meta_boxes( 'post_type_to_take_mboxes_from', 'side', $post );
}
add_action( 'submitpost_box', 'monkey_sidebar_meta_boxes' );

Of course you will need to check the screen->id and make sure you're adding the mboxes to the correct screen. This also won't register meta boxes the same so things like removing them from screen options won't work.


UPDATE: After thinking about this a bit it looks like this isn't as easy as I've described above. I'm assuming you'll want to have the meta boxes save the meta which you may have to do manually if the original save action is checking the post_type (which it should) and then there's the issue of the meta box not being compatible.

I figure there's gotta be a way around this like I originally mentioned with modifying the global variable. 😏

1
  • in this example, what is the post type of the ID used in the get_post portion?
    – Norcross
    Jun 18, 2015 at 23:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.