Запретите comments_template () для загрузки comments.php

9

Я разрабатываю тему WordPress с использованием шаблонизатора. Я хочу, чтобы мой код был максимально совместим с основными функциями WP.

Сначала какой-то контекст

Моя первая проблема состояла в том, чтобы найти способ разрешить шаблон, начиная с запроса WP. Я решил эту проблему, используя мою библиотеку Brain \ Hierarchy .

Что касается get_template_part()и других функций , которые загружают обертонам нравятся get_header(), get_footer()и похоже, что это было довольно легко писать обертку для частичной функциональности шаблона двигателя.

Проблема

Моя проблема сейчас, как загрузить шаблон комментариев.

Функция WordPress comments_template()- это функция ~ 200 строк, которая делает много вещей, которые я хочу сделать также для максимальной совместимости ядра.

Однако, как только я позвоню comments_template(), файл required, это первый из:

  • файл в константе COMMENTS_TEMPLATE, если он определен
  • comments.php в папке темы, если найден
  • /theme-compat/comments.php в WP включена папка как последнее средство откат

Короче говоря, нет способа помешать функции загружать файл PHP, что мне нежелательно, потому что мне нужно рендерить свои шаблоны, а не просто использовать require.

Текущее решение

В данный момент я отправляю пустой comments.phpфайл и использую 'comments_template'фильтр-ловушку, чтобы узнать, какой шаблон WordPress хочет загрузить, и использую функцию из моего механизма шаблонов для загрузки шаблона.

Что-то вроде этого:

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

Вопрос

Это работает, совместимо ли ядро, но ... есть ли способ заставить его работать, не отправляя пустой comments.php?

Потому что мне это не нравится.

Gmazzap
источник

Ответы:

4

Не уверен, что следующее решение лучше, чем решение в OP, скажем так, это альтернативное, возможно, более хакерское решение.

Я думаю, что вы можете использовать исключение PHP, чтобы остановить выполнение WordPress при применении 'comments_template'фильтра.

Вы можете использовать пользовательский класс исключений в качестве DTO для переноса шаблона.

Это черновик для исключения:

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

С этим классом исключения ваша функция становится:

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

finallyБлок требует PHP 5.5+.

Работает так же, и не требует пустого шаблона.

Gmazzap
источник
4

Я боролся с этим раньше, и мое решение состояло в том, что он может выбить себе файл, если он ничего не делает .

Вот соответствующий код из моего проекта шаблонов 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';
}

Я позволил comments_template()пройти через шаги, чтобы установить глобальные переменные и тому подобное, но передал ему пустой файл PHP requireи перешел к моему фактическому шаблону Twig для вывода.

Обратите внимание, что для этого необходимо иметь возможность перехватывать начальный comments_template()вызов, что я могу сделать, поскольку мой шаблон Twig вызывает промежуточную абстракцию, а не реальную функцию PHP.

Хотя мне все еще нужно отправить пустой файл для этого, я делаю это в библиотеке, и реализация темы не должна заботиться об этом вообще.

Rarst
источник
Спасибо, спасибо. Я уже видел ваш подход с тех пор, как использовал луг раньше. Что мне здесь не понравилось, так это то, что пустой шаблон должен быть доставлен в любом случае. Более того, это ломает любую попытку использовать comments_templateфильтр или COMMENTS_TEMPLATEконстанту для настройки шаблона. Что не является ключевым, но, как я уже сказал, я хотел быть максимально совместимым с ядром.
gmazzap
@gmazzap хммм ... без причины я не мог добавить поддержку фильтра и константы в свою обертку, но это входит в микроуправление.
Rarst
3

Решение: используйте временный файл - с уникальным именем файла

После многих прыжков и проникновения в самые грязные уголки PHP, я перефразировал вопрос:

Как можно обмануть PHP TRUEдля возвращения file_exists( $file )?

так как код в ядре как раз есть

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

Тогда вопрос был решен быстрее:

$template = tempnam( __DIR__, '' );

и это все. Возможно, было бы лучше использовать wp_upload_dir()вместо:

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

Другой вариант может заключаться в том, get_temp_dir()какие обертки использовать WP_TEMP_DIR. Подсказка: как ни странно, /tmp/файлы не сохранятся между перезагрузками, что /var/tmp/могло бы произойти . Можно выполнить простое сравнение строк в конце и проверить возвращаемое значение, а затем исправить это в случае необходимости, чего в этом случае нет:

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

Теперь, чтобы быстро проверить, есть ли ошибки для временного файла без содержимого:

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

И: нет ошибок → работает.

РЕДАКТИРОВАТЬ: Как @toscho указал в комментариях, есть еще лучший способ сделать это:

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

