问题描述
我正在开发一些我想要启用自定义页面的插件。在我的情况下,一些自定义页面将包含一个形式,如联系表单 (不是从字面上) 。当用户填写此表单并发送时,应该有下一步需要更多信息。让我们说,表单的第一页将位于 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 辅助翻译的英文资料结果。如果您对结果不满意,可以加入我们改善翻译效果:薇晓朵技术论坛。