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

First we need to check current page slug to see if that is our target page inside the init hook.

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 swp_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_tyle             = '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 swp_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_tyle             = 'page';
        $post->post_mime_type        = '';
        $post->comment_count         = 0;

        return $post;
    }
}

That’s it. Enjoy!

Published by Ashiqur Rahman

Programmer and photography enthusiast.

Leave a comment

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