带插件的自定义页面


13

我正在开发一些插件,用于启用自定义页面。在我的情况下,某些自定义页面将包含诸如联系表单之类的表单(并非字面意思)。当用户填写并发送此表格时,应该有需要更多信息的下一步。假设具有表单的第一页将位于www.domain.tld/custom-page/成功提交表单后的位置,然后应将用户重定向到www.domain.tld/custom-page/second。带有HTML元素和PHP代码的模板也应该是自定义的。

我认为使用自定义URL重写可以解决部分问题,但是我目前还不了解其他部分。我真的不知道该从哪里开始寻找该问题的正确命名。任何帮助将非常感激。


您是否希望这些页面以WordPress或“虚拟”存储?
Welcher 2014年

您必须使用重写api。这应该不太困难。确保将数据发布到第二页,并且应该没问题。
setterGetter 2014年

@Welcher:这些页面与WordPress在仪表板中提供的页面不同。他们应该只将数据保存到数据库中,但这不是问题。@ .setterGetter:您有任何示例如何将数据从第一页传递到第二页,以及在哪里(动作?)包括显示表单的PHP文件?
user1257255

您是否考虑过使用具有多个输入字段的幻灯片(javascript和/或css)的单页表单?
birgire

Answers:


57

当您访问前端页面时,WordPress将查询数据库,如果您的页面不存在于数据库中,则不需要该查询,这只是浪费资源。

幸运的是,WordPress提供了一种以自定义方式处理前端请求的方法。这要归功于'do_parse_request'过滤器。

返回false该钩子,您将能够停止WordPress处理请求并以自己的自定义方式进行处理。

也就是说,我想分享一种构建简单的OOP插件的方法,该插件可以以易于使用(和重用)的方式处理虚拟页面。

我们需要的

  • 虚拟页面对象的类
  • 控制器类,它将查看请求,如果它是针对虚拟页面的,则使用适当的模板显示该请求
  • 模板加载类
  • 主插件文件添加了使一切正常的钩子

介面

在构建类之前,让我们为上面列出的3个对象编写接口。

首先是页面界面(file PageInterface.php):

<?php
namespace GM\VirtualPages;

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();
}

大多数方法只是获取器和设置器,无需解释。应该使用最后一种方法WP_Post从虚拟页面获取对象。

控制器接口(文件ControllerInterface.php):

<?php
namespace GM\VirtualPages;

interface ControllerInterface {

    /**
     * Init the controller, fires the hook that allows consumer to add pages
     */
    function init();

    /**
     * Register a page object in the controller
     *
     * @param  \GM\VirtualPages\Page $page
     * @return \GM\VirtualPages\Page
     */
    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 ); 
}

和模板加载器接口(file TemplateLoaderInterface.php):

<?php
namespace GM\VirtualPages;

interface TemplateLoaderInterface {

