問題描述

我正在開發一些我想要啓用自定義頁面的插件。在我的情況下,一些自定義頁面將包含一個形式,如聯繫表單 (不是從字面上) 。當用户填寫此表單併發送時,應該有下一步需要更多信息。讓我們説,表單的第一頁將位於 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 類加載正確的模板。

在這個工作流程中,我們將觸發一些核心鈎子,如 wptemplate_redirecttemplate_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.phpindex.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 輔助翻譯的英文資料結果。如果您對結果不滿意,可以加入我們改善翻譯效果:薇曉朵技術論壇。