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?