Pagini personalizate cu ajutorul unui plugin
Dezvolt un plugin în care aș dori să activez pagini personalizate. În cazul meu, o pagină personalizată ar conține un formular similar cu un formular de contact (nu literal). Când utilizatorul va completa acest formular și îl va trimite, ar trebui să existe următorul pas care va solicita mai multe informații. Să zicem că prima pagină cu formular ar fi localizată la www.domain.tld/custom-page/
și după trimiterea cu succes a formularului, utilizatorul ar trebui să fie redirecționat către www.domain.tld/custom-page/second
. Template-ul cu elementele HTML și codul PHP ar trebui să fie de asemenea personalizat.
Cred că o parte din problemă poate fi rezolvată cu rewrite-uri URL personalizate, dar celelalte părți îmi sunt momentan necunoscute. Nu știu de unde ar trebui să încep să caut și care este denumirea corectă pentru această problemă. Orice ajutor ar fi foarte apreciat.

Când vizitați o pagină frontend, WordPress va interoga baza de date, iar dacă pagina dumneavoastră nu există în baza de date, acea interogare nu este necesară și reprezintă doar o pierdere de resurse.
Din fericire, WordPress oferă o modalitate de a gestiona cererile frontend într-un mod personalizat. Acest lucru se realizează datorită filtrului 'do_parse_request'
.
Returnând false
pe acest hook, veți putea opri WordPress din procesarea cererilor și o veți face în modul dumneavoastră personalizat.
Cu aceasta menționată, doresc să împărtășesc o metodă de a construi un simplu plugin OOP care poate gestiona pagini virtuale într-un mod ușor de utilizat (și reutilizat).
Ce avem nevoie
- O clasă pentru obiectele paginilor virtuale
- O clasă controler, care va analiza o cerere și, dacă este pentru o pagină virtuală, o va afișa folosind șablonul corespunzător
- O clasă pentru încărcarea șabloanelor
- Fișierele principale ale pluginului pentru a adăuga hook-urile care vor face totul să funcționeze
Interfețe
Înainte de a construi clasele, să scriem interfețele pentru cele 3 obiecte enumerate mai sus.
Mai întâi interfața pentru pagină (fișierul PageInterface.php
):
<?php
namespace GM\VirtualPages;
interface PageInterface {
function getUrl();
function getTemplate();
function getTitle();
function setTitle( $title );
function setContent( $content );
function setTemplate( $template );
/**
* Obține un obiect WP_Post construit folosind obiectul Page virtual
*
* @return \WP_Post
*/
function asWpPost();
}
Majoritatea metodelor sunt doar getteri și setteri, nu necesită explicații. Ultima metodă ar trebui utilizată pentru a obține un obiect WP_Post
dintr-o pagină virtuală.
Interfața controlerului (fișierul ControllerInterface.php
):
<?php
namespace GM\VirtualPages;
interface ControllerInterface {
/**
* Inițializează controlerul, lansează hook-ul care permite consumatorilor să adauge pagini
*/
function init();
/**
* Înregistrează un obiect de pagină în controler
*
* @param \GM\VirtualPages\Page $page
* @return \GM\VirtualPages\Page
*/
function addPage( PageInterface $page );
/**
* Rulează pe 'do_parse_request' și dacă cererea este pentru una din paginile înregistrate
* configurează variabilele globale, lansează hook-urile nucleu, încarcă șablonul paginii și iese.
*
* @param boolean $bool Valoarea booleană transmisă de 'do_parse_request'
* @param \WP $wp Obiectul global wp transmis de 'do_parse_request'
*/
function dispatch( $bool, \WP $wp );
}
și interfața pentru încărcarea șabloanelor (fișierul TemplateLoaderInterface.php
):
<?php
namespace GM\VirtualPages;
interface TemplateLoaderInterface {
/**
* Configurează încărcătorul pentru un obiect de pagină
*
* @param \GM\VirtualPagesPageInterface $page pagină virtuală potrivită
*/
public function init( PageInterface $page );
/**
* Lansează hook-urile nucleu și personalizate pentru a filtra șabloanele,
* apoi încarcă șablonul găsit.
*/
public function load();
}
Comentariile phpDoc ar trebui să fie destul de clare pentru aceste interfețe.
Planul
Acum că avem interfețele și înainte de a scrie clasele concrete, să revizuim fluxul de lucru:
- Mai întâi instanțiem o clasă
Controller
(care implementeazăControllerInterface
) și injectăm (probabil într-un constructor) o instanță a claseiTemplateLoader
(care implementeazăTemplateLoaderInterface
) - Pe hook-ul
init
apelăm metodaControllerInterface::init()
pentru a configura controlerul și pentru a lansa hook-ul pe care codul consumator îl va folosi pentru a adăuga pagini virtuale. - Pe 'do_parse_request' vom apela
ControllerInterface::dispatch()
, și acolo vom verifica toate paginile virtuale adăugate și dacă una dintre ele are același URL ca cererea curentă, o vom afișa; după ce am setat toate variabilele globale nucleu ($wp_query
,$post
). Vom folosi și clasaTemplateLoader
pentru a încărca șablonul corespunzător.
În timpul acestui flux de lucru vom declanșa unele hook-uri nucleu, cum ar fi wp
, template_redirect
, template_include
... pentru a face pluginul mai flexibil și a asigura compatibilitatea cu nucleul și alte pluginuri, sau cel puțin cu un număr bun dintre ele.
Pe lângă fluxul de lucru anterior, vom avea nevoie și de:
- Curățarea hook-urilor și variabilelor globale după ce rulează bucla principală, din nou pentru a îmbunătăți compatibilitatea cu nucleul și codul terților
- Adăugarea unui filtru pe
the_permalink
pentru a face să returneze URL-ul corect al paginii virtuale când este necesar.
Clasele Concrete
Acum putem scrie clasele noastre concrete. Să începem cu clasa pentru pagină (fișierul 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;
}
}
Nu mai mult decât implementarea interfeței.
Acum clasa controler (fișierul 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();
}
}
În esență, clasa creează un obiect SplObjectStorage
în care sunt stocate toate obiectele de pagini adăugate.
Pe 'do_parse_request'
, clasa controler parcurge acest depozit pentru a găsi o potrivire pentru URL-ul curent într-una din paginile adăugate.
Dacă este găsită, clasa face exact ceea ce am planificat: declanșează unele hook-uri, configurează variabilele și încarcă șablonul prin clasa care extinde TemplateLoaderInterface
.
După aceea, pur și simplu exit()
.
Deci, să scriem ultima clasă:
<?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;
}
}
}
Șabloanele stocate în pagina virtuală sunt combinate într-un array cu valorile implicite page.php
și index.php
, înainte de încărcarea șablonului 'template_redirect'
este declanșat, pentru a adăuga flexibilitate și a îmbunătăți compatibilitatea.
După aceea, șablonul găsit trece prin filtrul personalizat 'virtual_page_template'
și filtrul nucleu 'template_include'
: din nou pentru flexibilitate și compatibilitate.
În final, fișierul șablon este încărcat.
Fișierul principal al pluginului
În acest moment, trebuie să scriem fișierul cu antetele pluginului și să-l folosim pentru a adăuga hook-urile care vor face fluxul nostru de lucru să se întâmple:
<?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;
} );
În fișierul real, vom adăuga probabil mai multe antete, cum ar fi link-uri pentru plugin și autor, descriere, licență, etc.
Gist-ul Pluginului
Ok, am terminat cu pluginul nostru. Tot codul poate fi găsit într-un Gist aici.
Adăugarea Paginilor
Pluginul este gata și funcționează, dar nu am adăugat nicio pagină.
Aceasta poate fi făcută în interiorul pluginului în sine, în fișierul functions.php
al temei, în alt plugin, etc.
Adăugarea paginilor este doar o chestiune de:
<?php
add_action( 'gm_virtual_pages', function( $controller ) {
// prima pagină
$controller->addPage( new \GM\VirtualPages\Page( '/custom/page' ) )
->setTitle( 'My First Custom Page' )
->setTemplate( 'custom-page-form.php' );
// a doua pagină
$controller->addPage( new \GM\VirtualPages\Page( '/custom/page/deep' ) )
->setTitle( 'My Second Custom Page' )
->setTemplate( 'custom-page-deep.php' );
} );
Și așa mai departe. Puteți adăuga toate paginile de care aveți nevoie, doar amintiți-vă să folosiți URL-uri relative pentru pagini.
În interiorul fișierului șablon, puteți folosi toate etichetele de șabloane WordPress și puteți scrie tot PHP și HTML de care aveți nevoie.
Obiectul global post este umplut cu date provenite din pagina noastră virtuală. Pagina virtuală în sine poate fi accesată prin variabila $wp_query->virtual_page
.
Pentru a obține URL-ul unei pagini virtuale este la fel de ușor ca transmiterea către home_url()
a aceleiași căi folosite pentru a crea pagina:
$custom_page_url = home_url( '/custom/page' );
Rețineți că în bucla principală din șablonul încărcat, the_permalink()
va returna permalinkul corect către pagina virtuală.
Note despre stiluri / scripturi pentru paginile virtuale
Probabil, atunci când se adaugă pagini virtuale, este de dorit să avem și stiluri/scripturi personalizate încărcate și apoi să folosim doar wp_head()
în șabloanele personalizate.
Aceasta este foarte ușor, deoarece paginile virtuale sunt ușor de recunoscut uitându-ne la variabila $wp_query->virtual_page
și paginile virtuale pot fi distinse una de alta uitându-ne la URL-urile lor.
Doar un exemplu:
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 pentru OP
Transmiterea datelor de la o pagină la alta nu este legată de aceste pagini virtuale, ci este doar o sarcină generică.
Cu toate acestea, dacă aveți un formular în prima pagină și doriți să transmiteți date de acolo către a doua pagină, pur și simplu folosiți URL-ul celei de-a doua pagini în proprietatea action
a formularului.
De exemplu, în fișierul șablon al primei pagini puteți:
<form action="<?php echo home_url( '/custom/page/deep' ); ?>" method="POST">
<input type="text" name="testme">
</form>
și apoi în fișierul șablon al celei de-a doua pagini:
<?php $testme = filter_input( INPUT_POST, 'testme', FILTER_SANITIZE_STRING ); ?>
<h1>Test-Me value form other page is: <?php echo $testme; ?></h1>

