Pagine personalizzate con plugin
Sto sviluppando un plugin dove vorrei abilitare pagine personalizzate. Nel mio caso, alcune pagine personalizzate conterrebbero un modulo simile a un form di contatto (non letteralmente). Quando l'utente compilerà questo modulo e lo invierà, ci dovrebbe essere il passaggio successivo che richiederà ulteriori informazioni. Diciamo che la prima pagina con il modulo sarebbe collocata a www.domain.tld/custom-page/
e dopo l'invio del modulo avvenuto con successo, l'utente dovrebbe essere reindirizzato a www.domain.tld/custom-page/second
. Il template con elementi HTML e codice PHP dovrebbe essere anch'esso personalizzato.
Penso che una parte del problema sia possibile da realizzare con le riscritture URL personalizzate, ma le altre parti mi sono attualmente sconosciute. Non so davvero da dove dovrei iniziare a cercare e quale sia la corretta denominazione per questo problema. Qualsiasi aiuto sarebbe molto apprezzato.

Quando visiti una pagina frontend, WordPress eseguirà una query al database e se la tua pagina non esiste nel database, quella query non è necessaria e rappresenta solo uno spreco di risorse.
Fortunatamente, WordPress offre un modo per gestire le richieste frontend in modo personalizzato. Questo è possibile grazie al filtro 'do_parse_request'
.
Restituendo false
su questo hook, sarai in grado di interrompere il normale processo di gestione delle richieste da parte di WordPress e gestirle in modo personalizzato.
Detto questo, voglio condividere un modo per costruire un semplice plugin OOP che possa gestire pagine virtuali in modo semplice da usare (e riutilizzare).
Cosa ci serve
- Una classe per gli oggetti pagina virtuale
- Una classe controller, che esaminerà una richiesta e, se è per una pagina virtuale, la mostrerà utilizzando il template appropriato
- Una classe per il caricamento dei template
- I file principali del plugin per aggiungere gli hook che faranno funzionare tutto
Interfacce
Prima di costruire le classi, scriviamo le interfacce per i 3 oggetti elencati sopra.
Iniziamo con l'interfaccia della pagina (file PageInterface.php
):
<?php
namespace GM\VirtualPages;
interface PageInterface {
function getUrl();
function getTemplate();
function getTitle();
function setTitle( $title );
function setContent( $content );
function setTemplate( $template );
/**
* Ottieni un oggetto WP_Post costruito dall'oggetto Page virtuale
*
* @return \WP_Post
*/
function asWpPost();
}
La maggior parte dei metodi sono semplici getter e setter, non c'è bisogno di spiegazioni. L'ultimo metodo dovrebbe essere utilizzato per ottenere un oggetto WP_Post
da una pagina virtuale.
L'interfaccia del controller (file ControllerInterface.php
):
<?php
namespace GM\VirtualPages;
interface ControllerInterface {
/**
* Inizializza il controller, attiva l'hook che consente ai consumer di aggiungere pagine
*/
function init();
/**
* Registra un oggetto pagina nel controller
*
* @param \GM\VirtualPages\Page $page
* @return \GM\VirtualPages\Page
*/
function addPage( PageInterface $page );
/**
* Eseguito su 'do_parse_request' e se la richiesta è per una delle pagine registrate
* configura le variabili globali, attiva gli hook core, carica il template della pagina e termina.
*
* @param boolean $bool Il valore booleano passato da 'do_parse_request'
* @param \WP $wp L'oggetto wp globale passato da 'do_parse_request'
*/
function dispatch( $bool, \WP $wp );
}
e l'interfaccia del caricatore di template (file TemplateLoaderInterface.php
):
<?php
namespace GM\VirtualPages;
interface TemplateLoaderInterface {
/**
* Configura il caricatore per un oggetto pagina
*
* @param \GM\VirtualPagesPageInterface $page la pagina virtuale corrispondente
*/
public function init( PageInterface $page );
/**
* Attiva gli hook core e personalizzati per filtrare i template,
* quindi carica il template trovato.
*/
public function load();
}
I commenti phpDoc dovrebbero essere abbastanza chiari per queste interfacce.
Il Piano
Ora che abbiamo le interfacce, e prima di scrivere le classi concrete, rivediamo il nostro flusso di lavoro:
- Per prima cosa istanziamo una classe
Controller
(che implementaControllerInterface
) e iniettiamo (probabilmente in un costruttore) un'istanza della classeTemplateLoader
(che implementaTemplateLoaderInterface
) - Sull'hook
init
chiamiamo il metodoControllerInterface::init()
per configurare il controller e attivare l'hook che il codice consumer utilizzerà per aggiungere pagine virtuali. - Su 'do_parse_request' chiameremo
ControllerInterface::dispatch()
, e lì controlleremo tutte le pagine virtuali aggiunte e se una di esse ha lo stesso URL della richiesta corrente, la mostreremo; dopo aver impostato tutte le variabili globali core ($wp_query
,$post
). Utilizzeremo anche la classeTemplateLoader
per caricare il template corretto.
Durante questo flusso di lavoro attiveremo alcuni hook core, come wp
, template_redirect
, template_include
... per rendere il plugin più flessibile e garantire la compatibilità con il core e altri plugin, o almeno con un buon numero di essi.
Oltre al flusso di lavoro precedente, avremo anche bisogno di:
- Pulire gli hook e le variabili globali dopo l'esecuzione del main loop, sempre per migliorare la compatibilità con il core e il codice di terze parti
- Aggiungere un filtro su
the_permalink
per farlo restituire l'URL corretto della pagina virtuale quando necessario.
Classi Concrete
Ora possiamo codificare le nostre classi concrete. Iniziamo con la classe pagina (file 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;
}
}
Nulla di più che implementare l'interfaccia.
Ora la classe controller (file 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();
}
}
Essenzialmente la classe crea un oggetto SplObjectStorage
dove vengono memorizzati tutti gli oggetti pagina aggiunti.
Su 'do_parse_request'
, la classe controller esegue un loop su questo storage per trovare una corrispondenza tra l'URL corrente e uno degli URL delle pagine aggiunte.
Se viene trovata una corrispondenza, la classe fa esattamente ciò che avevamo pianificato: attiva alcuni hook, configura le variabili e carica il template tramite la classe che estende TemplateLoaderInterface
.
Dopodiché, semplicemente exit()
.
Quindi scriviamo l'ultima classe:
<?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;
}
}
}
I template memorizzati nella pagina virtuale vengono uniti in un array con i default page.php
e index.php
, prima del caricamento del template viene attivato 'template_redirect'
, per aggiungere flessibilità e migliorare la compatibilità.
Successivamente, il template trovato passa attraverso il filtro personalizzato 'virtual_page_template'
e il filtro core 'template_include'
: ancora una volta per flessibilità e compatibilità.
Infine il file del template viene semplicemente caricato.
File principale del plugin
A questo punto dobbiamo scrivere il file con gli header del plugin e utilizzarlo per aggiungere gli hook che faranno funzionare il nostro flusso di lavoro:
<?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;
} );
Nel file reale probabilmente aggiungeremo più header, come link al plugin e all'autore, descrizione, licenza, ecc.
Gist del Plugin
Ok, abbiamo finito con il nostro plugin. Tutto il codice può essere trovato in un Gist qui.
Aggiunta di Pagine
Il plugin è pronto e funzionante, ma non abbiamo ancora aggiunto alcuna pagina.
Questo può essere fatto all'interno del plugin stesso, all'interno del file functions.php
del tema, in un altro plugin, ecc.
Aggiungere pagine è solo una questione di:
<?php
add_action( 'gm_virtual_pages', function( $controller ) {
// prima pagina
$controller->addPage( new \GM\VirtualPages\Page( '/custom/page' ) )
->setTitle( 'My First Custom Page' )
->setTemplate( 'custom-page-form.php' );
// seconda pagina
$controller->addPage( new \GM\VirtualPages\Page( '/custom/page/deep' ) )
->setTitle( 'My Second Custom Page' )
->setTemplate( 'custom-page-deep.php' );
} );
E così via. Puoi aggiungere tutte le pagine di cui hai bisogno, ricorda solo di utilizzare URL relativi per le pagine.
All'interno del file template puoi utilizzare tutti i tag template di WordPress, e puoi scrivere tutto il PHP e HTML di cui hai bisogno.
L'oggetto post globale è riempito con i dati provenienti dalla nostra pagina virtuale. La pagina virtuale stessa può essere acceduta tramite la variabile $wp_query->virtual_page
.
Ottenere l'URL per una pagina virtuale è facile come passare a home_url()
lo stesso percorso utilizzato per creare la pagina:
$custom_page_url = home_url( '/custom/page' );
Nota che nel main loop nel template caricato, the_permalink()
restituirà il permalink corretto alla pagina virtuale.
Note su stili / script per pagine virtuali
Probabilmente quando vengono aggiunte pagine virtuali, è anche desiderabile avere stili/script personalizzati enqueued e quindi semplicemente utilizzare wp_head()
nei template personalizzati.
Questo è molto semplice, perché le pagine virtuali sono facilmente riconoscibili controllando la variabile $wp_query->virtual_page
e le pagine virtuali possono essere distinte l'una dall'altra controllando i loro URL.
Un semplice esempio:
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;
}
}
} );
Note per OP
Passare dati da una pagina all'altra non è correlato a queste pagine virtuali, ma è un compito generico.
Tuttavia, se hai un form nella prima pagina e vuoi passare dati da lì alla seconda pagina, usa semplicemente l'URL della seconda pagina nella proprietà action
del form.
Ad esempio, nel file template della prima pagina puoi:
<form action="<?php echo home_url( '/custom/page/deep' ); ?>" method="POST">
<input type="text" name="testme">
</form>
e poi nel file template della seconda pagina:
<?php $testme = filter_input( INPUT_POST, 'testme', FILTER_SANITIZE_STRING ); ?>
<h1>Il valore Test-Me dall'altra pagina è: <?php echo $testme; ?></h1>

