0

I have a hierarchical CPT event with taxonomy event_category. One of those categories is "Recap". When a user saves a post (could be draft, publish, or edit existing), if the event_category is Recap and the post has a parent, I want to set the slug to "recap" automatically.

The problem is, when wp_insert_post() fires (and all the hooks within it, like wp_insert_post_data and save_post, the taxonomy has not yet been updated, and the data sent to wp_insert_post() does not contain the taxonomy information. There is code in wp_insert_post() to handle taxonomy data if passed, ($postarr['tax_input']), but frustratingly WordPress does not use it. Instead, it seems to process the taxonomy separately. At the time wp_insert_post() runs, the taxonomy data will be equal to what it was the previous update (or empty if a new post).

I can hook into set_object_terms, but that does not trigger during a post update when the taxonomy hasn't changed, so it misses the scenario when an existing Recap is updated as a child of another Event.

Is there maybe a hook that fires when the update request comes in, and/or one when everything is finished?

2 Answers 2

0

I tried hooking in to rest_post_dispatch, and that seems to work for edits in the block editor. Only problem here is that the slug does not update in the editor, so the URL is wrong if the author clicks the link to view it after saving. A separate hook is needed to cover quick edit and bulk edit. Current working code:

/**
 * Default event recap slug
 * If has parent and is in category Recap, set slug to "recap".
 * Can't use `wp_insert_post_data` because taxonomy data isn't included, frustratingly.
 */
// Catch changes in block editor
add_filter( 'rest_post_dispatch', 'recap_slug_rest', 10, 3 );
function recap_slug_rest( $result, $server, $request ) {
    if ( 'PUT' == $request->get_method() ) {
        $post_id = $request->get_param( 'id' );
        $post = get_post( $post_id );
        if ( 'event' == $post->post_type && $post->post_parent ) {
            $terms = get_the_terms( $post_id, 'event_category' );
            foreach ( $terms as $term ) {
                if ( in_array( $term->slug, [ 'recap', 'recaps' ] ) ) {
                    wp_update_post( [ 'ID' => $post_id, 'post_name' => 'recap' ] );
                    break;
                }
            }
        }
    }
    return $result;
}
// Catch quick/bulk edit
add_action( 'save_post_event', 'default_recap_slug_save_post', 10, 3 );
function default_recap_slug_save_post( $post_ID, $post, $update ) {
    if ( defined( 'REST_REQUEST' ) ) return;
    if ( $post->post_parent ) {
        $terms = get_the_terms( $post, 'event_category' );
        foreach ( $terms as $term ) {
            if ( in_array( $term->slug, [ 'recap', 'recaps' ] ) ) {
                remove_action( 'save_post_event', 'default_recap_slug_save_post' );
                wp_update_post( [ 'ID' => $post->ID, 'post_name' => 'recap' ] );
                add_action( 'save_post_event', 'default_recap_slug_save_post', 10, 3 );
                break;
            }
        }
    }
}
0

Is there maybe a hook that fires when the update request comes in, and/or one when everything is finished?

Yes, there is. It's an action hook named wp_after_insert_post:

do_action( 'wp_after_insert_post', int $post_id, WP_Post $post, bool $update, null|WP_Post $post_before )

Fires once a post, its terms and meta data has been saved.

See the documentation and also New action wp_after_insert_post in WordPress 5.6. – Make WordPress Core.

So in response to your current answer, you could use just wp_after_insert_post instead of having to use rest_post_dispatch and save_post, and your code would work with both the classic editor and the block/Gutenberg editor.

Code I used for testing:

add_action( 'wp_after_insert_post', 'my_wp_after_insert_post' );
function my_wp_after_insert_post( $post_id ) {
    // In my actual code, I checked if the current (admin) page is edit.php, and
    // if so, I do nothing. I also run other checks, e.g. whether WordPress is
    // auto-saving the post, or is doing cron.

    // We call get post to retrieve the post's fresh data.
    $post = get_post( $post_id );

    // Do nothing if the post is invalid or the slug is already "recap", or if
    // the post doesn't have a parent or isn't in the "Recap" categories.
    if ( ! $post || 'recap' === $post->post_name ||
        $post->post_parent < 1 || 'event' !== $post->post_type ||
        ! has_term( [ 'recap', 'recaps' ], 'event_category', $post_id )
    ) {
        return;
    }

    // Unhook this function to avoid infinite loop.
    remove_action( 'wp_after_insert_post', __FUNCTION__ );

    wp_update_post( array( 'ID' => $post_id, 'post_name' => 'recap' ) );

    // Re-hook this function.
    add_action( 'wp_after_insert_post', __FUNCTION__ );
}

And as for this (in your answer):

Only problem here is that the slug does not update in the editor, so the URL is wrong if the author clicks the link to view it after saving.

Yes, and from what I found out via the core source, the REST API controller class (for Posts, Pages and CPTs) is unfortunately not using the programmatically-updated post slug, so as a workaround, you can use the rest_prepare_<post type> hook like so — to modify the response data, to ensure the post slug is the latest one:

add_filter( 'rest_prepare_event', 'my_rest_prepare_event', 10, 2 );
function my_rest_prepare_event( $response, $post ) {
    $data = $response->get_data();

    // In the REST API response, post_name is slug.
    $data['slug'] = get_post_field( 'post_name', $post->ID );

    $response->set_data( $data );

    return $response;
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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