Un răspuns uimitor de complet, nu doar pentru problema în sine, ci și pentru crearea unui plugin în stil OOP și nu numai. Cu siguranță ai primit votul meu pozitiv, imaginându-mi mai multe, unul pentru fiecare nivel pe care îl acoperă răspunsul.

Soluție foarte elegantă și directă. Am votat pozitiv și am distribuit pe Twitter.

Codul din Controller este puțin greșit... checkRequest() preia informațiile despre cale de la home_url() care returnează localhost/wordpress. După preg_replace și add_query_arg, acest URL devine /wordpress/pagina-virtuala. Și după tăierea în checkRequest, acest URL devine wordpress/virtual. Acest lucru ar funcționa dacă WordPress ar fi instalat în folderul rădăcină al domeniului. Poți oferi o corecție pentru această problemă, deoarece nu găsesc funcția adecvată care să returneze URL-ul corect? Mulțumesc pentru tot! (Voi accepta răspunsul după ce va deveni perfect :)

Felicitări, răspuns frumos și am nevoie să văd această mulțime de muncă ca soluție gratuită.

@G.M.: În cazul meu WordPress este instalat în .../htdocs/wordpress/ iar site-ul este disponibil la http://localhost/wordpress/. home_url() returnează http://localhost/wordpress iar add_query_arg(array()) returnează /wordpress/virtual-page/. Când comparăm $path și $this->pages->current()->getUrl() tăiat în checkRequest() apare problemă deoarece $path este wordpress/virtual-page
iar url-ul paginii tăiat este virtual-page
.

