There are many use cases for a virtual page. Let’s say showing the invoice or payment status page for a payment plugin in the front-end, without compromising active theme. I needed one recently to show payment statuses. Here is how I implemented it.
1. Hook “init” to catch the specific slug
init
class My_Virtual_Page {
public function __construct() {
add_action( 'init', array( $this, 'init' ) );
}
/**
* Hook to add the virtual page
*/
public function init() {
if ( get_option( 'permalink_structure' ) ) {
$param = trim( parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ), '/' );
} else {
parse_str( parse_url( $_SERVER['REQUEST_URI'], PHP_URL_QUERY ), $params );
$param = ( isset( $params['page_id'] ) ? $params['page_id'] : false );
}
if( $param == 'my-payment-page' ) {
//Enqueue any page specific styles and scripts here
add_filter( 'the_posts', array( $this, 'my_virtual_payment_page' ) );
}
}
}
Let’s say our virtual page is my-payment-page
. So, the expected URL is either mydomain.com/my-payment-page
or mydomain.com/?page_id=my-payment-page
.
If we find our target parameter inside the init
hook, we en-queue any required styles and scripts and add a filter for the content.
Note: The “slug” have to be unique to avoid conflict with any existing page.
2. Prepare the page object
Inside the filter, we prepare the content of the virtual page. Its pretty straight forward. We create a basic post object and fill the object with the custom values we want to display.
class My_Virtual_Page {
/*........*/
/**
* @param $posts
* The virtual page filter
*
* @return array
*/
function my_virtual_payment_page( $posts ) {
global $wp, $wp_query;
//Just double checking. Can be ignored.
if ( strcasecmp( $wp->request, 'my-payment-page' ) !== 0 ) {
return $posts;
}
$content = '<p>HTML Page content here!</p>';
$post = $this->my_virtual_post_object( 'my-payment-page', __( 'Virtual payment page', 'domain' ), $content );
}
/**
* @param $slug
* @param $title
* @param $content
*
* Generate the post object dynamically
*
* @return stdClass
*/
function my_virtual_post_object( $slug, $title, $content ) {
$post = new stdClass;
$post->ID = -1;
$post->post_author = 1;
$post->post_date = current_time( 'mysql' );
$post->post_date_gmt = current_time( 'mysql', 1 );
$post->post_content = $content;
$post->post_title = $title;
$post->post_excerpt = '';
$post->post_status = 'publish';
$post->comment_status = 'closed';
$post->ping_status = 'closed';
$post->post_password = '';
$post->post_name = $slug;
$post->to_ping = '';
$post->pinged = '';
$post->modified = $post->post_date;
$post->modified_gmt = $post->post_date_gmt;
$post->post_content_filtered = '';
$post->post_parent = 0;
$post->guid = get_home_url( 1, '/' . $slug );
$post->menu_order = 0;
$post->post_type = 'page';
$post->post_mime_type = '';
$post->comment_count = 0;
return $post;
}
}
3. Display the page
Finally we display the post by returning the post
object. Before doing that, we need to set the flags of $wp_query
to simulate that a post is found.
class My_Virtual_Page {
/*........*/
function my_virtual_payment_page( $posts ) {
/*.......*/
$post = $this->my_virtual_post_object( 'my-payment-page', __( 'Virtual payment page', 'domain' ), $content );
// set filter results
$posts = array( $post );
// reset wp_query properties to simulate a found page
$wp_query->is_page = true;
$wp_query->is_singular = true;
$wp_query->is_home = false;
$wp_query->is_archive = false;
$wp_query->is_category = false;
unset( $wp_query->query['error'] );
$wp_query->query_vars['error'] = '';
$wp_query->is_404 = false;
return ( $posts );
}
/*........*/
}
Putting it all together
class My_Virtual_Page {
public function __construct() {
add_action( 'init', array( $this, 'init' ) );
}
/**
* Hook to add the virtual page
*/
public function init() {
if ( get_option( 'permalink_structure' ) ) {
$param = trim( parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ), '/' );
} else {
parse_str( parse_url( $_SERVER['REQUEST_URI'], PHP_URL_QUERY ), $params );
$param = ( isset( $params['page_id'] ) ? $params['page_id'] : false );
}
if( $param == 'my-payment-page' ) {
//Enqueue any page specific styles and scripts here
add_filter( 'the_posts', array( $this, 'my_virtual_payment_page' ) );
}
}
/**
* @param $posts
* The virtual page filter
*
* @return array
*/
function my_virtual_payment_page( $posts ) {
global $wp, $wp_query;
//Just double checking. Can be ignored.
if ( strcasecmp( $wp->request, 'my-payment-page' ) !== 0 ) {
return $posts;
}
$content = '<p>HTML Page content here!</p>';
$post = $this->my_virtual_post_object( 'my-payment-page', __( 'Virtual payment page', 'domain' ), $content );
// set filter results
$posts = array( $post );
// reset wp_query properties to simulate a found page
$wp_query->is_page = true;
$wp_query->is_singular = true;
$wp_query->is_home = false;
$wp_query->is_archive = false;
$wp_query->is_category = false;
unset( $wp_query->query['error'] );
$wp_query->query_vars['error'] = '';
$wp_query->is_404 = false;
return ( $posts );
}
/**
* @param $slug
* @param $title
* @param $content
*
* Generate the post object dynamically
*
* @return stdClass
*/
function my_virtual_post_object( $slug, $title, $content ) {
$post = new stdClass;
$post->ID = -1;
$post->post_author = 1;
$post->post_date = current_time( 'mysql' );
$post->post_date_gmt = current_time( 'mysql', 1 );
$post->post_content = $content;
$post->post_title = $title;
$post->post_excerpt = '';
$post->post_status = 'publish';
$post->comment_status = 'closed';
$post->ping_status = 'closed';
$post->post_password = '';
$post->post_name = $slug;
$post->to_ping = '';
$post->pinged = '';
$post->modified = $post->post_date;
$post->modified_gmt = $post->post_date_gmt;
$post->post_content_filtered = '';
$post->post_parent = 0;
$post->guid = get_home_url( 1, '/' . $slug );
$post->menu_order = 0;
$post->post_type = 'page';
$post->post_mime_type = '';
$post->comment_count = 0;
return $post;
}
}
That’s it. Enjoy!
Leave a Reply