How to create a ‘fake’ WordPress post on the fly

Recently, while making updates to our WooCommerce Private Store plugin, I needed to find a way to create a WordPress page in code which doesn’t exist in the database.

All WordPress posts (and pages) live in the database in the wp_posts table, with additional meta info in the wp_postmeta table. This works fine for all standard WordPress loops, but what if you need to create a fake or dummy post that doesn’t actually exist in the database?

Turns out it is possible, you just need to hack the main $wp_query and do a bit of extra jiggery-pokery. Read on…

Step 1 – Creating the fake post

First of all, you’ll need to create a dummy WP_Post object. Looking at the constructor for WP_Post, we’ll see it expects to receive an object:

// WP_Post constructor
public function __construct( $post ) {
  foreach ( get_object_vars( $post ) as $key => $value )
    $this->$key = $value;
}

So you need to create one! The following code creates a basic object (using stdClass) and then sets the required properties on the object:

$post_id = -99; // negative ID, to avoid clash with a valid post
$post = new stdClass();
$post->ID = $post_id;
$post->post_author = 1;
$post->post_date = current_time( 'mysql' );
$post->post_date_gmt = current_time( 'mysql', 1 );
$post->post_title = 'Some title or other';
$post->post_content = 'Whatever you want here. Maybe some cat pictures....';
$post->post_status = 'publish';
$post->comment_status = 'closed';
$post->ping_status = 'closed';
$post->post_name = 'fake-page-' . rand( 1, 99999 ); // append random number to avoid clash
$post->post_type = 'page';
$post->filter = 'raw'; // important

A few things to note here:

  • We’re using a negative post ID. This is to avoid clashes with any existing posts in the database. In the testing I’ve done, I haven’t found any problems doing this.
  • We’re creating a page ($post->post_type = 'page') in this example, but you could just as easily create a ‘post’ or any post type you want.
  • The filter property is important. Other examples I found online for creating fake post objects didn’t include this step. I encountered numerous problems without this. The reason being the get_post() function. This core function is used everywhere in WordPress. If you look at this function you’ll notice in the following lines:
    } elseif ( 'raw' == $post->filter ) {
      $_post = new WP_Post( $post );
    } else {
      $_post = WP_Post::get_instance( $post->ID );
    }

    Because we set filter to raw, when this function is called it will usually fall into this block and create a new WP_Post object. All fine and dandy. If you don’t set this, you instead go into the get_instance() block, which results in a call to the database for a post with ID -99! Yeah good look with that. Errors will abound.

Next, we need to create the WP_Post object. This is to ensure any instanceof checks return return true for WP_Post.

// Convert to WP_Post object
$wp_post = new WP_Post( $post );

Finally we need to add our post object to the cache. This ensures that any calls to the cache for our post ID will return a valid object and thus prevent any (error producing) calls to the database. This happens (for example) when calling get_post() with an ID rather than an object:

 // Add the fake post to the cache
 wp_cache_add( $post_id, $wp_post, 'posts' );

Step 2 – Overriding the WordPress query

Now we have our WP_Post, we can start injecting that into $wp_query. In this use case, I’m assuming you want to override the main WordPress query, but you may have different requirements. In any case, the steps below should help you get an understanding of what you need to do.

global $wp, $wp_query;

// Update the main query
$wp_query->post = $wp_post;
$wp_query->posts = array( $wp_post );
$wp_query->queried_object = $wp_post;
$wp_query->queried_object_id = $post_id;
$wp_query->found_posts = 1;
$wp_query->post_count = 1;
$wp_query->max_num_pages = 1; 
$wp_query->is_page = true;
$wp_query->is_singular = true; 
$wp_query->is_single = false; 
$wp_query->is_attachment = false;
$wp_query->is_archive = false; 
$wp_query->is_category = false;
$wp_query->is_tag = false; 
$wp_query->is_tax = false;
$wp_query->is_author = false;
$wp_query->is_date = false;
$wp_query->is_year = false;
$wp_query->is_month = false;
$wp_query->is_day = false;
$wp_query->is_time = false;
$wp_query->is_search = false;
$wp_query->is_feed = false;
$wp_query->is_comment_feed = false;
$wp_query->is_trackback = false;
$wp_query->is_home = false;
$wp_query->is_embed = false;
$wp_query->is_404 = false; 
$wp_query->is_paged = false;
$wp_query->is_admin = false; 
$wp_query->is_preview = false; 
$wp_query->is_robots = false; 
$wp_query->is_posts_page = false;
$wp_query->is_post_type_archive = false;

There’s a lot of setting to false here, but that’s just to ensure that everything in $wp_query is correct for the fake query we’re now dealing with.

Lastly, we need to update some global variables. During my testing, I found it was safest to explicitly set the wp_query global to the $GLOBALS array. In addition, you want to call WordPress’s register_globals() function to set various other globals including the global $post:

// Update globals
$GLOBALS['wp_query'] = $wp_query;
$wp->register_globals();

Step 3 – Putting it all together

Putting it all together, you should now how a fully functioning query which completely replaces the main query that would have been used to display the current page. A good place to put this code is on the template_redirect action, before WordPress decides which template to load to display the current page:

add_action( 'template_redirect', 'xyz_create_fake_query' );
function xyz_create_fake_query() {
  global $wp, $wp_query;

  // Create our fake post
  $post_id = -99;

  // rest of code from above...
  // ...
  
  $GLOBALS['wp_query'] = $wp_query;
  $wp->register_globals();
}

That’s it! Let me know in the comments how you get on, or if you have any improvements on this solution….