Niestandardowe strony z wtyczką

13

Tworzę wtyczkę, w której chciałbym włączyć niestandardowe strony. W moim przypadku strona niestandardowa zawierałaby formularz podobny do formularza kontaktowego (nie dosłownie). Gdy użytkownik wypełni ten formularz i wyśle ​​go, powinien być następny krok, który będzie wymagał więcej informacji. Powiedzmy, że pierwsza strona z formularzem będzie znajdować się w www.domain.tld/custom-page/i po pomyślnym przesłaniu formularza, użytkownik powinien zostać przekierowany do www.domain.tld/custom-page/second. Szablon z elementami HTML i kodem PHP powinien być również niestandardowy.

Myślę, że część problemu jest możliwa do osiągnięcia dzięki niestandardowym przepisywaniu adresów URL, ale pozostałe części nie są mi obecnie znane. Naprawdę nie wiem, gdzie powinienem zacząć szukać i jak poprawnie nazwać ten problem. Każda pomoc byłaby bardzo mile widziana.

użytkownik1257255
źródło
Czy chcesz, aby te strony były przechowywane w WordPress, czy „wirtualne”?
Welcher
Musisz użyć interfejsu API do przepisywania. To nie powinno być zbyt trudne. Pamiętaj, aby opublikować dane na drugiej stronie i wszystko powinno być w porządku.
setterGetter
@Welcher: Te strony to nie to samo, co oferty WordPress na pulpicie nawigacyjnym. Powinny po prostu zapisywać dane w bazie danych, ale to nie jest problem. @ .setterGetter: Czy masz jakiś przykład, jak przekazywać dane z pierwszej strony na drugą i gdzie (działanie?), aby dołączyć plik PHP pokazujący formularz?
user1257255
Czy rozważałeś użycie formularza pojedynczej strony z wieloma slajdami (javascript i / lub css) pól wejściowych?
birgire

Odpowiedzi:

57

Podczas odwiedzania strony frontonu WordPress wysyła zapytanie do bazy danych, a jeśli twoja strona nie istnieje w bazie danych, zapytanie to nie jest potrzebne i jest tylko marnotrawstwem zasobów.

Na szczęście WordPress oferuje sposób na obsługę żądań interfejsu użytkownika w niestandardowy sposób. Odbywa się to dzięki 'do_parse_request'filtrowi.

Wracając falsedo tego haka, będziesz mógł powstrzymać WordPressa od przetwarzania żądań i zrobić to na swój własny sposób.

To powiedziawszy, chcę udostępnić sposób na zbudowanie prostej wtyczki OOP, która może obsługiwać wirtualne strony w łatwy w użyciu (i ponownym użyciu) sposób.

Czego potrzebujemy

  • Klasa dla wirtualnych obiektów strony
  • Klasa kontrolera, która będzie sprawdzać żądanie, a jeśli dotyczy ono strony wirtualnej, pokaż ją przy użyciu odpowiedniego szablonu
  • Klasa do ładowania szablonu
  • Główne pliki wtyczek, aby dodać haki, które sprawią, że wszystko zadziała

Interfejsy

Przed budowaniem klas napiszmy interfejsy dla 3 wyżej wymienionych obiektów.

Najpierw interfejs strony (plik 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();
}

Większość metod to tylko osoby pobierające i ustawiające, bez potrzeby wyjaśniania. Do uzyskania WP_Postobiektu ze strony wirtualnej należy użyć ostatniej metody .

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

oraz interfejs modułu ładującego szablony (plik 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();
}

Komentarze phpDoc powinny być dość jasne dla tych interfejsów.

Plan

