問題描述
我正在開發一些我想要啟用自定義頁面的外掛。在我的情況下,一些自定義頁面將包含一個形式,如聯絡表單 (不是從字面上) 。當使用者填寫此表單併傳送時,應該有下一步需要更多資訊。讓我們說,表單的第一頁將位於 www.domain.tld/custom-page/,成功提交表單後,使用者應該被重定向到 www.domain.tld/custom-page/second 。具有 HTML 元素和 PHP 程式碼的模板也應該是自定義的。
我認為問題的一部分可能透過自定義 URL 重寫來實現,但是其他部分目前是我未知的。我真的不知道我應該從哪裡開始尋找,那個問題的正確命名是什麼?任何幫助將非常感激。
最佳解決方案
當您訪問前端頁面時,WordPress 將查詢資料庫,如果您的頁面不存在於資料庫中,則不需要該查詢,並且只是浪費資源。
幸運的是,WordPress 提供了一種以自定義方式處理前端請求的方法。這是由於'do_parse_request'濾波器完成的。
在該鉤子上返回 false,您將能夠停止 WordPress 處理請求,並以您自己的自定義方式執行。
也就是說,我想分享一種方法來構建一個簡單的 OOP 外掛,可以透過易於使用 (和 re-use) 來處理虛擬頁面。
我們需要的
-
虛擬頁面物件的類
-
一個控制器類,將檢視一個請求,如果它是一個虛擬頁面,使用適當的模板顯示它
-
一個用於模板載入的類
-
主要外掛檔案新增鉤子,將使一切正常
Interfaces
在構建類之前,讓我們編寫上面列出的 3 個物件的介面。
首先是頁面介面 (檔案 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();
}
大多數方法只是 getter 和 setter,不需要解釋。應該使用最後一個方法從虛擬頁面獲取 WP_Post 物件。
控制器介面 (檔案 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 );
}
和模板載入程式介面 (檔案 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 註釋應該很清楚。
計劃
現在我們有介面,在編寫具體的課程之前,我們來看看我們的工作流程:
-
首先,我們例項化一個
Controller類 (實現ControllerInterface) 並注入 (可能在一個建構函式中) 一個TemplateLoader類的例項 (實現TemplateLoaderInterface) -
在
init鉤子上,我們呼叫ControllerInterface::init()方法來設定控制器並觸發消費者程式碼將用來新增虛擬頁面的鉤子。 -
在‘do_parse_request’ 上,我們將呼叫
ControllerInterface::dispatch(),我們將檢查所有新增的虛擬頁面,如果其中一個具有相同的當前請求的 URL,則顯示它; 在設定了所有核心全域性變數 ($wp_query,$post) 之後。我們還將使用TemplateLoader類載入正確的模板。
在這個工作流程中,我們將觸發一些核心鉤子,如 wp,template_redirect,template_include … 使外掛更加靈活,並確保與核心和其他外掛的相容性,或者至少有很多的外掛。
除了以前的工作流程,我們還需要:
-
在主迴圈執行之後,清除鉤子和全域性變數,再次提高與核心和第三方程式碼的相容性
-
在
the_permalink上新增一個過濾器,使其在需要時返回正確的虛擬頁面 URL 。
混凝土類
現在我們可以編寫我們的具體類。我們從頁面類開始 (檔案 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;
}
}
沒有什麼比實現介面。
現在控制器類 (檔案 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();
}
}
本質上,該類建立一個 SplObjectStorage 物件,其中儲存所有新增的頁面物件。
在'do_parse_request'上,控制器類迴圈此儲存,以查詢其中一個新增的頁面中當前 URL 的匹配。
如果找到,該類完全符合我們的計劃:觸發一些鉤子,設定變數,並透過擴充套件 TemplateLoaderInterface 的類載入模板。之後,只是 exit()。
所以我們來寫最後一個類:
<?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;
}
}
}
儲存在虛擬頁面中的模板將合併到預設值為 page.php 和 index.php 的陣列中,然後再載入模板'template_redirect'才能啟用,以增加靈活性並提高相容性。
之後,找到的模板透過自定義'virtual_page_template'和核心'template_include'過濾器:再次為了靈活性和相容性。
最後,模板檔案剛剛載入。
主要外掛檔案
在這一點上,我們需要使用外掛頭編寫檔案,並使用它來新增將使我們的工作流發生的鉤子:
<?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;
} );
在真實的檔案中,我們可能會新增更多的標題,如外掛和作者連結,描述,許可證等。
外掛 Gist
好的,我們完成了我們的外掛。所有的程式碼都可以在 Gist here 中找到。
新增頁面
外掛已準備就緒,但我們尚未新增任何頁面。
這可以在外掛本身內部,主題 functions.php,另一個外掛等。
新增頁面只是一個問題:
<?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' );
} );
等等。您可以新增所需的所有頁面,只需記住使用頁面的相對 URL 。
在模板檔案中,您可以使用所有 WordPress 模板標籤,並且可以編寫所需的所有 PHP 和 HTML 。
全域性後置物件填充有來自我們的虛擬頁面的資料。虛擬頁面本身可以透過 $wp_query->virtual_page 變數訪問。
獲取虛擬頁面的 URL 與傳遞給 home_url()一樣簡單,用於建立頁面的路徑:
$custom_page_url = home_url( '/custom/page' );
請注意,在載入模板的主迴圈中,the_permalink()將返回正確的永久連結到虛擬頁面。
關於虛擬頁面的樣式/指令碼的註釋
可能當新增虛擬頁面時,還需要自定義樣式/指令碼排隊,然後在自定義模板中使用 wp_head()。
這很簡單,因為虛擬頁面很容易被識別,可以看到 $wp_query->virtual_page 變數,虛擬頁面可以從另一個角度看待他們的 URL 。
只是一個例子:
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;
}
}
} );
OP 說明
將資料從頁面傳遞到另一個與這些虛擬頁面無關,但只是一個通用的任務。
但是,如果您在第一頁中有表單,並希望將資料從那裡傳遞到第二頁,只需使用 action 屬性中的第二頁的 URL 。
例如。在第一頁模板檔案中,您可以:
<form action="<?php echo home_url( '/custom/page/deep' ); ?>" method="POST">
<input type="text" name="testme">
</form>
然後在第二頁模板檔案中:
<?php $testme = filter_input( INPUT_POST, 'testme', FILTER_SANITIZE_STRING ); ?>
<h1>Test-Me value form other page is: <?php echo $testme; ?></h1>
參考文獻
注:本文內容整合自 Google/Baidu/Bing 輔助翻譯的英文資料結果。如果您對結果不滿意,可以加入我們改善翻譯效果:薇曉朵技術論壇。