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:

  1. We're using a negative post ID. This is to avoid clashes with any existing posts in the database. Although our post won't be saved to the database, it's possible it will form part of a loop with other posts which are, so we want to avoid a clash. In the testing I've done I haven't found any problems with this approach.
  2. We're creating an instance of stdClass (a generic 'empty' class in PHP) rather than using the WP_Post constructor. This is because WP_Post requires an object passed to it, which it then uses to set all of its properties. So we create a basic object here to pass to the constructor later.
  3. 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.
  4. The filter="raw" step is important. Other examples I found for creating fake post objects didn't include this step. I encountered numerous problems without this. The reason being the core get_post() function. This function is used everywhere in WordPress. If you look at this you'll notice in the following code:
    } 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 fall into this block and create a new WP_Post object. If you don't set this, you instead go into the get_instance() block, which results in a call to the database (or the cache) for a post with ID -99. That obviously isn't going to work, so we need to ensure the constructor method is used.

Next, we need to create the WP_Post object. This is to ensure any instanceof checks return return true for WP_Post. (See note 3 above).

// 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', 'spoof_main_query' );
function spoof_main_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....

31 Comments

  1. sorry i dont understand on how to putting it together , anyone can help me ?

    • Hi, Paddy. Thanks for your comment and sorry to hear you're having difficulty. If you're unfamiliar with this and don't have a developer who can do it for you, then I recommend that you post a job on Codeable where their pre-approved WordPress experts will send you a quote. We have partnered with them to provide plugin customization for our customers. I hope this helps point you in the right direction. Best regards.

  2. Had been trying to figure out how to insert custom search results into the standard WordPress search. This was the piece I was missing. Many thanks!

    • Wonderful! I'm glad to hear that you were able to find what you are looking for.

Please share your thoughts...

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