Risposta incredibilmente completa, non solo sul problema in sé, ma anche sulla creazione di un plugin in stile OOP e altro ancora. Hai sicuramente il mio voto positivo, immagina di più, uno per ogni livello che la risposta copre.

Il codice nel Controller è leggermente sbagliato... checkRequest() sta ottenendo le informazioni sul percorso da home_url() che restituisce localhost/wordpress. Dopo preg_replace e add_query_arg, questo URL diventa /wordpress/virtual-page. E dopo il trim in checkRequest questo URL diventa wordpress/virtual. Funzionerebbe se wordpress fosse installato nella cartella principale del dominio. Potresti per favore fornire una correzione per questo problema perché non riesco a trovare la funzione appropriata che restituirebbe l'URL corretto. Grazie per tutto! (Accetterò la risposta quando sarà perfetta :)

Congratulazioni, ottima risposta e mi piacerebbe vedere tutto questo lavoro come soluzione gratuita.

@G.M.: Nel mio caso WordPress è installato in .../htdocs/wordpress/ e il sito è disponibile su http://localhost/wordpress/. home_url() restituisce http://localhost/wordpress e add_query_arg(array()) restituisce /wordpress/virtual-page/. Quando confrontiamo $path e il trimmed $this->pages->current()->getUrl() in checkRequest() c'è un problema perché $path è wordpress/virtual-page
e l'url troncato della pagina è virtual-page
.

