WordPress 4.4+ : How to revision CPT + metadata

In WordPress 4.4+, how do you enable revisions for Custom Post Types along with their metadata? How can you get this information to appear in the diff view?

I’ve attempted to implement what was described in the following blog posts, but the result was my implementation failing silently:

http://carolandrews.co.uk/saving-revisions-for-custom-post-type-meta-data/
https://johnblackbourn.com/post-meta-revisions-wordpress

The WP-Post-Meta-Revisions plugin doesn’t handle the revision screen case, and seems to only partially work? It is listed as being compatible up to: 4.3.3, so I can’t be too surprised I guess. The related WordPress ticket (20564) has also not been updated in 6 months.

Have all of the above solutions bit-rotted, or is it an error on my part?

Below is my attempt:


My custom post types are registered with supports => array('revisions') and these work as expected for the native WordPress fields.

I’m using an Object Oriented approach to my plugin/theme development and have an abstract class representing my Custom Post Types. Each subclass represents a specific CPT.

The relevant portions are as follows:

abstract class Foo_Post {
//...

    public function __construct(){
        $post_type = $this->get_post_type();
        add_action('init', array($this,'init'));
        //...
        add_filter('_wp_post_revision_fields', array($this, 'extend_revision_screen_keys'),10,1);
        add_action('wp_restore_post_revision', array('restore_revision', 10, 2 ) );
        add_action('admin_head', array($this,'render_revision_fields') );
    }

    public function init() {
        $post_type = $this->get_post_type();
        //...
        add_action("save_post_$post_type", array($this,'save_post'), 10, 2);
    }

    public function extend_revision_screen_keys($fields) {
        foreach($this->fields() as $field_name => $field_value) {
            if (!isset($field_value['field_type']) || Foo_Post::is_native_field($field_value))
                continue;
            $fields[$field_name] = $field_name;
        }
        return $fields;
    }

    public function render_revision_fields() {
        foreach($this->fields() as $field_name=>$field_value){
            if (!isset($field_value['field_type']) || Foo_Post::is_native_field($field_value))
                continue;
            add_filter( '_wp_post_revision_field_'.$field_name, array($this,'render_revision_field'), 10, 4 );
        }
    }

    public function render_revision_field($value, $field_name, $post, $context) {
        foreach($this->fields() as $field_key => $field_value) {
            if (!isset($field_value['field_type']) || Foo_Post::is_native_field($field_value) || $field_key != $field_name)
                continue;

            switch($field_value['field_type']) {
                case Foo_FieldType::checkbox :
                    $value = get_post_meta(get_the_ID(),$field_key,true);
                break;
                case Foo_FieldType::date :
                    $value = get_post_meta(get_the_ID(),$field_key,true);
                break;
                case Foo_FieldType::email :
                    $value = get_post_meta(get_the_ID(),$field_key,true);
                break;
                case Foo_FieldType::number :
                    $value = get_post_meta(get_the_ID(),$field_key,true);
                break;
                case Foo_FieldType::multi_text :
                    $value = get_post_meta(get_the_ID(),$field_key,true);
                break;
                //native types don't need to be rendered as it is managed by wordpress
                case Foo_FieldType::rel_many :
                    $value = get_post_meta(get_the_ID(),$field_key,true);
                    $value = join(',',$value);
                break;
                case Foo_FieldType::rel_single :
                    $value = get_post_meta(get_the_ID(),$field_key,true);
                    $value = join(',',$value);
                break;
                case Foo_FieldType::rich_text :
                    $value = get_post_meta(get_the_ID(),$field_key,true);
                break;
                case Foo_FieldType::select :
                    $value = get_post_meta(get_the_ID(),$field_key,true);
                    $value = join(',',$value);
                break;
                case Foo_FieldType::single_text :
                    $value = get_post_meta(get_the_ID(),$field_key,true);
                break;
                case Foo_FieldType::tel :
                    $value = get_post_meta(get_the_ID(),$field_key,true);
                break;
                case Foo_FieldType::url :
                    $value = get_post_meta(get_the_ID(),$field_key,true);
                break;
            }
        }
        return $value;
    }

    public function restore_revision($post_id, $revision_id){
        $revision = get_post( $revision_id );
        $meta     = get_post_meta( $post_id );

        foreach($meta as $field_key => $field_value) {
            //only want the metadata fields I've defined. (prefixed with _foo_)
            $pos = strpos($field_key,'_foo_');
            if($pos !== false && $pos == 0) {
                $meta = get_metadata( 'post', $revision->ID, $field_key, true );
                if ( false !== $meta )
                    update_post_meta( $post_id, $field_key, $meta );
                //TODO: Why the following?
                //else
                //    delete_post_meta( $post_id, 'my_meta' );
            }
        }
    }

