WordPress hooks on post creation

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 );
...
 }

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.