Incredibile, ma come diavolo ho digitato ' invece di " in preg_replace. Hai ragione, è stato un mio errore, scusa. Ora sto ricevendo un altro errore dopo aver risolto il primo. Undefined property: WP_Query::$queried_object_id
. Questo probabilmente è relativo a Page.php dove impostiamo gli attributi del post o al setupQuery del Controller. Ho un'altra domanda riguardo 'post_author' in Page.php dove c'è la funzione is_user_logged_in(). Ho un sito dove chiunque può registrarsi e fare il login. Potrebbe essere una minaccia per la sicurezza? È obbligatorio avere la funzione is_user_logged_in() o può essere impostata a 0 di default?

La funzione is_user_logged_in()
non è un problema di sicurezza: non stai assegnando alcuna capacità all'utente loggato, e la pagina non esiste nemmeno nel database. Tuttavia, impostarla sempre a 0 non sarà un problema. Per quanto riguarda l'errore, ho testato il codice molte volte, in diverse configurazioni (windows, linux, php 5.4, 5.5)... con WP debug e plugin di debug attivi. Sempre nessun problema. Quindi, o hai modificato qualcosa nel codice, o il problema è relativo a un plugin/tema in conflitto. In entrambi i casi non posso risolverlo e spero capirai che penso di aver già dedicato troppo tempo a questa risposta. @user1257255

