钩子文档

save_post

💡 云策文档标注

概述

save_post 是 WordPress 中一个核心的 Action Hook,在文章或页面被创建或更新时触发。它允许开发者在保存后执行自定义逻辑,如修改元数据、发送通知或避免无限循环。

关键要点

  • save_post 在文章保存后立即触发,适用于创建或更新操作,可通过 get_post($post_id) 访问文章对象。
  • 参数包括 $post_id(文章 ID)、$post(文章对象)和 $update(是否为更新操作)。
  • 从 WordPress 3.7 开始,引入了 save_post_{post_type} 钩子,用于特定文章类型,以减少不必要的回调触发。
  • 使用 wp_update_post 等函数时需注意避免无限循环,可通过临时移除钩子或使用全局变量快照来解决。
  • 用户贡献笔记提供了多个代码示例,包括处理修订、特定文章类型、元数据更新和术语同步等场景。

代码示例

/**
 * 使默认分类中的所有文章变为私有。
 *
 * @see 'save_post'
 *
 * @param int $post_id 正在保存的文章 ID。
 */
function set_private_categories( $post_id ) {
    // 如果是修订版,获取真实文章 ID。
    $parent_id = wp_is_post_revision( $post_id );

    if ( false !== $parent_id ) {
        $post_id = $parent_id;
    }

    // 从选项中获取默认分类 ID。
    $defaultcat = get_option( 'default_category' );

    // 检查此文章是否在默认分类中。
    if ( in_category( $defaultcat, $post_id ) ) {
        // 移除此函数以避免无限循环
        remove_action( 'save_post', 'set_private_categories' );

        // 更新文章,这会再次调用 save_post。
        wp_update_post( array( 'ID' => $post_id, 'post_status' => 'private' ) );

        // 重新添加此函数。
        add_action( 'save_post', 'set_private_categories' );
    }
}
add_action( 'save_post', 'set_private_categories' );

注意事项

  • save_post_{post_type} 在 save_post 之前触发,可能被插件覆盖,需根据需求选择钩子。
  • 更新术语时,save_post 可能在术语保存到数据库前触发,建议使用 rest_after_insert_{post_type} 确保数据准确性。
  • 避免在回调中直接调用触发 save_post 的函数,以防止无限循环。

📄 原文内容

Fires once a post has been saved.

Parameters

$post_idint
Post ID.
$postWP_Post
Post object.
$updatebool
Whether this is an existing post being updated.

More Information

save_post is an action triggered whenever a post or page is created or updated, which could be from an import, post/page edit form, xmlrpc, or post by email. The data for the post is stored in $_POST, $_GET or the global $post_data, depending on how the post was edited. For example, quick edits use $_POST.

Since this action is triggered right after the post has been saved, you can easily access this post object by using get_post($post_id);.

NOTE: As of WP 3.7, an alternative action has been introduced, which is called for specific post types: <a href="https://developer.wordpress.org/reference/hooks/save_post_post-post_type/">save_post_{post_type}</a>. Hooking to this action prevents your callback to be unnecessarily triggered.

Avoiding infinite loops

If you are calling a function such as wp_update_post that includes the save_post hook, your hooked function will create an infinite loop. To avoid this, unhook your function before calling the function you need, then re-hook it afterward.

<span style=", sans-serif">/**
</span><span style=", sans-serif">  * Makes all posts in the default category private.
</span><span style=", sans-serif">  *
</span><span style=", sans-serif">  * @see 'save_post'
</span><span style=", sans-serif">  *
</span><span style=", sans-serif">  * @param int $post_id The post being saved.
</span><span style=", sans-serif">  */
</span><span style=", sans-serif">function set_private_categories( $post_id ) {
</span><span style=", sans-serif">    // If this is a revision, get real post ID.
</span><span style=", sans-serif">    $parent_id = wp_is_post_revision( $post_id );</span>

<span style=", sans-serif">    if ( false !== $parent_id ) {
</span><span style=", sans-serif">        $post_id = $parent_id;
</span><span style=", sans-serif">    }

</span><span style=", sans-serif">    // Get default category ID from options.
</span><span style=", sans-serif">    $defaultcat = get_option( 'default_category' );

</span><span style=", sans-serif">    // Check if this post is in default category.
</span><span style=", sans-serif">    if ( in_category( $defaultcat, $post_id ) ) {
</span><span style=", sans-serif">        // unhook this function so it doesn't loop infinitely
</span><span style=", sans-serif">        remove_action( 'save_post', 'set_private_categories' );

</span><span style=", sans-serif">        // update the post, which calls save_post again.
</span><span style=", sans-serif">        wp_update_post( array( 'ID' => $post_id, 'post_status' => 'private' ) );

</span><span style=", sans-serif">        // re-hook this function.
</span><span style=", sans-serif">        add_action( 'save_post', 'set_private_categories' );
</span><span style=", sans-serif">    }
</span><span style=", sans-serif">}
</span><span style=", sans-serif">add_action( 'save_post', 'set_private_categories' );
</span><span style=", sans-serif">

