Skip to content

Instantly share code, notes, and snippets.

@n7studios
Last active March 9, 2024 10:47
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save n7studios/56fd05f19f5da26f19f6da0ccb57b144 to your computer and use it in GitHub Desktop.
Save n7studios/56fd05f19f5da26f19f6da0ccb57b144 to your computer and use it in GitHub Desktop.
Determine Post Publish / Update via Classic Editor, Gutenberg and REST API
<?php
/**
* Example class implementation to perform actions, such as sending a Post
* to a third party API or service, when the Post is published or updated through:
* - Classic Editor
* - Gutenberg
* - REST API
*
* @package Post_To_Social
* @author Tim Carr
* @version 1.0.0
*/
class Post_To_Social {
/**
* Constructor
*
* @since 1.0.0
*/
public function __construct() {
// Actions
add_action( 'wp_loaded', array( $this, 'register_publish_hooks' ), 1 );
}
/**
* Registers publish hooks against all public Post Types,
*
* @since 1.0.0
*/
public function register_publish_hooks() {
add_action( 'transition_post_status', array( $this, 'transition_post_status' ), 10, 3 );
}
/**
* Fired when a Post's status transitions.
*
* Called by WordPress when wp_insert_post() is called.
*
* As wp_insert_post() is called by WordPress and the REST API whenever creating or updating a Post,
* we can safely rely on this hook.
*
* @since 1.0.0
*
* @param string $new_status New Status
* @param string $old_status Old Status
* @param WP_Post $post Post
*/
public function transition_post_status( $new_status, $old_status, $post ) {
// Bail if the Post Type isn't public
// This prevents the rest of this routine running on e.g. ACF Free, when saving Fields (which results in Field loss)
$post_types = array( 'post', 'page' );
if ( ! in_array( $post->post_type, $post_types ) ) {
return;
}
// Bail if we're working on a draft or trashed item
if ( $new_status == 'auto-draft' || $new_status == 'draft' || $new_status == 'inherit' || $new_status == 'trash' ) {
return;
}
/**
* = REST API =
* If this is a REST API Request, we can't use the wp_insert_post action, because any metadata
* included in the REST API request is *not* included in the call to wp_insert_post().
*
* Instead, we must use a late REST API action that gives the REST API time to save metadata.
*
* Thankfully, the REST API supplies an action to do this: rest_after_insert_posttype, where posttype
* is the Post Type in question.
*
* Note that any meta being supplied in the REST API Request MUST be registered with WordPress using
* register_meta(). If you're using a third party plugin to register custom fields, you'll need to
* confirm it uses register_meta() as part of its process.
*
* = Gutenberg =
* If Gutenberg is being used on the given Post Type, two requests are sent:
* - a REST API request, comprising of Post Data and Metadata registered *in* Gutenberg,
* - a standard request, comprising of Post Metadata registered *outside* of Gutenberg (i.e. add_meta_box() data)
*
* If we're publishing a Post, the second request will be seen by transition_post_status() as an update, which
* isn't strictly true.
*
* Therefore, we set a meta flag on the first Gutenberg REST API request to defer acting on the Post until
* the second, standard request - at which point, all Post metadata will be available to the Plugin.
*
* = Classic Editor =
* Metadata is included in the call to wp_insert_post(), meaning that it's saved to the Post before we use it.
*/
// Flag to determine if the current Post is a Gutenberg Post
$is_gutenberg_post = $this->is_gutenberg_post( $post );
// If a previous request flagged that an 'update' request should be treated as a publish request (i.e.
// we're using Gutenberg and request to post.php was made after the REST API), do this now.
$needs_publishing = get_post_meta( $post->ID, '_needs_publishing', true );
if ( $needs_publishing ) {
// Run Publish Status Action now
delete_post_meta( $post->ID, '_needs_publishing' );
add_action( 'wp_insert_post', array( $this, 'wp_insert_post_publish' ), 999 );
// Don't need to do anything else, so exit
return;
}
// If a previous request flagged that an update request be deferred (i.e.
// we're using Gutenberg and request to post.php was made after the REST API), do this now.
$needs_updating = get_post_meta( $post->ID, '_needs_updating', true );
if ( $needs_updating ) {
// Run Publish Status Action now
delete_post_meta( $post->ID, '_needs_updating' );
add_action( 'wp_insert_post', array( $this, 'wp_insert_post_update' ), 999 );
// Don't need to do anything else, so exit
return;
}
// Publish
if ( $new_status == 'publish' && $new_status != $old_status ) {
/**
* Classic Editor
*/
if ( ! defined( 'REST_REQUEST' ) || ( defined( 'REST_REQUEST' ) && ! REST_REQUEST ) ) {
add_action( 'wp_insert_post', array( $this, 'wp_insert_post_publish' ), 999 );
// Don't need to do anything else, so exit
return;
}
/**
* Gutenberg Editor
* - Non-Gutenberg metaboxes are POSTed via a second, separate request to post.php, which appears
* as an 'update'. Define a meta key that we'll check on the separate request later.
*/
if ( $is_gutenberg_post ) {
update_post_meta( $post->ID, '_needs_publishing', 1 );
// Don't need to do anything else, so exit
return;
}
/**
* REST API
*/
add_action( 'rest_after_insert_' . $post->post_type, array( $this, 'rest_api_post_publish' ), 10, 2 );
// Don't need to do anything else, so exit
return;
}
// Update
if ( $new_status == 'publish' && $old_status == 'publish' ) {
/**
* Classic Editor
*/
if ( ! defined( 'REST_REQUEST' ) || ( defined( 'REST_REQUEST' ) && ! REST_REQUEST ) ) {
add_action( 'wp_insert_post', array( $this, 'wp_insert_post_update' ), 999 );
// Don't need to do anything else, so exit
return;
}
/**
* Gutenberg Editor
* - Non-Gutenberg metaboxes are POSTed via a second, separate request to post.php, which appears
* as an 'update'. Define a meta key that we'll check on the separate request later.
*/
if ( $is_gutenberg_post ) {
update_post_meta( $post->ID, '_needs_updating', 1 );
// Don't need to do anything else, so exit
return;
}
/**
* REST API
*/
add_action( 'rest_after_insert_' . $post->post_type, array( $this, 'rest_api_post_update' ), 10, 2 );
// Don't need to do anything else, so exit
return;
}
}
/**
* Helper function to determine if the Post is using the Gutenberg Editor.
*
* @since 1.0.0
*
* @param WP_Post $post Post
* @return bool Post uses Gutenberg Editor
*/
private function is_gutenberg_post( $post ) {
// This will fail if a Post is created or updated with no content and only a title.
if ( strpos( $post->post_content, '<!-- wp:' ) === false ) {
return false;
}
return true;
}
/**
* Called when a Post has been Published via the REST API
*
* @since 1.0.0
*
* @param WP_Post $post Post
* @param WP_REST_Request $request Request Object
*/
public function rest_api_post_publish( $post, $request ) {
$this->wp_insert_post_publish( $post->ID );
}
/**
* Called when a Post has been Published via the REST API
*
* @since 1.0.0
*
* @param WP_Post $post Post
* @param WP_REST_Request $request Request Object
*/
public function rest_api_post_update( $post, $request ) {
$this->wp_insert_post_update( $post->ID );
}
/**
* Called when a Post has been Published
*
* @since 1.0.0
*
* @param int $post_id Post ID
*/
public function wp_insert_post_publish( $post_id ) {
// Call main function
$this->send( $post_id, 'publish' );
}
/**
* Called when a Post has been Updated
*
* @since 1.0.0
*
* @param int $post_id Post ID
*/
public function wp_insert_post_update( $post_id ) {
// Call main function
$this->send( $post_id, 'update' );
}
/**
* Main function. Called when any Page, Post or CPT is published or updated
*
* @since 1.0.0
*
* @param int $post_id Post ID
* @param string $action Action (publish|update)
* @return mixed WP_Error | API Results array
*/
public function send( $post_id, $action ) {
// Get Post
global $post;
$post = get_post( $post_id );
if ( ! $post ) {
return new WP_Error( 'no_post', sprintf( __( 'No WordPress Post could be found for Post ID %s' ), $post_id ) );
}
// @TODO Save any metadata that your Plugin expects now - such as post-specific settings your Plugin may offer via add_meta_box() calls
update_post_meta( $post_id, 'your-key', sanitize_text_field( $_POST['your-key'] ) );
// @TODO Add your code here to send your Post to whichever API / third party service
}
}
@jackmcconnell
Copy link

