Páginas personalizadas con plugin

13

Estoy desarrollando un complemento en el que me gustaría habilitar páginas personalizadas. En mi caso, alguna página personalizada contendría un formulario como formulario de contacto (no literalmente). Cuando el usuario complete este formulario y lo envíe, debe haber el siguiente paso que requerirá más información. Digamos que la primera página con el formulario se ubicaría en www.domain.tld/custom-page/y después del envío exitoso del formulario, el usuario debería ser redirigido a www.domain.tld/custom-page/second. La plantilla con elementos HTML y código PHP también debe ser personalizada.

Creo que es posible lograr una parte del problema con reescrituras de URL personalizadas, pero las otras partes son actualmente desconocidas para mí. Realmente no sé dónde debería comenzar a buscar y cuál es el nombre correcto para ese problema. Cualquier ayuda sería muy apreciada.

usuario1257255
fuente
¿Desea que estas páginas se almacenen en WordPress o 'virtual'?
Welcher
Tendrías que usar la API de reescritura. Esto no debería ser demasiado difícil. Asegúrate de publicar los datos en la segunda página y estarás bien.
setterGetter
@Welcher: estas páginas no son lo mismo que las ofertas de WordPress en el panel de control. Deberían guardar los datos en la base de datos, pero ese no es el problema. @ .setterGetter: ¿Tiene algún ejemplo de cómo pasar datos de la primera página a la segunda y dónde (¿acción?) incluir el archivo PHP que muestra el formulario?
user1257255
¿Consideró usar un formulario de página único, con múltiples diapositivas (javascript y / o css) de campos de entrada?
Birgire

Respuestas:

56

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 falsea 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_Postobjeto 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 Controllerclase (implementando ControllerInterface) e inyectamos (probablemente en un constructor) una instancia de TemplateLoaderclase (implementando TemplateLoaderInterface)
  • En el initgancho, 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 TemplateLoaderclass 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_permalinkpara 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 SplObjectStorageobjeto 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.phpy 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_pagevariable.

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_pagepá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 actionpropiedad 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>
gmazzap
fuente
99
Increíble respuesta integral, no solo sobre el problema en sí, sino también sobre la creación de un complemento de estilo OOP y más. Seguro que obtuviste mi voto, imagina más, uno para cada nivel que cubre la respuesta.
Nicolai
2
Solución muy elegante y directa. Votado a favor, tuiteado.
Kaiser
El código en Controller está un poco equivocado ... checkRequest () está obteniendo información de ruta de home_url () que devuelve localhost / wordpress. Después de preg_replace y add_query_arg, esta url se convierte en / wordpress / virtual-page. Y después del recorte en checkRequest, esta url es wordpress / virtual. Esto funcionaría si WordPress se instalara en la carpeta raíz del dominio. ¿Puede proporcionar una solución para ese problema porque no puedo encontrar la función adecuada que devuelva la URL correcta? ¡Gracias por todo! (Aceptaré la respuesta después de que sea perfecta :)
user1257255
2
Felicidades, buena respuesta y necesito ver todo este trabajo como una solución gratuita.
bueltge
@GM: en mi caso, WordPress está instalado en ... / htdocs / wordpress / y el sitio está disponible en localhost / wordpress . home_url () devuelve localhost / wordpress y add_query_arg (array ()) devuelve / wordpress / virtual-page /. Cuando estamos comparando $ path y recortado $ this-> pages-> current () -> getUrl () en checkRequest () es un problema porque $ path es wordpress/virtual-pagey la URL recortada de la página es virtual-page.
user1257255
0

Una vez utilicé una solución descrita aquí: http://scott.sherrillmix.com/blog/blogger/creating-a-better-fake-post-with-a-wordpress-plugin/

En realidad, cuando lo estaba usando, extiendo la solución de una manera que puedo registrar más de una página a la vez (el resto de un código es +/- similar a la solución que estoy vinculando desde el párrafo anterior).

La solución requiere que tengas enlaces permanentes permitidos aunque ...

<?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();
david.binda
fuente
¿Qué pasa con la plantilla personalizada donde puedo colocar mi formulario?
user1257255
contenten la matriz cuando está registrando, la página falsa se muestra en el cuerpo de la página; puede contener un HTML, así como texto simple o incluso un código corto.
david.binda