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

August 11, 2020

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….

27 Comments

  1. juzhax
    September 22, 2020 Reply

    Great, it works with $post->post_parent too

    • Edge
      September 22, 2020 Reply

      Brilliant. Thanks for sharing that it does!

  2. pooya
    June 6, 2020 Reply

    It worked perfectly for me, thank you Mr. Andy

  3. Ben
    May 5, 2020 Reply

    That is is super useful, still in 2020!! Thanks a lot Andy!

  4. ALi
    July 11, 2019 Reply

    thank you !!! you are a life saver.

    • EJ
      July 11, 2019 Reply

      I'm glad this was useful to you.

  5. Aussi
    November 26, 2018 Reply

    Great solution, this works really well, thank you. Do you have any insight into how a fake 'Featured Image', using an existing image file on the server, could be attached to the fake post? This would allow theme features such as social sharing images to work well.

    • Andy Keith
      November 26, 2018 Reply

      No problem :-)

      That would be a bit more complex but you could give it a go. You'd be best to add your image to the media library first, to give it an 'attachment ID'. All the WP image/media functions rely on this, so there's no way you could get it to load straight from a URL.

      You'd then need to fake the post's thumbnail ID. This is stored as in the post meta table under the key _thumbnail_id. If your 'fake' post ID is fixed, you could just add the meta data to the DB - e.g. add_post_meta( [fake post ID], '_thumbnail_id', [attachment ID] );. Alternatively, you could add it to the meta cache. Bear in mind that with both approaches, you won't be able to use a negative post ID (as I do in my example).

      • Aussi
        November 26, 2018 Reply

        Thanks for the fast reply! Hmm, interesting. I don't really want to add all the images to the media library - on my site there are over 4000 'fake' pages, each with an image and they are all built externally from the site using a separate process.

        I might take a different approach and instead of faking a full attachment and then injecting that into the Wordpress engine, I'll try altering the theme to pick up the appropriate image.

        • Andy Keith
          November 27, 2018

          Sounds like a good approach.

  6. aan
    October 11, 2018 Reply

    thanks mate :)
    but I got a problem on headers
    I got this in headers

    Expires:
    Wed, 11 Jan 1984 05:00:00 GMT
    Cache-Control:
    no-cache, must-revalidate, max-age=0

    • Andy Keith
      October 11, 2018 Reply

      That looks like the 'no cache' headers set by WordPress. Probably because it couldn't figure out what the 'page' was so it used the nocache headers. Check the send_headers() function.

      • Andy Keith
        October 11, 2018 Reply

        Not sure, but it might be best to filter on 'wp_headers' rather than use the 'send_headers' action.

        • aan
          October 12, 2018

          ok mate
          thank you very much for help :)

      • aan
        October 11, 2018 Reply

        thanks for the respond mate!
        I'm trying the following code, but the "Expires: Wed, 11 Jan 1984 05:00:00 GMT" and friend still appear. Really confusing hahahaha

        add_action( 'send_headers', 'cache_headers' );
        function cache_headers( $headers ) {
        unset($headers['Cache-Control']);
        return $headers;
        }

  7. Alexey
    June 19, 2018 Reply

    Thanks - this is interesting solution.

    And how can I add fake comments?

    Please.

    • Andy Keith
      June 19, 2018 Reply

      Hmm, I'm not sure how you would do that. If you figure it out, let me know!

  8. Collins Agbonghama
    June 10, 2018 Reply

    You're a life saver. This came in handy for a recent project I worked on.

  9. Jessica
    March 25, 2018 Reply

    This is really very informative post

  10. Lior
    August 22, 2017 Reply

    But how can you make it part of a category?

    • Andy Keith
      August 22, 2017 Reply

      Hi,

      I don't think you can. WP_Post doesn't contain any reference to the categories its part of, therefore you can't assign a category to the 'fake' WP_Post object. Categories (or more generically, terms) are associated with posts via their post (or object) ID, in the wp_term_relationships table.

  11. Bradford Knowlton
    June 5, 2017 Reply

    Thank you for everything you did, and shared. It works really well, except 1 small typo:

    You have a small typo,

    $wp_query->is_singlular = true;

    Should be:

    $wp_query->is_singular = true;

    I was very confused, frustrated and irrated why page titles didn't work. Took a lot of debugging to find. I wanted to share and save someone else the trouble in the future.

    Brad

    • Katie Keith
      June 6, 2017 Reply

      Hi Brad, thanks for letting me know - I have fixed this now. Sorry for the confusion!

  12. Carly Henry
    May 26, 2017 Reply

    You're a lifesaver.

    I was able to use this code to return a fake 'page' through the loop for a custom post type archive so that a shortcode would work on the custom post type archive page.

    Seriously, great work, thank you!

    • Andy Keith
      May 26, 2017 Reply

      No problem. Glad you found it useful :-)

Please share your thoughts...

Your email address will not be published.