Cuando visita una página frontend, WordPress consultará la base de datos y si su página no existe en la base de datos, esa consulta no es necesaria y es solo un desperdicio de recursos.
Afortunadamente, WordPress ofrece una forma de manejar solicitudes frontend de una manera personalizada. Eso se hace gracias al 'do_parse_request'
filtro.
Volviendo false
a ese gancho, podrá evitar que WordPress procese solicitudes y hacerlo de manera personalizada.
Dicho esto, quiero compartir una forma de construir un complemento OOP simple que pueda manejar páginas virtuales de una manera fácil de usar (y reutilizar).
Lo que necesitamos
- Una clase para objetos de página virtual.
- Una clase de controlador, que examinará una solicitud y, si es para una página virtual, muéstrela utilizando la plantilla adecuada
- Una clase para cargar plantillas
- Principales archivos de complemento para agregar los ganchos que harán que todo funcione
Interfaces
Antes de construir clases, escribamos las interfaces para los 3 objetos enumerados anteriormente.
Primero la interfaz de la página (archivo PageInterface.php
):
<?php
namespace GM\VirtualPages;
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();
}
La mayoría de los métodos son solo getters y setters, sin necesidad de explicación. El último método debe usarse para obtener un WP_Post
objeto de una página virtual.
La interfaz del controlador (archivo ControllerInterface.php
):
<?php
namespace GM\VirtualPages;
interface ControllerInterface {
/**
* Init the controller, fires the hook that allows consumer to add pages
*/
function init();
/**
* Register a page object in the controller
*
* @param \GM\VirtualPages\Page $page
* @return \GM\VirtualPages\Page
*/
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 );
}
y la interfaz del cargador de plantillas (archivo TemplateLoaderInterface.php
):
<?php
namespace GM\VirtualPages;
interface TemplateLoaderInterface {
/**
* Setup loader for a page objects
*
* @param \GM\VirtualPagesPageInterface $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();
}
Los comentarios de phpDoc deberían ser bastante claros para estas interfaces.
El plan
Ahora que tenemos interfaces, y antes de escribir clases concretas, repasemos nuestro flujo de trabajo:
- Primero instanciamos una
Controller
clase (implementando ControllerInterface
) e inyectamos (probablemente en un constructor) una instancia de TemplateLoader
clase (implementando TemplateLoaderInterface
)
- En el
init
gancho, llamamos al ControllerInterface::init()
método para configurar el controlador y disparar el gancho que el código del consumidor usará para agregar páginas virtuales.
- En 'do_parse_request' llamaremos
ControllerInterface::dispatch()
, y allí revisaremos todas las páginas virtuales agregadas y si una de ellas tiene la misma URL de la solicitud actual, muéstrela; después de haber establecido todas las variables globales centrales ( $wp_query
, $post
). También usaremos TemplateLoader
class para cargar la plantilla correcta.
Durante este flujo de trabajo vamos a desencadenar algunos ganchos básicos, como wp
, template_redirect
, template_include
... para hacer el plugin más flexible y asegurar la compatibilidad con el núcleo y otros plugins, o al menos con un número bueno de ellos.
Además del flujo de trabajo anterior, también tendremos que:
- Limpie los ganchos y las variables globales después de que se ejecute el bucle principal, nuevamente para mejorar la compatibilidad con el núcleo y el código de terceros
- Agregue un filtro
the_permalink
para que devuelva la URL de página virtual correcta cuando sea necesario.
Clases de hormigón
Ahora podemos codificar nuestras clases concretas. Comencemos con la clase de página (archivo Page.php
):
<?php
namespace GM\VirtualPages;
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;
}
}
Nada más que implementar la interfaz.
Ahora la clase de controlador (archivo Controller.php
):
<?php
namespace GM\VirtualPages;
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();
}
}
Esencialmente, la clase crea un SplObjectStorage
objeto donde se almacenan todos los objetos de páginas agregados.
Encendido 'do_parse_request'
, la clase de controlador repite este almacenamiento para encontrar una coincidencia para la URL actual en una de las páginas agregadas.
Si se encuentra, la clase hace exactamente lo que planeamos: desencadenar algunos ganchos, configurar variables y cargar la plantilla a través de la extensión de la clase TemplateLoaderInterface
. Después de eso, solo exit()
.
Entonces, escribamos la última clase:
<?php
namespace GM\VirtualPages;
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;
}
}
}
Las plantillas almacenadas en la página virtual se fusionan en una matriz con valores predeterminados page.php
y index.php
, antes de que se active la carga de la plantilla 'template_redirect'
, para agregar flexibilidad y mejorar la compatibilidad.
Después de eso, la plantilla encontrada pasa a través de los filtros personalizados 'virtual_page_template'
y principales 'template_include'
: nuevamente para mayor flexibilidad y compatibilidad.
Finalmente, el archivo de plantilla se acaba de cargar.
Archivo de complemento principal
En este punto, necesitamos escribir el archivo con encabezados de plugin y usarlo para agregar los ganchos que harán que nuestro flujo de trabajo suceda:
<?php namespace GM\VirtualPages;
/*
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;
} );
En el archivo real probablemente agregaremos más encabezados, como plugins y enlaces de autor, descripción, licencia, etc.
Plugin Gist
Ok, hemos terminado con nuestro complemento. Todo el código se puede encontrar en un Gist aquí .
Agregar páginas
El complemento está listo y funcionando, pero no hemos agregado ninguna página.
Eso se puede hacer dentro del complemento en sí, dentro del tema functions.php
, en otro complemento, etc.
Agregar páginas es solo una cuestión de:
<?php
add_action( 'gm_virtual_pages', function( $controller ) {
// first page
$controller->addPage( new \GM\VirtualPages\Page( '/custom/page' ) )
->setTitle( 'My First Custom Page' )
->setTemplate( 'custom-page-form.php' );
// second page
$controller->addPage( new \GM\VirtualPages\Page( '/custom/page/deep' ) )
->setTitle( 'My Second Custom Page' )
->setTemplate( 'custom-page-deep.php' );
} );
Y así. Puede agregar todas las páginas que necesita, solo recuerde usar URL relativas para las páginas.
Dentro del archivo de plantilla puede usar todas las etiquetas de plantilla de WordPress, y puede escribir todo el PHP y HTML que necesite.
El objeto de publicación global se llena con datos procedentes de nuestra página virtual. Se puede acceder a la página virtual en sí a través de la $wp_query->virtual_page
variable.
Para obtener la URL de una página virtual es tan fácil como pasar a home_url()
la misma ruta utilizada para crear la página:
$custom_page_url = home_url( '/custom/page' );
Tenga en cuenta que en el bucle principal de la plantilla cargada, the_permalink()
devolverá el enlace permanente correcto a la página virtual.
Notas sobre estilos / scripts para páginas virtuales
Probablemente cuando se agregan páginas virtuales, también es deseable tener estilos / scripts personalizados en cola y luego usarlos wp_head()
en plantillas personalizadas.
Eso es muy fácil, porque las páginas virtuales se reconocen fácilmente mirando $wp_query->virtual_page
páginas variables y las páginas virtuales se pueden distinguir una de otra mirando sus URL.
Solo un ejemplo:
add_action( 'wp_enqueue_scripts', function() {
global $wp_query;
if (
is_page()
&& isset( $wp_query->virtual_page )
&& $wp_query->virtual_page instanceof \GM\VirtualPages\PageInterface
) {
$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;
}
}
} );
Notas a OP
Pasar datos de una página a otra no está relacionado con estas páginas virtuales, pero es solo una tarea genérica.
Sin embargo, si tiene un formulario en la primera página y desea pasar datos desde allí a la segunda página, simplemente use la URL de la segunda página en la action
propiedad de formulario .
Por ejemplo, en el primer archivo de plantilla de página puede:
<form action="<?php echo home_url( '/custom/page/deep' ); ?>" method="POST">
<input type="text" name="testme">
</form>
y luego en el segundo archivo de plantilla de página:
<?php $testme = filter_input( INPUT_POST, 'testme', FILTER_SANITIZE_STRING ); ?>
<h1>Test-Me value form other page is: <?php echo $testme; ?></h1>