When you create a post in WordPress a bunch of actions happen and a lot of them allow hooking into. In this post I’ll examine the process of post creation in detail.
The function responsible for doing main stuff is called wp_insert_post().
And immediately there is something interesting about it. Namely when you just go to Dashboard and click [Posts]→[Add New] to just to get a form for a new record, WordPress already calls that function! So let’s look closely what it does.
wp_insert_post explained
So wp_insert_post is called presumably whenever any change to a post occurs. What if we want to catch some specific event? Like when post was just published. Or just saved as draft. How can we determine if the post was published first time or just updated?
Let’s look at the wp_insert_post function closely.
function wp_insert_post( $postarr, $wp_error = false ) { ... }
When it’s called first time for presenting new form it’s being given following array of parameters:
post_title: string = "Auto Draft"
post_type: string = post
post_status: string = auto-draft
It gets then a bunch of default params, the most interesting one is $previous_status = ‘new’ which we could hopefully use somehow.
function wp_insert_post( $postarr, $wp_error = false ) { global $wpdb; $user_id = get_current_user_id(); $defaults = array('post_status' => 'draft', 'post_type' => 'post', 'post_author' => $user_id, 'ping_status' => get_option('default_ping_status'), 'post_parent' => 0, 'menu_order' => 0, 'to_ping' => '', 'pinged' => '', 'post_password' => '', 'guid' => '', 'post_content_filtered' => '', 'post_excerpt' => '', 'import_id' => 0, 'post_content' => '', 'post_title' => ''); $postarr = wp_parse_args($postarr, $defaults); ... if ( ! empty( $ID ) ) { ... } else { $previous_status = 'new'; } }
Now the first hook to be called is the filter wp_insert_post_parent
but it’s used for pages so we don’t care so much about it.
function wp_insert_post( $postarr, $wp_error = false ) { ... // Check the post_parent to see if it will cause a hierarchy loop $post_parent = apply_filters( 'wp_insert_post_parent', $post_parent, $post_ID, compact( array_keys( $postarr ) ), $postarr ); ... }
After that wp_unique_post_slug() function is called which might trigger wp_unique_post_slug
filter and bunch of other related filters. Probably it’s related to generation of the permalink
function wp_insert_post( $postarr, $wp_error = false ) { ... $post_name = wp_unique_post_slug($post_name, $post_ID, $post_status, $post_type, $post_parent); ... }
The next filter called is wp_insert_post_data
. Despite its name it is called on post update too. It gets all the $data
about the post plus the original $postarr
. This two arrays contain almost the same information, I won’t go into details because I don’t understand why there are two arrays in the first place. At this moment both these arrays has the same $data['post_title'] == $postarr['post_title'] == 'Auto Draft'
. Also $postarr['ID'] == 0
indicating that we are in the process of creating post.
Now look closely. Up to this moment nothing has been written into db. This is your first chance to insert some XSRF valuable data into the post. Simply edit the $data
param.
function wp_insert_post( $postarr, $wp_error = false ) { ... // expected_slashed (everything!) $data = compact( array( 'post_author', 'post_date', 'post_date_gmt', 'post_content', 'post_content_filtered', 'post_title', 'post_excerpt', 'post_status', 'post_type', 'comment_status', 'ping_status', 'post_password', 'post_name', 'to_ping', 'pinged', 'post_modified', 'post_modified_gmt', 'post_parent', 'menu_order', 'guid' ) ); $data = apply_filters('wp_insert_post_data', $data, $postarr); $data = wp_unslash( $data ); ... }
By the way I still fail to understand this coding convention about when to put a space between a bracket and a param.
Depending on the calculated $update
variable WordPress decides whether to call UPDATE or INSERT sql. Normally INSERT should be called only once — the first time. So it happens only when you open the “Add New” form. Imagine we are now in this workflow.
function wp_insert_post( $postarr, $wp_error = false ) { ... if ( $update ) { do_action( 'pre_post_update', $post_ID, $data ); if ( false === $wpdb->update( $wpdb->posts, $data, $where ) ) { ... } } else { ... if ( false === $wpdb->insert( $wpdb->posts, $data ) ) { ... } } ... }
BTW, you may notice that right before updating the pre_post_update
action is called. This can be handy to distinguish the further calls.
Now post is inserted into db and $post_ID is generated. Even though you didn’t actually written anything!
Let’s go further. wp_set_post_categories(), wp_set_post_tags(), wp_set_post_terms() functions may be called which might trigger add_term_relationship
and added_term_relationship
actions. Also later set_object_terms
action.
$wpdb->update()
might be called for auto-drafts only to update the post_name
that is, the slug for permalink.
clean_post_cache() function is called which executes clean_post_cache
action and clean_page_cache
action.
another $wpdb->update()
might be called but only to set guid (also for permalink)
wp_transition_post_status() is called which does nothing but three action calls: transition_post_status
, {$old_status}_to_{$new_status}
and {$new_status}_{$post->post_type}
. More information is here:
function wp_insert_post( $postarr, $wp_error = false ) { ... wp_transition_post_status($data['post_status'], $previous_status, $post); ... }
If the post gets updated then edit_post
action is called and then post_updated
action. The difference is that post_updated
receives two params: $post_after
and $post_before
. $post_before
is the post after the very first $wpdb->update
call whereas $post_after
is the post after edit_post
action.
Finally save_post
and wp_insert_post
actions are called with the same parameters. These two actions are equivalent!
function wp_insert_post( $postarr, $wp_error = false ) { ... if ( $update ) { do_action('edit_post', $post_ID, $post); $post_after = get_post($post_ID); do_action( 'post_updated', $post_ID, $post_after, $post_before); } do_action( "save_post_{$post->post_type}", $post_ID, $post, $update ); do_action( 'save_post', $post_ID, $post, $update ); do_action( 'wp_insert_post', $post_ID, $post, $update ); ... }