    /**
     * Setup loader for a page objects
     *
     * @param \GM\VirtualPagesPageInterface $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)。我们还将使用TemplateLoaderclass加载正确的模板。

在这个工作流程,我们会引发一些核心钩子,像wptemplate_redirecttemplate_include...使插件更加灵活,确保核心和其他插件,或至少与他们的良好的相容性数量。

除了以前的工作流程之外,我们还需要:

  • 在主循环运行后清理钩子和全局变量,以再次提高与核心代码和第三方代码的兼容性
  • 添加一个过滤器the_permalink,使其在需要时返回正确的虚拟页面URL。

具体课程

现在我们可以编写具体的类了。让我们从页面类(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;
    }
}

只不过是实现接口。

现在,控制器类(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();
    }
}

本质上,该类创建一个SplObjectStorage对象,所有添加的页面对象都存储在该对象中。

在上'do_parse_request',控制器类循环该存储,以在添加的页面之一中找到当前URL的匹配项。

如果找到该类,则该类将完全按照我们的计划进行操作:触发一些钩子,设置变量,并通过extends类加载模板TemplateLoaderInterface。之后,只需exit()

因此,让我们编写最后一个类:

<?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;
        }
    }
}

在激发加载模板之前,将虚拟页面中存储的模板与默认值page.php和合并到一个数组中,以增加灵活性并提高兼容性。index.php'template_redirect'

之后,找到的模板将通过自定义过滤器'virtual_page_template'和核心'template_include'过滤器:再次是为了灵活性和兼容性。

最后,模板文件刚刚加载。

主插件文件

此时,我们需要编写带有插件头的文件,并使用它添加将使我们的工作流发生的钩子:

<?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;
} );

在实际文件中,我们可能会添加更多标头,例如插件和作者链接,描述,许可证等。

插件要点

好的,我们完成了我们的插件。所有代码都可以在这里找到。

添加页面

插件已准备就绪且可以正常使用,但我们尚未添加任何页面。

这可以在插件本身内部,主题内部functions.php,其他插件等内部完成。

添加页面只是以下问题:

<?php
add_action( 'gm_virtual_pages', function( $controller ) {

    // first page
    $controller->addPage( new \GM\VirtualPages\Page( '/custom/page' ) )
        ->setTitle( 'My First Custom Page' )
        ->setTemplate( 'custom-page-form.php' );

    // second page
    $controller->addPage( new \GM\VirtualPages\Page( '/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 \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;
        }
    }

} );

OP注意事项

将数据从一个页面传递到另一个页面与这些虚拟页面无关,而只是一个常规任务。

但是,如果您在第一页中有一个表单,并且想要将数据从那里传递到第二页,只需在form 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>

9
令人惊奇的综合答案,不仅涉及问题本身,而且涉及创建OOP风格的插件等等。您肯定会得到我的支持,想像更多,答案涵盖的每个级别都有一个。
Nicolai 2014年

2
非常光滑和直接的解决方案。更新,发推文。
kaiser 2014年

Controller中的代码有点错误... checkRequest()从home_url()获取路径信息,该信息返回localhost / wordpress。在preg_replace和add_query_arg之后,此url变为/ wordpress / virtual-page。在checkRequest中进行修剪后,此网址为wordpress / virtual。如果将wordpress安装在domain的根文件夹中,则此方法有效。您能为这个问题提供解决方法吗,因为我找不到合适的函数来返回正确的URL。谢谢你为我做的一切!(在它变得完美之后,我将接受答案:)
user1257255

2
恭喜,答案很好,我需要将大量工作视为免费的解决方案。
bueltge 2014年

@GM:就我而言,WordPress安装在... / htdocs / wordpress /中,并且该站点位于localhost / wordpress上。home_url()返回localhost / wordpress,而add_query_arg(array())返回/ wordpress / virtual-page /。当我们比较$ path并在checkRequest()中修剪$ this-> pages-> current()-> getUrl()时出现问题,因为$ path是wordpress/virtual-page并且页面的修剪url是virtual-page
user1257255 2014年

0

我曾经使用这里描述的解决方案:http : //scott.sherrillmix.com/blog/blogger/creating-a-better-fake-post-with-a-wordpress-plugin/

实际上,当我使用它时,我以一种可以一次注册多个页面的方式扩展了解决方案(代码的其余部分+/-与我在上一段中链接的解决方案类似)。

解决方案要求您拥有允许的良好永久链接。

<?php

class FakePages {

    public function __construct() {
        add_filter( 'the_posts', array( $this, 'fake_pages' ) );
    }

    /**
     * Internally registers pages we want to fake. Array key is the slug under which it is being available from the frontend
     * @return mixed
     */
    private static function get_fake_pages() {
        //http://example.com/fakepage1
        $fake_pages['fakepage1'] = array(
            'title'   => 'Fake Page 1',
            'content' => 'This is a content of fake page 1'
        );
        //http://example.com/fakepage2
        $fake_pages['fakepage2'] = array(
            'title'   => 'Fake Page 2',
            'content' => 'This is a content of fake page 2'
        );

        return $fake_pages;
    }

    /**
     * Fakes get posts result
     *
     * @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;
            //Longer permalink structures may not match the fake post slug and cause a 404 error so we catch the error here
            unset( $wp_query->query["error"] );
            $wp_query->query_vars["error"] = "";
            $wp_query->is_404              = false;
        }

        return $posts;
    }

    /**
     * Creates virtual fake page
     *
     * @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();

可以在其中放置表单的自定义模板呢?
user1257255

content在注册时,数组中的假页面将显示在页面的主体中-它可以包含HTML以及简单文本甚至是简码。
david.binda 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.