Source

do_action( 'save_post', $post_id, $post, $update );

Changelog

Version Description
1.5.0 Introduced.

User Contributed Notes

  1. Skip to note 11 content

    When using WordPress 3.7 or later, it’s a good idea to use the save_post_{$post->post_type} hook when it makes sense to in order to reduce code and fire less hooks overall when posts are created and updated.

    Documentation can be found here: https://developer.wordpress.org/reference/hooks/save_post_post-post_type/

  2. Skip to note 12 content

    The save_post_{post_type} hook fires BEFORE the general save_post hook, meaning that save_post will override any meta updates made with save_post_{post_type}. Many plugins like ACF and Pods use the save post action hook, so if you are trying to update a meta field and you are using one of these plugins, you must use the save_post hook instead.

    // does not work
    function my_save_meta_function( $post_id, $post, $update )
    {
    	// fires but can be overridden by plugins, regardless of priority number
    	update_post_meta( $post_id, 'address', '123 Test St' );
    }
    add_action( 'save_post_event', 'my_save_meta_function', 99, 3 );
    
    
    // does work
    function my_save_meta_function( $post_id, $post, $update )
    {
    	if ( get_post_type( $post_id ) !== 'event' ) return;
    	update_post_meta( $post_id, 'address', '123 Test St' );
    }
    add_action( 'save_post', 'my_save_meta_function', 99, 3 );

  3. Skip to note 13 content

    Force a new post of have specific category term,

    add_action( 'save_post', 'set_post_default_category', 10,3 );
    
    function set_post_default_category( $post_id, $post, $update ) {
    	// Only want to set if this is a new post!
    	if ( $update ){
    		return;
    	}
    	
    	// Only set for post_type = post!
    	if ( 'post' !== $post->post_type ) {
    		return;
    	}
    	
    	// Get the default term using the slug, its more portable!
    	$term = get_term_by( 'slug', 'my-custom-term', 'category' );
    
    	wp_set_post_terms( $post_id, $term->term_id, 'category', true );
    }

  4. Skip to note 14 content

    Below is a basic example that will send an email every time a post or page is updated on your website.

    function my_project_updated_send_email( $post_id ) {
    
    	// If this is just a revision, don't send the email.
    	if ( wp_is_post_revision( $post_id ) ) {
    		return;
            }
    
    	$post_title = get_the_title( $post_id );
    	$post_url = get_permalink( $post_id );
    	$subject = 'A post has been updated';
    
    	$message = "A post has been updated on your website:nn";
    	$message .= $post_title . ": " . $post_url;
    
    	// Send email to admin.
    	wp_mail( 'admin@example.com', $subject, $message );
    }
    add_action( 'save_post', 'my_project_updated_send_email' );

  5. Skip to note 15 content

    The documentation provides a way to avoid an infinite loop by removing the hook and then adding it again after we have used a function such as `wp_update_post`.

    The problem about this is that other actions on the same hook may still trigger twice.

    I came up with the following alternative:

    /**
     * Prevent infinite loop.
     */
    //remove_action( 'save_post', array( self::class, 'save_meta_boxes' ) );
    
    global $wp_actions, $wp_filters, $wp_filter;
    
    $actions = $wp_actions;
    $filters = $wp_filters;
    $filter  = $wp_filter;
    
    remove_all_actions( 'save_post' );
    
    wp_update_post( $post );
    
    $wp_actions = $actions;
    $wp_filters = $filters;
    $wp_filter  = $filter;
    //add_action( 'save_post', array( self::class, 'save_meta_boxes' ) );

    By doing this we take a “snapshot” of all the registered hooks before we update the post. Then we remove all the actions for the hook, and once it has been updated, we restore the snapshots.

    You should also think about removing all `save_post_{$post->post-type}` actions and viceversa.

    It would be nice if there was a core function for this. `wp_update_post` already has a third optional parameter to prevent firing the after insert hooks. A fourth parameter could be added to prevent firing the `save_post` and `save_post_{$post->post-type}` hooks so that we don’t have to use workarounds to prevent the infinite loop issue.

  6. Skip to note 16 content

    To trigger for specific post type, assume we have a post type name ‘book’

    function wpdocs_book_meta( $post_id ) {
    	// Check the logged in user has permission to edit this post
    	if ( ! current_user_can( 'manage_options' ) ) {
    		return $post_id;
    	}
    
    	if ( isset( $_POST['website'] ) ) {
    		$website = esc_url_raw( $_POST['website'] );
    		update_post_meta( $post_id, 'website', $website );
    	}
    }
    add_action( 'save_post_book', 'wpdocs_book_meta' );

  7. Skip to note 17 content

    The $post_ID passed to the action is the ID of the revision while updating a post. To find the ID of the parent post, use wp_get_post_parent_id.

    function my_function_on_save_post( $post_id ) {
    
    	// Find parent post_id.
    	if ( $post_parent_id = wp_get_post_parent_id( $post_id ) ) {
    		$post_id = $post_parent_id;
    	}
    
    	// Do something.
    
    }
    add_action( 'save_post', 'my_function_on_save_post' );

  8. Skip to note 18 content

    I was trying to add a hook to review the current posts and terms in the database whenever a post was updated (through the regular editing interface, not direct calls to the API) but hit a problem.

    If a user updates only a post’s Terms (categories, tags), save_post is triggered before the new Terms are pushed to the database. (If the Term changes are saved with any other changes, the Terms are written to the DB before save_post is triggered.)

    Details:

    wp_insert_post() does look like it saves Terms before triggering the save_post action but the Term data it passes to (ultimately) wp_set_object_terms() is the OLD terms for the post. (Why resave the old data? Dunno.)

    After wp_insert_post() triggers save_post, an additional call to wp_set_object_terms() is made (from WP_REST_Posts_Controller) that has a the new Terms data in it. (I didn’t dig further to find out why the two calls for this case.)

    Workaround:

    If your hook needs the Terms to be accurate, attach it to rest_after_insert_(post|page|attachment) instead. This is triggered when ALL changes are stored, regardless of whether it is just the post content, just the terms, or both.

  9. Skip to note 19 content

    Here is my updated code which will check whether post title is exist or not before inserting into news post title.

    function wpdocs_save_post_callback( $post_id ) {
        $post = get_post( $post_id );
    
        if ( $post && 'post' === $post->post_type && isset( $_POST['post_title'] ) ) {
            $post_title = sanitize_text_field( $_POST['post_title'] );
    
            // Check if a post with the same title already exists in the "news" post type
            $existing_post = get_page_by_title( $post_title, OBJECT, 'news' );
    
            if ( ! $existing_post ) { // If no post with the same title exists
                $post_data = array(
                    'post_type'   => 'news',
                    'post_status' => 'publish',
                    'post_title'  => $post_title
                );
    
                wp_insert_post( $post_data );
            } else {
                // Post with the same title already exists
                // You can handle this case according to your requirement, e.g., display a message or update the existing post
            }
        }
    }
    add_action( 'save_post', 'wpdocs_save_post_callback' );

  10. Skip to note 20 content

    Here is a simple example which will save post on publish and also at the same time add the post in another post type also.

    function wpdocs_save_post_callback( $post_id ) {
    	$post = get_post( $post_id );
    
          	if ( $post && 'post' === $post->post_type && isset( $_POST['post_title'] ) ) {
    		$post_title = sanitize_text_field( $_POST['post_title'] );
    
    		$post_data = array(
    			'post_type' => 'news',
    			'post_status' => 'publish',
    			'post_title' => $post_title
    		);
    
    		wp_insert_post( $post_data );
    	}
    }
    add_action( 'save_post', 'wpdocs_save_post_callback' );