Teraz, gdy mamy interfejsy i przed napisaniem konkretnych klas, przejrzyjmy nasz przepływ pracy:

  • Najpierw tworzymy instancję Controllerklasy (implementując ControllerInterface) i wstrzykujemy (prawdopodobnie w konstruktorze) instancję TemplateLoaderklasy (implementując TemplateLoaderInterface)
  • Podczas initprzechwytywania wywołujemy ControllerInterface::init()metodę ustawiania kontrolera i wystrzeliwania zaczepu, którego kod konsumenta użyje do dodania stron wirtualnych.
  • Na „do_parse_request” zadzwonimy ControllerInterface::dispatch()i tam sprawdzimy wszystkie dodane strony wirtualne i jeśli jedna z nich ma ten sam adres URL bieżącego żądania, wyświetl go; po ustawieniu wszystkich podstawowych zmiennych globalnych ( $wp_query, $post). Użyjemy również TemplateLoaderklasy, aby załadować odpowiedni szablon.

Podczas tego przepływu pracy będziemy wyzwalać pewne haczyki rdzeniowe, jak wp, template_redirect,template_include ..., aby plugin bardziej elastyczne i zapewniają zgodność z rdzenia i innych wtyczek, albo przynajmniej z kilku dobrych z nich.

Oprócz poprzedniego przepływu pracy będziemy również musieli:

  • Wyczyść haki i zmienne globalne po uruchomieniu pętli głównej, ponownie, aby poprawić zgodność z kodem podstawowym i zewnętrznym
  • Dodaj filtr, the_permalinkaby w razie potrzeby zwracał właściwy adres URL strony wirtualnej.

Klasy betonowe

Teraz możemy zakodować nasze konkretne klasy. Zacznijmy od klasy strony (pliku 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;
    }
}

Nic więcej niż implementacja interfejsu.

Teraz klasa kontrolera (plik 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();
    }
}

Zasadniczo klasa tworzy SplObjectStorageobiekt, w którym przechowywane są wszystkie dodane obiekty stron.

Na 'do_parse_request' klasa kontrolera zapętla tę pamięć, aby znaleźć dopasowanie do bieżącego adresu URL na jednej z dodanych stron.

Jeśli zostanie znaleziony, klasa robi dokładnie to, co zaplanowaliśmy: uruchamia niektóre przechwyty, ustawia zmienne i ładuje szablon poprzez rozszerzenie klasy TemplateLoaderInterface. Potem tylko exit().

Napiszmy więc ostatnią klasę:

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

Szablony przechowywane na stronie wirtualnej są scalane w tablicę z ustawieniami domyślnymi page.phpi index.phpprzed uruchomieniem szablonu 'template_redirect'są uruchamiane, aby zwiększyć elastyczność i poprawić zgodność.

Następnie znaleziony szablon przechodzi przez filtry niestandardowe 'virtual_page_template'i podstawowe 'template_include': ponownie dla elastyczności i kompatybilności.

Wreszcie plik szablonu został właśnie załadowany.

Główny plik wtyczki

W tym momencie musimy napisać plik z nagłówkami wtyczek i użyć go, aby dodać haki, które spowodują nasz przepływ pracy:

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

W prawdziwym pliku prawdopodobnie dodamy więcej nagłówków, takich jak wtyczka i linki autora, opis, licencja itp.

Plugin Gist

Ok, skończyliśmy z naszą wtyczką. Cały kod można znaleźć w Gist tutaj .

Dodawanie stron

Wtyczka jest gotowa i działa, ale nie dodaliśmy żadnych stron.

Można to zrobić w samej wtyczce, w temacie functions.php, w innej wtyczce itp.

Dodawanie stron to tylko kwestia:

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

} );

I tak dalej. Możesz dodać wszystkie potrzebne strony, pamiętaj tylko o stosowaniu względnych adresów URL dla stron.

W pliku szablonu możesz używać wszystkich tagów szablonów WordPress, a także pisać wszystkie potrzebne PHP i HTML.

Globalny obiekt postu jest wypełniony danymi pochodzącymi z naszej wirtualnej strony. Do samej wirtualnej strony można uzyskać dostęp poprzez $wp_query->virtual_pagezmienną.

Aby uzyskać adres URL strony wirtualnej, wystarczy przejść do home_url()tej samej ścieżki, którą użyto do utworzenia strony:

$custom_page_url = home_url( '/custom/page' );

Zauważ, że w głównej pętli w załadowanym szablonie the_permalink()zwróci poprawny bezpośredni link do strony wirtualnej.

Uwagi na temat stylów / skryptów dla stron wirtualnych

Prawdopodobnie po dodaniu stron wirtualnych pożądane jest także umieszczenie w kolejce niestandardowych stylów / skryptów, a następnie użycie ich wp_head()w niestandardowych szablonach.

To bardzo łatwe, ponieważ strony wirtualne są łatwo rozpoznawalne na podstawie $wp_query->virtual_pagezmiennych, a strony wirtualne można odróżnić od siebie na podstawie ich adresów URL.

Tylko przykład:

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

} );

Uwagi do OP

Przekazywanie danych ze strony na inną nie jest powiązane z tymi wirtualnymi stronami, ale jest jedynie ogólnym zadaniem.

Jeśli jednak masz formularz na pierwszej stronie i chcesz przenieść stamtąd dane na drugą stronę, po prostu użyj adresu URL drugiej strony we actionwłaściwości formularza .

Np. W pliku szablonu pierwszej strony możesz:

<form action="<?php echo home_url( '/custom/page/deep' ); ?>" method="POST">
    <input type="text" name="testme">
</form>

a następnie w pliku szablonu drugiej strony:

<?php $testme = filter_input( INPUT_POST, 'testme', FILTER_SANITIZE_STRING ); ?>
<h1>Test-Me value form other page is: <?php echo $testme; ?></h1>
gmazzap
źródło
9
Niesamowita kompleksowa odpowiedź, nie tylko na temat samego problemu, ale także na stworzenie wtyczki w stylu OOP i nie tylko. Na pewno masz moje poparcie, wyobraź sobie więcej, jeden na każdy poziom objęty odpowiedzią.
Nicolai
2
Bardzo płynne i proste rozwiązanie. Zaktualizowano, opublikowano na Twitterze.
kaiser
Kod w kontrolerze jest trochę niepoprawny ... checkRequest () pobiera informacje o ścieżce z home_url (), która zwraca localhost / wordpress. Po preg_replace i add_query_arg ten adres URL zmienia się na / wordpress / virtual-page. A po sprawdzeniu wykończenia Zapytaj ten adres URL to wordpress / virtual. Działałoby to, gdyby wordpress został zainstalowany w folderze głównym domeny. Czy możesz podać rozwiązanie tego problemu, ponieważ nie mogę znaleźć odpowiedniej funkcji, która zwróciłaby właściwy adres URL. Dziękuję Ci za wszystko! (Przyjmę odpowiedź, gdy stanie się idealna :)
user1257255
2
Gratulacje, miła odpowiedź i muszę zobaczyć tę pracę jako bezpłatne rozwiązanie.
bueltge
@GM: W moim przypadku WordPress jest zainstalowany w ... / htdocs / wordpress /, a strona jest dostępna na localhost / wordpress . home_url () zwraca localhost / wordpress i add_query_arg (array ()) zwraca / wordpress / virtual-page /. Kiedy porównujemy $ path i przycięte $ this-> pages-> current () -> getUrl () w checkRequest () jest problem, ponieważ $ path jest, wordpress/virtual-pagea URL przyciętej strony to virtual-page.
user1257255
0

Kiedyś użyłem opisanego tutaj rozwiązania: http://scott.sherrillmix.com/blog/blogger/creating-a-better-fake-post-with-a-wordpress-plugin/

Właściwie, kiedy go używałem, rozszerzam rozwiązanie w taki sposób, że mogę zarejestrować więcej niż jedną stronę na raz (reszta kodu jest +/- podobna do rozwiązania, które łączę z powyższego akapitu).

Rozwiązanie wymaga, aby dozwolone były permalinki, które ...

<?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();
david.binda
źródło
Co z niestandardowym szablonem, w którym mogę umieścić formularz?
user1257255
contentw tablicy podczas rejestracji fałszywa strona jest wyświetlana w jej treści - może zawierać zarówno HTML, jak i prosty tekst, a nawet krótki kod.
david.binda