Zapobiegaj comments_template (), aby załadować comments.php


9

Tworzę motyw WordPress za pomocą silnika szablonów. Chcę, aby mój kod był jak najbardziej zgodny z podstawową funkcjonalnością WP.

Najpierw jakiś kontekst

Moim pierwszym problemem było znalezienie sposobu rozwiązania szablonu, zaczynając od zapytania WP. Rozwiązałem ten przy użyciu mojej biblioteki, Brain \ Hierarchy .

Jeśli chodzi o get_template_part()inne funkcje ładujące częściowe get_header(), takie jak get_footer()i podobne, dość łatwo było napisać opakowanie do częściowej funkcjonalności silnika szablonu.

Problem

Mój problem polega teraz na tym, jak załadować szablon komentarzy.

Funkcja WordPress comments_template()to ~ 200 linii funkcja, która robi wiele rzeczy, które chcę zrobić również dla maksymalnej kompatybilności rdzenia.

Jednak jak tylko zadzwonię comments_template(), plik ma postać required, jest to pierwszy z następujących elementów:

  • plik w stałej COMMENTS_TEMPLATE, jeśli jest zdefiniowany
  • comments.php w folderze motywu, jeśli został znaleziony
  • /theme-compat/comments.php w WP zawiera folder jako rezerwowy w ostateczności

Krótko mówiąc, nie ma sposobu, aby funkcja nie załadowała pliku PHP, co nie jest dla mnie pożądane, ponieważ muszę renderować moje szablony, a nie po prostu ich używać require.

Aktualne rozwiązanie

W tej chwili wysyłam pusty comments.phpplik i korzystam z 'comments_template'haka filtru, aby wiedzieć, który szablon WordPress chce załadować, i użyć funkcji z mojego silnika szablonów, aby załadować szablon.

Coś takiego:

function engineCommentsTemplate($myEngine) {

    $toLoad = null; // this will hold the template path

    $tmplGetter = function($tmpl) use(&$toLoad) {
       $toLoad = $tmpl;

       return $tmpl;
    };

    // late priority to allow filters attached here to do their job
    add_filter('comments_template', $tmplGetter, PHP_INT_MAX);

    // this will load an empty comments.php file I ship in my theme
    comments_template();

    remove_filter('comments_template', $tmplGetter, PHP_INT_MAX);

    if (is_file($toLoad) && is_readable($toLoad)) {
       return $myEngine->render($toLoad);
    }

    return '';    
}

Pytanie

Działa, jest kompatybilny z rdzeniem, ale ... czy istnieje sposób, aby działał bez konieczności wysyłania pustego comments.php?

Ponieważ mi się nie podoba.

Odpowiedzi:


4

Nie jestem pewien, czy poniższe rozwiązanie jest lepsze niż rozwiązanie w OP, powiedzmy, że jest alternatywnym, prawdopodobnie bardziej hackerskim rozwiązaniem.

Myślę, że możesz użyć wyjątku PHP, aby zatrzymać wykonywanie WordPressa po 'comments_template'zastosowaniu filtru.

Możesz użyć niestandardowej klasy wyjątku jako DTO do przeniesienia szablonu.

Oto projekt wyjątku:

class CommentsTemplateException extends \Exception {

   protected $template;

   public static function forTemplate($template) {
     $instance = new static();
     $instance->template = $template;

     return $instance;
   }

   public function template() {
      return $this->template;
   }
}

Po udostępnieniu tej klasy wyjątków funkcja staje się:

function engineCommentsTemplate($myEngine) {

    $filter = function($template) {
       throw CommentsTemplateException::forTemplate($template);
    };  

    try {
       add_filter('comments_template', $filter, PHP_INT_MAX); 
       // this will throw the excption that makes `catch` block run
       comments_template();
    } catch(CommentsTemplateException $e) {
       return $myEngine->render($e->template());
    } finally {
       remove_filter('comments_template', $filter, PHP_INT_MAX);
    }
}

finallyBlok wymaga PHP 5.5+.

Działa w ten sam sposób i nie wymaga pustego szablonu.


4

Walczyłem z tym już wcześniej, a moim rozwiązaniem było - może on wybić wymagający pliku, o ile nic nie robi .

Oto odpowiedni kod z mojego projektu szablonów Meadow :

public function comments_template( \Twig_Environment $env, $context, $file = 'comments.twig', $separate_comments = false ) {

    try {
        $env->loadTemplate( $file );
    } catch ( \Twig_Error_Loader $e ) {
        ob_start();
        comments_template( '/comments.php', $separate_comments );
        return ob_get_clean();
    }

    add_filter( 'comments_template', array( $this, 'return_blank_template' ) );
    comments_template( '/comments.php', $separate_comments );
    remove_filter( 'comments_template', array( $this, 'return_blank_template' ) );

    return twig_include( $env, $context, $file );
}

public function return_blank_template() {

    return __DIR__ . '/blank.php';
}

Pozbyłem comments_template()się ruchów, aby skonfigurować globały i tym podobne, ale nakarmiłem go pustym plikiem PHP requirei przejdę do mojego aktualnego szablonu Twig w celu uzyskania wyników.

Zauważ, że wymaga to możliwości przechwycenia początkowego comments_template()wywołania, co mogę zrobić, ponieważ mój szablon Twig wywołuje abstrakcję pośrednią zamiast faktycznej funkcji PHP.

Chociaż wciąż muszę wysłać dla niego pusty plik, robię to w bibliotece i implementacja motywu wcale nie musi o to dbać.


Dzięki, dziękuję. Widziałem już twoje podejście, odkąd wcześniej użyłem Meadow. Nie podobało mi się tutaj to, że i tak trzeba wysłać pusty szablon. Co więcej, przerywa to każdą próbę użycia comments_templatefiltru lub COMMENTS_TEMPLATEstałej w celu dostosowania szablonu. Co nie jest kluczowe, ale, jak powiedziałem, chciałem zachować jak największą zgodność z rdzeniem.
gmazzap

@gmazzap hmmm ... nie ma powodu, dla którego nie mogłem dodać obsługi filtru i stałej w moim opakowaniu, ale przechodzi w mikrozarządzanie.
Rarst

3

Rozwiązanie: Użyj pliku tymczasowego - z unikalną nazwą pliku

Po wielu skokach i pełzaniu w najbrudniejsze zakątki PHP sformułowałem pytanie w następujący sposób:

Jak można oszukać PHP w powrocie TRUEdo file_exists( $file )?

jak w samym rdzeniu kod

file_exists( apply_filters( 'comments_template', $template ) )

Następnie pytanie zostało rozwiązane szybciej:

$template = tempnam( __DIR__, '' );

i to wszystko. Może lepiej byłoby użyć wp_upload_dir()zamiast tego:

$uploads = wp_upload_dir();
$template = tempname( $uploads['basedir'], '' );

Inną opcją może być użycie, get_temp_dir()które zawija WP_TEMP_DIR. Wskazówka: Dziwnie wraca do tego, /tmp/aby pliki nie były zachowywane między restartami, co /var/tmp/byłoby. Na końcu można wykonać proste porównanie łańcucha i sprawdzić wartość zwracaną, a następnie naprawić to w razie potrzeby - czego nie ma w tym przypadku:

$template = tempname( get_temp_dir(), '' )

Teraz szybko przetestuj, czy w pliku tymczasowym bez zawartości nie wystąpiły błędy:

<?php
error_reporting( E_ALL );
$template = tempnam( __DIR__, '' );
var_dump( $template );
require $template;

I: Brak błędów → działa.

EDYCJA: Jak zauważył @toscho w komentarzach, jest jeszcze lepszy sposób:

$template = tempnam( trailingslashit( untrailingslashit( sys_get_temp_dir() ) ), 'comments.php' );

Uwaga: Zgodnie z nutą użytkowników na docs php.net , że sys_get_temp_dir()różni się zachowanie między systemami. Dlatego wynik usuwa ukośnik końcowy, a następnie dodaje ponownie. Ponieważ podstawowy błąd nr 22267 został naprawiony, teraz również powinien działać na serwerach Win / IIS.

Twoja funkcja refaktoryzacji (nie testowana):

function engineCommentsTemplate( $engine )
{
    $template = null;

    $tmplGetter = function( $original ) use( &$template ) {
        $template = $original;
        return tempnam( 
            trailingslashit( untrailingslashit( sys_get_temp_dir() ) ),
            'comments.php'
        );
    };

    add_filter( 'comments_template', $tmplGetter, PHP_INT_MAX );

    comments_template();

    remove_filter( 'comments_template', $tmplGetter, PHP_INT_MAX );

    if ( is_file( $template ) && is_readable( $template ) ) {
        return $engine->render( $template );
    }

    return '';
}

Bonus nr 1: tmpfile()powróci NULL. Tak, naprawdę.

Bonus nr 2: file_exists( __DIR__ )powróci TRUE. Tak, naprawdę… na wypadek gdybyś zapomniał.

^ Prowadzi to do faktycznego błędu w rdzeniu WP.


Aby pomóc innym osobom przechodzącym w trybie eksploratora i znajdującym je (źle dla nieudokumentowanych elementów), szybko podsumuję to, co próbowałem:

Próba 1: plik tymczasowy w pamięci

Pierwszą próbą, jaką podjąłem, było utworzenie strumienia do pliku tymczasowego przy użyciu php://temp. Z dokumentów PHP:

Jedyną różnicą między nimi jest to, że php://memoryzawsze będą przechowywać swoje dane w pamięci, podczas gdy php://tempużyje pliku tymczasowego, gdy ilość przechowywanych danych przekroczy ustalony limit (domyślnie 2 MB). Lokalizacja tego pliku tymczasowego jest określana w taki sam sposób, jak sys_get_temp_dir()funkcja.

Kod:

$handle = fopen( 'php://temp', 'r+' );
fwrite( $handle, 'foo' );
rewind( $handle );
var_dump( file_exist( stream_get_contents( $handle, 5 ) );

Znalezienie: Nie, nie działa.

Próba 2: użyj pliku tymczasowego

Jest tmpfile(), więc dlaczego tego nie użyć ?!

var_dump( file_exists( tmpfile() ) );
// boolean FALSE

Tak, tyle o tym skrócie.

Próba 3: użyj niestandardowego opakowania strumienia

Następnie pomyślałem, że mogę zbudować niestandardowe opakowanie strumienia i zarejestrować je za pomocąstream_wrapper_register() . Następnie mógłbym użyć wirtualnego szablonu z tego strumienia, aby oszukać rdzeń w przekonaniu, że mamy plik. Przykładowy kod poniżej (już usunąłem pełną klasę, a historia nie ma wystarczającej liczby kroków…)

class TemplateStreamWrapper
{
    public $context;

    public function stream_open( $path, $mode, $options, &$opened )
    {
        // return boolean
    }
}

stream_wrapper_register( 'vt://comments', 'TemplateStreamWrapper' );
// … etc. …

Ponownie, to wrócił NULLna file_exists().


Testowane z PHP 5.6.20


Myślę, że twoja Próba 3 powinna działać w teorii. Czy wdrożyłeś w niestandardowym opakowaniu strumienia stream_stat()? Myślę, że to właśnie file_exists()zadzwoni, aby sprawdzić ... php.net/manual/en/streamwrapper.stream-stat.php
Alain Schlesser

Pozytywnie oceniany, ponieważ jest całkiem miły i niezbyt hackerski. Ponieważ jednak mój kod ma być używany w różnych konfiguracjach, obawiam się, że pozwolenie na pisanie może być problemem. Co więcej, pliki tymczasowe muszą zostać usunięte, co nie jest tak łatwe w locie , ponieważ nie jest łatwo przechwycić pełną ścieżkę zwróconą przez tempnam(). Korzystanie z zadania cron będzie działać, ale jest to dodatkowy narzut ...
gmazzap

Myślę, że pisanie pliku tymczasowego jest gorsze niż wysyłanie pustego szablonu. Naprawiony pusty szablon zostanie zbuforowany w opcode. Plik tymczasowy będzie musiał zostać zapisany na dysku, przeanalizowany na zimno (bez kodu operacyjnego), a następnie usunięty. Lepiej zminimalizować liczbę trafień na dysk bez uzasadnionego powodu.
Rarst

@Rarst Pytanie nigdy nie brzmiało „co jest lepsze” pod względem wydajności. Pytanie sprowadzało się do braku pliku szablonu :)
Kaiser

1
tempnam( sys_get_temp_dir(), 'comments.php' )jest napisany raz , możesz ponownie użyć nazwy pliku, a plik jest pusty , więc nie zużywa wielu zasobów. Ponadto kod jest łatwy do zrozumienia. Zdecydowanie najlepsze rozwiązanie, imho.
fuxia

3

Ponieważ @AlainSchlesser zasugerował, aby podążać tą trasą (a ponieważ niedziałające rzeczy zawsze powodują błąd), ponowiłem próbę zbudowania opakowania strumienia dla plików wirtualnych. Nie mogłem go rozwiązać (czytaj: czytanie wartości zwracanych w dokumentach) na własną rękę, ale rozwiązałem go przy pomocy @HPierce na SO .

class VirtualTemplateWrapper
{
    public $context;

    public function stream_open( $path, $mode, $options, &$opened_path ) { return true; }

    public function stream_read( $count ) { return ''; }

    public function stream_eof() { return ''; }

    public function stream_stat() {
        # $user = posix_getpwuid( posix_geteuid() );
        $data = [
            'dev'     => 0,
            'ino'     => getmyinode(),
            'mode'    => 'r',
            'nlink'   => 0,
            'uid'     => getmyuid(),
            'gid'     => getmygid(),
            #'uid'     => $user['uid'],
            #'gid'     => $user['gid'],
            'rdev'    => 0,
            'size'    => 0,
            'atime'   => time(),
            'mtime'   => getlastmod(),
            'ctime'   => FALSE,
            'blksize' => 0,
            'blocks'  => 0,
        ];
        return array_merge( array_values( $data ), $data );
    }

    public function url_stat( $path, $flags ) {
        return $this->stream_stat();
    }
}

Musisz tylko zarejestrować nową klasę jako nowy protokół:

add_action( 'template_redirect', function() {
    stream_wrapper_register( 'virtual', 'VirtualTemplateWrapper' );
}, 0 );

Umożliwia to utworzenie wirtualnego (nieistniejącego) pliku:

$template = fopen( "virtual://comments", 'r+' );

Twoja funkcja może zostać przekształcona w:

function engineCommentsTemplate( $engine )
{
    $replacement = null;
    $virtual = fopen( "virtual://comments", 'r+' );

    $tmplGetter = function( $original ) use( &$replacement, $virtual ) {
        $replacement = $original;
        return $virtual;
    };

    add_filter( 'comments_template', $tmplGetter, PHP_INT_MAX );

    comments_template();

    remove_filter( 'comments_template', $tmplGetter, PHP_INT_MAX );

    // As the PHP internals are quite unclear: Better safe then sorry
    unset( $virtual );

    if ( is_file( $replacement ) && is_readable( $replacement ) ) {
        return $engine->render( $replacement );
    }

    return '';
}

ponieważ file_exists()rdzeń odprawy powraca TRUEi require $filenie zgłasza błędu.

Muszę zauważyć, że jestem bardzo szczęśliwy, jak się to potoczyło, ponieważ może być naprawdę pomocne w testach jednostkowych.


1
Świetne ustalenia! Najbardziej podoba mi się to podejście ;-) Jestem pewien, że istnieją inne części rdzenia, do których można by to zastosować.
birgire

1
Pozytywne i dziękuję! Do testów jednostkowych jest już github.com/mikey179/vfsStream, więc nie trzeba wymyślać koła ponownie;) Btw, podoba mi się to podejście, nie jestem pewien, czy go
użyję,


@kaiser nah, znalazłem to, ponieważ I RTFM: P phpunit.de/manual/current/en/…
gmazzap
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.