Примечание. Согласно примечанию пользователя на php.net docs , sys_get_temp_dir()поведение в разных системах различается. Поэтому в результате удаляется завершающий слеш, а затем добавляется снова. Поскольку основная ошибка № 22267 исправлена, теперь это должно работать и на серверах Win / IIS.

Ваша рефакторированная функция (не проверена):

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

Бонус №1: tmpfile()вернется NULL. Да, действительно.

Бонус №2: file_exists( __DIR__ )вернется TRUE. Да, действительно ... если ты забыл.

^ Это приводит к реальной ошибке в ядре WP.


Чтобы помочь другим перейти в режим проводника и найти их (плохо для недокументированных фрагментов), я быстро подведу итог тому, что я пробовал:

Попытка 1: временный файл в памяти

Первой попыткой было создать поток во временный файл, используя php://temp. Из документов PHP:

Единственное различие между ними заключается в том, что php://memoryони всегда будут хранить свои данные в памяти, тогда как php://tempбудут использовать временный файл, как только объем хранимых данных достигнет заранее установленного предела (по умолчанию 2 МБ). Расположение этого временного файла определяется так же, как и sys_get_temp_dir()функция.

Код:

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

Находка: Нет, не работает.

Попытка 2: использовать временный файл

Там tmpfile(), так почему бы не использовать это ?!

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

Да, так много об этом ярлыке.

Попытка 3: использовать пользовательскую потоковую оболочку

Затем я подумал, что мог бы создать собственную обертку потока и зарегистрировать ее, используяstream_wrapper_register() . Тогда я мог бы использовать виртуальный шаблон из этого потока, чтобы обмануть ядро, поверив, что у нас есть файл. Пример кода ниже (я уже удалил весь класс, а истории не хватает шагов…)

class TemplateStreamWrapper
{
    public $context;

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

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

Опять же , это возвращается NULLна file_exists().


Протестировано с PHP 5.6.20

кайзер
источник
Я думаю, что ваша попытка 3 должна работать в теории. В своей пользовательской потоковой обертке вы реализовали stream_stat()? Я думаю, что это то, что file_exists()вызовет, чтобы проверить ... php.net/manual/en/streamwrapper.stream-stat.php
Ален Шлессер
Проголосовал, потому что это довольно приятно и не очень хакерский. Однако, поскольку мой код предназначен для использования в разных установках, я боюсь, что разрешение на запись может быть проблемой. Кроме того, временные файлы должны быть удалены, что не так просто на лету , потому что нелегко перехватить полный путь, возвращаемый tempnam(). Использование задания cron будет работать, но это дополнительные издержки ...
gmazzap
Я думаю, что написание временного файла хуже, чем доставка пустого шаблона. Фиксированный пустой шаблон будет кэширован в код операции. Временный файл должен быть записан на диск, подвергнут холодному анализу (без кода операции), а затем удален. Лучше минимизировать попадания на диск без веской причины.
Rarst
@Rarst Вопрос никогда не был «что лучше» в отношении производительности. Вопрос сводился к отсутствию файла шаблона :)
kaiser
1
tempnam( sys_get_temp_dir(), 'comments.php' )записывается один раз , вы можете повторно использовать имя файла, и файл пуст , поэтому он не использует много ресурсов. Плюс это легко понять в вашем коде. Безусловно лучшее решение, имхо.
fuxia
3

Поскольку @AlainSchlesser предложил следовать по маршруту (и поскольку нерабочие вещи всегда вызывают у меня проблемы ), я попытался создать потоковую оболочку для виртуальных файлов. Я не мог решить это (читай: чтение возвращаемых значений в документации) самостоятельно, но решил это с помощью @HPierce на 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();
    }
}

Вам просто нужно зарегистрировать новый класс как новый протокол:

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

Затем это позволяет создать виртуальный (не существующий) файл:

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

Ваша функция может затем быть реорганизована в:

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

как file_exists()проверка в ядре возвращается TRUEи require $fileвыбрасывает без ошибок.

Я должен отметить, что я очень рад, как это получилось, поскольку это может быть очень полезно при юнит-тестах.

кайзер
источник
1
Отличные результаты! Мне больше нравится этот подход ;-) Я уверен, что есть другие части ядра, к которым это можно применить.
Birgire
1
Проголосовал и спасибо! Для модульных тестов уже есть github.com/mikey179/vfsStream, поэтому не нужно изобретать велосипед;) Кстати, мне нравится этот подход, я не уверен, что буду использовать его, потому что метод исключения заставляет меня чувствовать себя счастливо злым: D
gmazzap
@gmazzap Я очень уверен, что именно так ты выглядел, когда узнал .
Кайзер
@ Кайзер Нет, я нашел это, потому что я RTFM: P phpunit.de/manual/current/en/…
gmazzap