I’m developing some plugin where I would like to enable custom pages. In my case some custom page would contain a form like contact form (not literally). When user will fill out this form and send it, there should be the next step which will require more information. Lets say that the first page with form would be located at www.domain.tld/custom-page/ and after successful form submission, the user should be redirected to www.domain.tld/custom-page/second. Template with HTML elements and PHP code should be also custom.
I think that a part of the problem is possible to achieve with custom URL rewrites, but the other parts are currently unknown to me. I really don’t know where should I start looking and what’s the correct naming for that problem. Any help would be really appreciated.
Answers:
Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.
Method 1
When you visit a frontend page, WordPress will query the database and if your page does not exist in the database, that query is not needed and is just a waste of resources.
Luckily, WordPress offers a way to handle frontend requests in a custom way. That is done thanks to the 'do_parse_request' filter.
Returning false on that hook, you will be able to stop WordPress from processing requests and do it in your own custom way.
That said, I want to share a way to build a simple OOP plugin that can handle virtual pages in a easy to use (and re-use) way.
What we need
- A class for virtual page objects
- A controller class, that will look at a request and if it is for a virtual page, show it using the proper template
- A class for template loading
- Main plugin files to add the hooks that will make everything work
Interfaces
Before building classes, let’s write the interfaces for the 3 objects listed above.
First the page interface (file PageInterface.php):
<?php
namespace GMVirtualPages;
interface PageInterface {
function getUrl();
function getTemplate();
function getTitle();
function setTitle( $title );
function setContent( $content );
function setTemplate( $template );
/**
* Get a WP_Post build using virtual Page object
*
* @return WP_Post
*/
function asWpPost();
}
Most methods are just getters and setters, no need for explanation. Last method should be used to get a WP_Post object from a virtual page.
The controller interface (file ControllerInterface.php):
<?php
namespace GMVirtualPages;
interface ControllerInterface {
/**
* Init the controller, fires the hook that allows consumer to add pages
*/
function init();
/**
* Register a page object in the controller
*
* @param GMVirtualPagesPage $page
* @return GMVirtualPagesPage
*/
function addPage( PageInterface $page );
/**
* Run on 'do_parse_request' and if the request is for one of the registered pages
* setup global variables, fire core hooks, requires page template and exit.
*
* @param boolean $bool The boolean flag value passed by 'do_parse_request'
* @param WP $wp The global wp object passed by 'do_parse_request'
*/
function dispatch( $bool, WP $wp );
}
and the template loader interface (file TemplateLoaderInterface.php ):
<?php
namespace GMVirtualPages;
interface TemplateLoaderInterface {
/**
* Setup loader for a page objects
*
* @param GMVirtualPagesPageInterface $page matched virtual page
*/
public function init( PageInterface $page );
/**
* Trigger core and custom hooks to filter templates,
* then load the found template.
*/
public function load();
}
phpDoc comments should be pretty clear for these interfaces.
The Plan
Now that we have interfaces, and before writing concrete classes, let’s review our workflow:
- First we instantiate a
Controllerclass (implementingControllerInterface) and inject (probably in a constructor) an instance ofTemplateLoaderclass (implementingTemplateLoaderInterface) - On
inithook we call theControllerInterface::init()method to setup the controller and to fire the hook that consumer code will use to add virtual pages. - On ‘do_parse_request’ we will call
ControllerInterface::dispatch(), and there we will check all the virtual pages added and if one of them has same URL of current request, display it; after having set all the core global variables ($wp_query,$post). We will also useTemplateLoaderclass to load the right template.
During this workflow we will trigger some core hooks, like wp, template_redirect, template_include… to make the plugin more flexible and ensure compatibility with core and other plugins, or at least with a good number of them.
Aside from previous workflow, we also will need to:
- Clean up hooks and global variables after main loop runs, again to improve compatibility with core and 3rd party code
- Add a filter on
the_permalinkto make it return the right virtual page URL when needed.
Concrete Classes
Now we can code our concrete classes. Let’s start with page class (file Page.php):
<?php
namespace GMVirtualPages;
class Page implements PageInterface {
private $url;
private $title;
private $content;
private $template;
private $wp_post;
function __construct( $url, $title = 'Untitled', $template = 'page.php' ) {
$this->url = filter_var( $url, FILTER_SANITIZE_URL );
$this->setTitle( $title );
$this->setTemplate( $template);
}
function getUrl() {
return $this->url;
}
function getTemplate() {
return $this->template;
}
function getTitle() {
return $this->title;
}
function setTitle( $title ) {
$this->title = filter_var( $title, FILTER_SANITIZE_STRING );
return $this;
}
function setContent( $content ) {
$this->content = $content;
return $this;
}
function setTemplate( $template ) {
$this->template = $template;
return $this;
}
function asWpPost() {
if ( is_null( $this->wp_post ) ) {
$post = array(
'ID' => 0,
'post_title' => $this->title,
'post_name' => sanitize_title( $this->title ),
'post_content' => $this->content ? : '',
'post_excerpt' => '',
'post_parent' => 0,
'menu_order' => 0,
'post_type' => 'page',
'post_status' => 'publish',
'comment_status' => 'closed',
'ping_status' => 'closed',
'comment_count' => 0,
'post_password' => '',
'to_ping' => '',
'pinged' => '',
'guid' => home_url( $this->getUrl() ),
'post_date' => current_time( 'mysql' ),
'post_date_gmt' => current_time( 'mysql', 1 ),
'post_author' => is_user_logged_in() ? get_current_user_id() : 0,
'is_virtual' => TRUE,
'filter' => 'raw'
);
$this->wp_post = new WP_Post( (object) $post );
}
return $this->wp_post;
}
}
Nothing more than implementing the interface.
Now the controller class (file Controller.php):
<?php
namespace GMVirtualPages;
class Controller implements ControllerInterface {
private $pages;
private $loader;
private $matched;
function __construct( TemplateLoaderInterface $loader ) {
$this->pages = new SplObjectStorage;
$this->loader = $loader;
}
function init() {
do_action( 'gm_virtual_pages', $this );
}
function addPage( PageInterface $page ) {
$this->pages->attach( $page );
return $page;
}
function dispatch( $bool, WP $wp ) {
if ( $this->checkRequest() && $this->matched instanceof Page ) {
$this->loader->init( $this->matched );
$wp->virtual_page = $this->matched;
do_action( 'parse_request', $wp );
$this->setupQuery();
do_action( 'wp', $wp );
$this->loader->load();
$this->handleExit();
}
return $bool;
}
private function checkRequest() {
$this->pages->rewind();
$path = trim( $this->getPathInfo(), '/' );
while( $this->pages->valid() ) {
if ( trim( $this->pages->current()->getUrl(), '/' ) === $path ) {
$this->matched = $this->pages->current();
return TRUE;
}
$this->pages->next();
}
}
private function getPathInfo() {
$home_path = parse_url( home_url(), PHP_URL_PATH );
return preg_replace( "#^/?{$home_path}/#", '/', esc_url( add_query_arg(array()) ) );
}
private function setupQuery() {
global $wp_query;
$wp_query->init();
$wp_query->is_page = TRUE;
$wp_query->is_singular = TRUE;
$wp_query->is_home = FALSE;
$wp_query->found_posts = 1;
$wp_query->post_count = 1;
$wp_query->max_num_pages = 1;
$posts = (array) apply_filters(
'the_posts', array( $this->matched->asWpPost() ), $wp_query
);
$post = $posts[0];
$wp_query->posts = $posts;
$wp_query->post = $post;
$wp_query->queried_object = $post;
$GLOBALS['post'] = $post;
$wp_query->virtual_page = $post instanceof WP_Post && isset( $post->is_virtual )
? $this->matched
: NULL;
}
public function handleExit() {
exit();
}
}
Essentially the class creates an SplObjectStorage object where all the added pages objects are stored.
On 'do_parse_request', the controller class loops this storage to find a match for the current URL in one of the added pages.
If it is found, the class does exactly what we planned: trigger some hooks, setup variables and load the template via the class extending TemplateLoaderInterface.
After that, just exit().
So let’s write the last class:
<?php
namespace GMVirtualPages;
class TemplateLoader implements TemplateLoaderInterface {
public function init( PageInterface $page ) {
$this->templates = wp_parse_args(
array( 'page.php', 'index.php' ), (array) $page->getTemplate()
);
}
public function load() {
do_action( 'template_redirect' );
$template = locate_template( array_filter( $this->templates ) );
$filtered = apply_filters( 'template_include',
apply_filters( 'virtual_page_template', $template )
);
if ( empty( $filtered ) || file_exists( $filtered ) ) {
$template = $filtered;
}
if ( ! empty( $template ) && file_exists( $template ) ) {
require_once $template;
}
}
}
Templates stored in the virtual page are merged in an array with defaults page.php and index.php, before loading template 'template_redirect' is fired, to add flexibility and improve compatibility.
After that, the found template passes through the custom 'virtual_page_template' and the core 'template_include' filters: again for flexibility and compatibility.
Finally the template file is just loaded.
Main plugin file
At this point we need to write the file with plugin headers and use it to add the hooks that will make our workflow happen:
<?php namespace GMVirtualPages;
/*
Plugin Name: GM Virtual Pages
*/
require_once 'PageInterface.php';
require_once 'ControllerInterface.php';
require_once 'TemplateLoaderInterface.php';
require_once 'Page.php';
require_once 'Controller.php';
require_once 'TemplateLoader.php';
$controller = new Controller ( new TemplateLoader );
add_action( 'init', array( $controller, 'init' ) );
add_filter( 'do_parse_request', array( $controller, 'dispatch' ), PHP_INT_MAX, 2 );
add_action( 'loop_end', function( WP_Query $query ) {
if ( isset( $query->virtual_page ) && ! empty( $query->virtual_page ) ) {
$query->virtual_page = NULL;
}
} );
add_filter( 'the_permalink', function( $plink ) {
global $post, $wp_query;
if (
$wp_query->is_page && isset( $wp_query->virtual_page )
&& $wp_query->virtual_page instanceof Page
&& isset( $post->is_virtual ) && $post->is_virtual
) {
$plink = home_url( $wp_query->virtual_page->getUrl() );
}
return $plink;
} );
In the real file we will probably add more headers, like plugin and author links, description, license, etc.
Plugin Gist
Ok, we are done with our plugin. All the code can be find in a Gist here.
Adding Pages
Plugin is ready and working, but we haven’t added any pages.
That can be done inside the plugin itself, inside theme functions.php, in another plugin, etc.
Add pages is just a matter of:
<?php
add_action( 'gm_virtual_pages', function( $controller ) {
// first page
$controller->addPage( new GMVirtualPagesPage( '/custom/page' ) )
->setTitle( 'My First Custom Page' )
->setTemplate( 'custom-page-form.php' );
// second page
$controller->addPage( new GMVirtualPagesPage( '/custom/page/deep' ) )
->setTitle( 'My Second Custom Page' )
->setTemplate( 'custom-page-deep.php' );
} );
And so on. You can add all the pages you need, just remember to use relative URLs for the pages.
Inside the template file you can use all WordPress template tags, and you can write all the PHP and HTML you need.
The global post object is filled with data coming from our virtual page. The virtual page itself can be accessed via $wp_query->virtual_page variable.
To get URL for a virtual page is as easy as passing to home_url() the same path used to create the page:
$custom_page_url = home_url( '/custom/page' );
Note that in main loop in the loaded template, the_permalink() will return correct permalink to virtual page.
Notes on styles / scripts for virtual pages
Probably when virtual pages are added, it’s also desirable to have custom styles/scripts enqueued and then just use wp_head() in custom templates.
That’s very easy, because virtual pages are easily recognized looking at $wp_query->virtual_page variable and virtual pages can be distinguished one from another looking at their URLs.
Just an example:
add_action( 'wp_enqueue_scripts', function() {
global $wp_query;
if (
is_page()
&& isset( $wp_query->virtual_page )
&& $wp_query->virtual_page instanceof GMVirtualPagesPageInterface
) {
$url = $wp_query->virtual_page->getUrl();
switch ( $url ) {
case '/custom/page' :
wp_enqueue_script( 'a_script', $a_script_url );
wp_enqueue_style( 'a_style', $a_style_url );
break;
case '/custom/page/deep' :
wp_enqueue_script( 'another_script', $another_script_url );
wp_enqueue_style( 'another_style', $another_style_url );
break;
}
}
} );
Notes to OP
Passing data from a page to another is not related to these virtual pages, but is just a generic task.
However, if you have a form in the first page, and want to pass data from there to the second page, simply use the URL of the second page in form action property.
E.g. in the first page template file you can:
<form action="<?php echo home_url( '/custom/page/deep' ); ?>" method="POST">
<input type="text" name="testme">
</form>
and then in the second page template file:
<?php $testme = filter_input( INPUT_POST, 'testme', FILTER_SANITIZE_STRING ); ?> <h1>Test-Me value form other page is: <?php echo $testme; ?></h1>
Method 2
I once used a solution described here: http://scott.sherrillmix.com/blog/blogger/creating-a-better-fake-post-with-a-wordpress-plugin/
Actually, when I was using it, I extend the solution in a way I can register more than one page a time (the rest of a code is +/- similar to the solution I’m linking from a paragraph above).
The solution requires you to have nice permalinks allowed tho…
<?php
class FakePages {
public function __construct() {
add_filter( 'the_posts', array( $this, 'fake_pages' ) );
}
/**
* Internally registers pages we want to fake. Array key is the slug under which it is being available from the frontend
* @return mixed
*/
private static function get_fake_pages() {
//http://example.com/fakepage1
$fake_pages['fakepage1'] = array(
'title' => 'Fake Page 1',
'content' => 'This is a content of fake page 1'
);
//http://example.com/fakepage2
$fake_pages['fakepage2'] = array(
'title' => 'Fake Page 2',
'content' => 'This is a content of fake page 2'
);
return $fake_pages;
}
/**
* Fakes get posts result
*
* @param $posts
*
* @return array|null
*/
public function fake_pages( $posts ) {
global $wp, $wp_query;
$fake_pages = self::get_fake_pages();
$fake_pages_slugs = array();
foreach ( $fake_pages as $slug => $fp ) {
$fake_pages_slugs[] = $slug;
}
if ( true === in_array( strtolower( $wp->request ), $fake_pages_slugs )
|| ( true === isset( $wp->query_vars['page_id'] )
&& true === in_array( strtolower( $wp->query_vars['page_id'] ), $fake_pages_slugs )
)
) {
if ( true === in_array( strtolower( $wp->request ), $fake_pages_slugs ) ) {
$fake_page = strtolower( $wp->request );
} else {
$fake_page = strtolower( $wp->query_vars['page_id'] );
}
$posts = null;
$posts[] = self::create_fake_page( $fake_page, $fake_pages[ $fake_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;
$wp_query->is_fake_page = true;
$wp_query->fake_page = $wp->request;
//Longer permalink structures may not match the fake post slug and cause a 404 error so we catch the error here
unset( $wp_query->query["error"] );
$wp_query->query_vars["error"] = "";
$wp_query->is_404 = false;
}
return $posts;
}
/**
* Creates virtual fake page
*
* @param $pagename
* @param $page
*
* @return stdClass
*/
private static function create_fake_page( $pagename, $page ) {
$post = new stdClass;
$post->post_author = 1;
$post->post_name = $pagename;
$post->guid = get_bloginfo( 'wpurl' ) . '/' . $pagename;
$post->post_title = $page['title'];
$post->post_content = $page['content'];
$post->ID = - 1;
$post->post_status = 'static';
$post->comment_status = 'closed';
$post->ping_status = 'closed';
$post->comment_count = 0;
$post->post_date = current_time( 'mysql' );
$post->post_date_gmt = current_time( 'mysql', 1 );
return $post;
}
}
new FakePages();
All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0