Capisco. Grazie mille per questa risposta così approfondita e per tutto il resto! :)

Ciao @gmazzap se passo un parametro query non funziona la route con addPage

Una soluzione molto più semplice è aggiungere automaticamente la pagina se non esiste. Possiamo utilizzare una meta key personalizzata per verificare la sua esistenza. Possiamo implementare un callback nel filtro the_content
se l'ID corrente corrisponde all'ID della pagina.
$this->id = $this->settings['id'];
add_action('init', function (){
$page = get_posts([
'meta_key' => 'my_frontend_id',
'meta_value' => $this->id,
'post_type' => 'page',
'post_status' => 'any',
'numberposts' => 1
])[0];
if (is_null($page)){
$pageId = wp_insert_post($this->settings);
add_post_meta($pageId, 'my_frontend_id', $this->id, true);
}
else
$pageId = $page->ID;
add_filter('the_content', function ($content) use ($pageId){
if (is_singular() && in_the_loop() && is_main_query() && get_the_ID() === $pageId){
ob_start();
$this->render();
return ob_get_clean();
}
return $content;
}, 1);
});
Però c'è un inconveniente in questo approccio. Ad esempio, la protezione con password non funziona in questo modo. Richiede uno sforzo aggiuntivo per proteggere correttamente queste pagine da accessi non autorizzati in lettura o scrittura. Alla fine ho preferito utilizzare gli shortcode invece di questo filtro, anche se posso immaginare scenari in cui potrebbe essere utile.

Una volta ho utilizzato una soluzione descritta qui: http://scott.sherrillmix.com/blog/blogger/creating-a-better-fake-post-with-a-wordpress-plugin/
In realtà, quando l'ho usata, ho esteso la soluzione in modo da poter registrare più di una pagina alla volta (il resto del codice è più o meno simile alla soluzione linkata nel paragrafo precedente).
La soluzione richiede però che siano abilitati i permalink "belli"...
<?php
class FakePages {
public function __construct() {
add_filter( 'the_posts', array( $this, 'fake_pages' ) );
}
/**
* Registra internamente le pagine che vogliamo falsificare. La chiave dell'array è lo slug sotto cui sarà disponibile nel frontend
* @return mixed
*/
private static function get_fake_pages() {
//http://example.com/fakepage1
$fake_pages['fakepage1'] = array(
'title' => 'Pagina Falsa 1',
'content' => 'Questo è il contenuto della pagina falsa 1'
);
//http://example.com/fakepage2
$fake_pages['fakepage2'] = array(
'title' => 'Pagina Falsa 2',
'content' => 'Questo è il contenuto della pagina falsa 2'
);
return $fake_pages;
}
/**
* Falsifica il risultato di get posts
*
* @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;
//Strutture di permalink più lunghe potrebbero non corrispondere allo slug del post falso e causare un errore 404, quindi lo intercettiamo qui
unset( $wp_query->query["error"] );
$wp_query->query_vars["error"] = "";
$wp_query->is_404 = false;
}
return $posts;
}
/**
* Crea una pagina falsa virtuale
*
* @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();

E per quanto riguarda un template personalizzato dove posso inserire il mio modulo?