    public function save_post($post_id, $post) {
        if(!$this->can_save($post_id))
            return;

        $parent_id = wp_is_post_revision($post_id);
        $revision  = $parent_id ? get_post( $parent_id ) : null;

        foreach($this->fields() as $field_key => $field_value) {
            $value = NULL;
            switch($field_value['field_type']) {
                case Foo_FieldType::checkbox :
                    $value = isset($_POST[$field_key]) ? ($_POST[$field_key] == 'true' ? 'checked' : '') : false;
                break;
                case Foo_FieldType::date :
                    $value = isset($_POST[$field_key]) ? trim($_POST[$field_key]) : date('Y-m-d H:i:s');
                break;
                case Foo_FieldType::email :
                    $value = isset($_POST[$field_key]) ? trim($_POST[$field_key]) : '';
                break;
                case Foo_FieldType::multi_text :
                    $value = isset($_POST[$field_key]) ? trim($_POST[$field_key]) : '';
                break;
                case Foo_FieldType::number :
                    $value = isset($_POST[$field_key]) ? trim($_POST[$field_key]) : '0';
                break;
                //skipping native type as it is managed by wordpress
                case Foo_FieldType::rel_many :
                    $value = isset($_POST[$field_key]) ? array_map('intval', (array) $_POST[$field_key]) : array();
                break;
                case Foo_FieldType::rel_single :
                    $value = isset($_POST[$field_key]) ? $_POST[$field_key] : -1;
                break;
                case Foo_FieldType::rich_text :
                    $value = isset($_POST[$field_key]) ? trim($_POST[$field_key]) : '';
                break;
                case Foo_FieldType::select :
                    $value = isset($_POST[$field_key]) ? $_POST[$field_key] : '';
                break;
                case Foo_FieldType::single_text :
                    $value = isset($_POST[$field_key]) ? trim($_POST[$field_key]) : '';
                break;
                case Foo_FieldType::tel :
                    $value = isset($_POST[$field_key]) ? trim($_POST[$field_key]) : '';
                break;
                case Foo_FieldType::url :
                    $value = isset($_POST[$field_key]) ? trim($_POST[$field_key]) : '';
                break;
            }

            if(!is_null($value))
                $this->save_post_field_value($post_id, $revision, $field_key, $value);
        }
    }

    //...
}

Is there something obvious that I’ve overlooked, or is there finally an official approach to implementing such a thing? The documentation on this seems sparse at best. Any feedback is of course appreciated.


Edit

It looks like add_filter( "_wp_post_revision_field_$field_name", array($this,'render_revision_field'), 10, 4 ); fails to register. so when wodpress’s includes/revision.php attempts to apply it, it doesn’t exist.

It also looks like add_filter('_wp_post_revision_fields', array($this, 'extend_revision_screen_keys'), 10, 1); also doesn’t get added.

My class above is included and instantiated in the plugin’s root index.php file.

Perhaps the issue is the order of events?


Edit 2 (2016-03-31)

To followup on @ialocin’s proof of concept:

Regarding the failing silently issue, it looks like the problem was that the suggestion of the first link was incorrect. It added the filter render_revision_fields AFTER the revision.php file is processed.

I’ve moved that registration to admin_init and it looks to be picked up properly now.

global $revision fix: Acknowledged.

_wp_post_revision_field_foo of 3 parameters vs. 4 : Acknowledged

get_the_ID() : Good catch. A casualty of copy-paste

What I don’t understand now is the save/restore justification. Before when I was wanting to save meta_data on a post I would do the following:

update_post_meta($post_id,$field_key,$field_value)

IIUC: Now with the revision version, the desired post is the parent of the revision, but what if wp_is_post_revision( $post_id ) fails? what does that mean? Does that mean there is not currently a revision and I should update $post in the missing else branch of your example? That wouldn’t make a new revision automatically would it?

Why would the add_metadata/get_metadata be associated with ‘post’ instead of the CPT value (say ‘product’ for instance)? Or should it in this case? Are revisions always stored as ‘post’ type and just associated with the parent of the desired CPT?

1
1

I’m not one-hundred percent sure, what is failing for you, but I can tell you, generally it should work. The proof is following under thirdly. Before I have some remarks for you:

Firstly, my guess would be, that it indeed isn’t failing silently, but while doing an AJAX process. So make use of a tool like FireBug to take a closer look at that.

Secondly, I’m pretty sure get_the_ID() – as used in your code – will get you the parent posts ID, which certainly isn’t what you want. If I’m seeing it correctly, then you can replace all occurrences with $post->ID – so that is easy enough for a change.

Thirdly, as you are more or less following the »John Blackbourn«-approach, I took his example code as plugin to take a closer look myself. Less cleanup work to do, in comparison to working with your code. But anyway, below you see the code with the small necessary changes, see comments for details, to make it work.

/*
Plugin Name: Post Meta Revisions
Description: Revisions for the 'foo' post meta field
Version:     http://wordpress.stackexchange.com/questions/221946
Author:      John Blackbourn
Plugin URI:  http://lud.icro.us/post-meta-revisions-wordpress
*/

function pmr_fields( $fields ) {
    $fields['foo'] = 'Foo';
    return $fields;
}

// global $revision doesn't work, using third parameter $post instead
function pmr_field( $value, $field, $post ) {
    return get_metadata( 'post', $post->ID, $field, true );
}

function pmr_restore_revision( $post_id, $revision_id ) {
    $post     = get_post( $post_id );
    $revision = get_post( $revision_id );
    $meta     = get_metadata( 'post', $revision->ID, 'foo', true );

    if ( false === $meta )
        delete_post_meta( $post_id, 'foo' );
    else
        update_post_meta( $post_id, 'foo', $meta );
}

function pmr_save_post( $post_id, $post ) {
    if ( $parent_id = wp_is_post_revision( $post_id ) ) {
        $parent = get_post( $parent_id );
        $meta = get_post_meta( $parent->ID, 'foo', true );

        if ( false !== $meta )
            add_metadata( 'post', $post_id, 'foo', $meta );
    }
}

// we are using three parameters
add_filter( '_wp_post_revision_field_foo', 'pmr_field', 10, 3 );
add_action( 'save_post',                   'pmr_save_post', 10, 2 );
add_action( 'wp_restore_post_revision',    'pmr_restore_revision', 10, 2 );
add_filter( '_wp_post_revision_fields',    'pmr_fields' );

After giving it a quick test, it seems to work quite well.

Leave a Comment