De necrezut, dar cum naiba am tastat ' în loc de " în preg_replace. Ai dreptate, a fost greșeala mea, îmi cer scuze pentru asta. Am primit o altă eroare după ce am rezolvat prima. Undefined property: WP_Query::$queried_object_id
. Aceasta este probabil legată de Page.php unde setăm atributele postului sau de setupQuery din Controller. Am încă o întrebare despre 'post_author' în Page.php unde este funcția is_user_logged_in(). Am un site unde oricine se poate înregistra și autentifica. Ar putea fi aceasta o amenințare la securitate? Este obligatoriu să avem funcția is_user_logged_in() sau poate fi setată implicit la 0?

Funcția is_user_logged_in()
nu reprezintă o problemă de securitate: nu aloci nicio capacitate utilizatorilor autentificați, iar pagina nici măcar nu există în baza de date. Cu toate acestea, setarea ei întotdeauna la 0 nu va fi o problemă. În ceea ce privește eroarea, am testat codul de multe ori, în diferite configurații (Windows, Linux, PHP 5.4, 5.5)... cu WP debug și plugin-uri de debug pornite. Niciodată nu am întâmpinat probleme. Deci, fie ai modificat ceva în cod, fie problema este legată de un plugin/theme conflictual. În ambele cazuri, nu pot rezolva asta și sper că vei înțelege că cred că am petrecut deja prea mult timp pe acest răspuns. @user1257255