Thank you for this. This is really great and working well. One thing I've noticed though is that send() is still being triggered on revisions and other post types even though the post_type check (line 57) should prevent it from being run in those circumstances. Do you know why this might be?

@jackmcconnell
Copy link

Thank you for this. This is really great and working well. One thing I've noticed though is that send() is still being triggered on revisions and other post types even though the post_type check (line 57) should prevent it from being run in those circumstances. Do you know why this might be?

I was able to fix this by adding another check against the post type in wp_insert_post_publish(). Worth noting that _needs_publishing isn't set when creating a new post in Gutenberg as no draft is created. It's only set if you've saved a draft first, and then published the post.

@marijnbent
Copy link

marijnbent commented Feb 1, 2022

If anyone reads this in 2022; I ended up adding a nonce to the metabox for the classic editor. On the save_post action, I check if this nonce exists to save the metabox value. Otherwise, I ignore it and handle it in JS for Gutenberg metaboxes.

Edited example:

//add_action('add_meta_boxes', [$this, 'addMetaboxToClassicEditor'])
    public function addMetaboxToClassicEditor()
    {
            add_meta_box(
                'MyId',
                'MyTitle',
                [$this, 'myHtml'],
                'your-post-type',
                'side',
                'default',
                [
                    '__back_compat_meta_box' => true, //removes the metabox from the block editor.
                ]
            );
    }

//add_action('save_post', [$this, 'myNonceKey'])
    public function saveClassicMetaboxPostMeta($postId)
    {
        if (array_key_exists('myNonceKey', $_POST)) {
            if (wp_verify_nonce($_POST['myNonceKey'], 'save_post')) {
                update_post_meta(
                    $postId,
                    $this->metaKey,
                    array_key_exists($this->metaKey, $_POST)
                );
            }
        }
    }

    public function myHtml($post)
    {        
        wp_nonce_field('save_post', 'myNonceKey');
        ?>
        <input type="checkbox" id="<?php echo $this->metaKey; ?>" name="<?php echo $this->metaKey; ?>"
               value="1" <?php echo boolval($value) ? 'checked' : ''; ?>>
        <label for="<?php echo $this->metaKey; ?>"My Label</label>
        <?php
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment