Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Wordpress plugin generating virtual pages and using theme template

I have a need to be able to generate fake/virtual/dynamic pages based on a url like http://www.mycinema.com/wpcinema/movie/MOVIEID to be able to display movies for cinemas with info on the movie and live session feed information.

After spending many hours researching, there doesn't seem to be much stuff written on how to do virtual pages in WordPress, so I will be writing up my experiences after getting this resolved!

So far, the current plan is to use the two filters - template_redirect to set the template to the current plugin's page.php template, and the_content to insert the content. The idea is to use the theme's template so the pages theme in well with the site.

(I got this approach from this excellent 2012 page from Xavi Esteve).

I have two problems:

  1. What is the best, most bullet proof, way to do this? Am I using the wrong approach? My thinking was that using the current theme's template was likely to provide the best current fit for the style of the website.

  2. TwentyTwelve does not appear to be calling the the_content filter in the context I'm using it. I suspect I'm doing something wrong, but cannot find the problem. This is probably closely related to question 1. TwentyTwelve definitely calls the_content for a normal page, and even an early add_filter() doesn't trigger in my code.

I discovered get_template_part() yesterday and wondered if I should be using that instead of manually looking in the child folder then the parent and running an include.

I wouldn't be asking, but I'm at my wit's end having googled extensively, possibly for the wrong search terms.

I've considered custom post types, but there are various complexities around this (including content that may change every few minutes) which means a dynamically generated page works much better.

This is an excerpt from the code I've written to explain the problem further:

add_action('parse_request', array(&$this, 'vm_parse_request'));

function vm_parse_request( &$wp )
{
    global $wp;
    if (empty($wp->query_vars['pagename']))
        return; // page isn't permalink

    $p = $wp->query_vars['pagename'];

    if (! preg_match("#wp-cinema/movie/([^/]+)#", $p, $m))
        return;

    // setup hooks and filters to generate virtual movie page
    add_action('template_redirect', array(&$this, 'vm_template_redir'));
    add_filter('the_content', array(&$this, 'vm_the_content'));
}

function vm_template_redir()
{
    // Reset currrently set 404 flag as this is a plugin-generated page
    global $wp_query;
    $wp_query->is_404 = false;

    $template = 'page.php';

    include(STYLESHEETPATH."/page.php"); // child
    // parent case left out for brevity

    exit;
}


function vm_the_content($content)
{
    return "my new content (actually generated dynamically)";
}

This is going to be an increasingly common thing in WordPress - can anyone offer suggestions or help? Anything is much appreciated.

like image 688
Brian C Avatar asked Jul 31 '13 02:07

Brian C


1 Answers

(Update at foot of article including link to improved working code gist)

I wanted to post an answer to this as it seems that NONE of the queries about WordPress virtual pages on here had answers! And there was a lot of blood involved in getting the answer to this one, testing it and ensuring it worked well. Hopefully this will save a few others the pain I went through ...

It turns out that in 2013 with WordPress 3.5.2+ (now 3.6 as of a week ago) the solution from Xavi Esteve mentioned above no longer works, as WordPress has evolved, dangit.

Using the template_redirect method above by itself, the problem is that as far as WordPress is concerned, there is no page/post content and thus many themes will not call the_content(), thus my code on the the_content filter never gets called.

The best solution nowadays seems to be to hook into the 'the_posts' filter and return a pseudo-page, however that in itself has no themeing attached to it.

The solution to the lack of theme was to hybridize this with part of Xavi Esteve's approach to allow me to change the template being used to generate the page.

This approach should work immediately for most, if not all, WordPress themes which was my goal and it's working really well with the themes I have tested so far.

I used the approach documented by Dave Jesch on this page (there are other versions of this, but Dave is the only one who explained it carefully, thanks Dave!): http://davejesch.com/wordpress/wordpress-tech/creating-virtual-pages-in-wordpress/

I also went through lots of pain here with the Wordpress comments section appearing at the bottom of the page in some themes. The solution for this will appear in the file linked above and is probably out of scope for this specific solution.

Also, to prevent a warning with WordPress 3.5.2+, I had to also add a post member:

 $post->ancestors = array();

This solution is used in the wp-cinema WordPress plugin (file views.php if you want to grab some working code, should be checked in in the next few weeks). If there are problems with the approach, I'll be keeping that file current as it's part of a larger project.

The full working solution is below. This is excerpted from a much longer piece of code which also prevents comments from appearing etc (see link provided above). The code:

add_action('parse_request', 'vm_parse_request');


// Check page requests for Virtual movie pages
// If we have one, generate 'movie details' Virtual page.
// ...
//
function vm_parse_request(&$wp)
{
    if (empty($wp->query_vars['pagename']))
       return; // page isn't permalink

    $p = $wp->query_vars['pagename'];

    if (! preg_match("#wp-cinema/movie/([^/]+)#", $p, $m))
       return;

    // setup hooks and filters to generate virtual movie page
    add_action('template_redirect', 'vm_template_redir');

    $this->vm_body = "page body text";

    add_filter('the_posts', 'vm_createdummypost');

    // now that we know it's my page,
    // prevent shortcode content from having spurious <p> and <br> added
    remove_filter('the_content', 'wpautop');
}


// Setup a dummy post/page 
// From the WP view, a post == a page
//
function vm_createdummypost($posts)
{
    // have to create a dummy post as otherwise many templates
    // don't call the_content filter
    global $wp, $wp_query;

    //create a fake post intance
    $p = new stdClass;
    // fill $p with everything a page in the database would have
    $p->ID = -1;
    $p->post_author = 1;
    $p->post_date = current_time('mysql');
    $p->post_date_gmt =  current_time('mysql', $gmt = 1);
    $p->post_content = $this->vm_body;
    $p->post_title = $this->vm_title;
    $p->post_excerpt = '';
    $p->post_status = 'publish';
    $p->ping_status = 'closed';
    $p->post_password = '';
    $p->post_name = 'movie_details'; // slug
    $p->to_ping = '';
    $p->pinged = '';
    $p->modified = $p->post_date;
    $p->modified_gmt = $p->post_date_gmt;
    $p->post_content_filtered = '';
    $p->post_parent = 0;
    $p->guid = get_home_url('/' . $p->post_name); // use url instead?
    $p->menu_order = 0;
    $p->post_type = 'page';
    $p->post_mime_type = '';
    $p->comment_status = 'closed';
    $p->comment_count = 0;
    $p->filter = 'raw';
    $p->ancestors = array(); // 3.6

    // 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 = array();
    $wp_query->query_vars['error'] = '';
    $wp_query->is_404 = FALSE;

    $wp_query->current_post = $p->ID;
    $wp_query->found_posts = 1;
    $wp_query->post_count = 1;
    $wp_query->comment_count = 0;
    // -1 for current_comment displays comment if not logged in!
    $wp_query->current_comment = null;
    $wp_query->is_singular = 1;

    $wp_query->post = $p;
    $wp_query->posts = array($p);
    $wp_query->queried_object = $p;
    $wp_query->queried_object_id = $p->ID;
    $wp_query->current_post = $p->ID;
    $wp_query->post_count = 1;

    return array($p);
}


// Virtual Movie page - tell wordpress we are using the page.php
// template if it exists (it normally will).
//
// We use the theme page.php if we possibly can; if not, we do our best.
// The get_template_part() call will use child theme template if it exists.
// This gets called before any output to browser
//
function vm_template_redir()
{
    // Display movie template using WordPress' internal precedence
    //  ie: child > parent; page-movie.php > page.php
    //  this call includes the template which outputs the content
    get_template_part('page', 'movie');

    exit;
}

By the way, it's important to say that I feel this is pretty much a hack and would love to know how it could be done better. Also, I'd love to see WordPress step up to the mark and provide an API for generating fake pages. (I suspect they have ideological reasons why they won't, but it would be nice to see their solutions to this, even if alternative, explained in depth); I personally feel there are cases where I don't want to go meddling with a user's site just to generate pages.

UPDATE Feb 2014: I've abstracted this into a class which should provide sufficient flexibility for most applications: https://gist.github.com/brianoz/9105004

like image 190
Brian C Avatar answered Oct 21 '22 07:10

Brian C