Am înțeles. Mulțumesc foarte mult pentru un răspuns atât de detaliat și pentru tot restul! :)

Hei @gmazzap dacă trec un parametru de query, ruta cu addPage nu funcționează

O soluție mult mai simplă este adăugarea automată a paginii dacă aceasta nu există. Putem folosi o cheie meta personalizată pentru a verifica existența acesteia. Putem face un callback în filtrul the_content
dacă ID-ul curent este același cu ID-ul paginii.
$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);
});
Totuși, există o problemă aici. De exemplu, protecția prin parolă nu funcționează cu această abordare. Este nevoie de efort pentru a proteja corect aceste pagini împotriva accesului neautorizat la citire sau scriere. Am preferat să folosesc coduri scurte în loc să utilizez acest filtru, deși îmi pot imagina un scenariu în care acesta ar fi util.

Am folosit odată o soluție descrisă aici: http://scott.sherrillmix.com/blog/blogger/creating-a-better-fake-post-with-a-wordpress-plugin/
De fapt, când am folosit-o, am extins soluția astfel încât să pot înregistra mai multe pagini simultan (restul codului este +/- similar cu soluția la care mă refer în paragraful anterior).
Totuși, soluția necesită să ai permalink-uri prietenoase activate...
<?php
class FakePages {
public function __construct() {
add_filter( 'the_posts', array( $this, 'fake_pages' ) );
}
/**
* Înregistrează intern paginile pe care vrem să le simulăm. Cheia array-ului este slug-ul sub care va fi disponibilă în frontend
* @return mixed
*/
private static function get_fake_pages() {
//http://example.com/fakepage1
$fake_pages['fakepage1'] = array(
'title' => 'Pagina Falsă 1',
'content' => 'Acesta este conținutul paginii false 1'
);
//http://example.com/fakepage2
$fake_pages['fakepage2'] = array(
'title' => 'Pagina Falsă 2',
'content' => 'Acesta este conținutul paginii false 2'
);
return $fake_pages;
}
/**
* Simulează rezultatul funcției 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;
//Permalink-urile mai lungi pot să nu se potrivească cu slug-ul postării false și să cauzeze o eroare 404, așa că prindem eroarea aici
unset( $wp_query->query["error"] );
$wp_query->query_vars["error"] = "";
$wp_query->is_404 = false;
}
return $posts;
}
/**
* Creează o pagină falsă virtuală
*
* @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();
