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. 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.
- We're creating an instance of stdClass (a generic 'empty' class in PHP) rather than using the
WP_Post
constructor. This is becauseWP_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. - 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="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 coreget_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 newWP_Post
object. If you don't set this, you instead go into theget_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
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.
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.
Great, it works with $post->post_parent too
Brilliant. Thanks for sharing that it does!
It worked perfectly for me, thank you Mr. Andy
Hi, Pooya. Thanks for letting us know! I'm glad to hear our article was helpful. Let me know if you have any questions about WooCommerce Private Store or any of our other awesome plugins for WordPress and WooCommerce via our dedicated Support Center. Cheers!
That is is super useful, still in 2020!! Thanks a lot Andy!
thank you !!! you are a life saver.
I'm glad this was useful to you.
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.
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).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.
Sounds like a good approach.
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
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.
Not sure, but it might be best to filter on 'wp_headers' rather than use the 'send_headers' action.
ok mate
thank you very much for help :)
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;
}
Thanks - this is interesting solution.
And how can I add fake comments?
Please.
Hmm, I'm not sure how you would do that. If you figure it out, let me know!
You're a life saver. This came in handy for a recent project I worked on.
No problem
This is really very informative post
But how can you make it part of a category?
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.
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
Hi Brad, thanks for letting me know - I have fixed this now. Sorry for the confusion!
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!
No problem. Glad you found it useful :-)