Запобігати коментарям_шаблон () для завантаження comments.php


9

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

Перший контекст

Першою моєю проблемою було знайти спосіб вирішити шаблон, починаючи з WP-запиту. Я вирішив це, використовуючи мою бібліотеку, Мозок \ Ієрархія .

Що стосується 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?

Бо мені це не подобається.

Відповіді:


4

Не впевнений, що наступне рішення краще, ніж рішення в ОП, скажімо лише, що це альтернативне, ймовірно, більш хакітське рішення.

Я думаю, ви можете використовувати виняток 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+.

Працює так само, і не потрібен порожній шаблон.


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.

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


Оновлено, дякую. Я вже бачив ваш підхід, оскільки я раніше користувався Лугом. Що мені тут не сподобалось - це той факт, що порожній шаблон все одно потрібно відправляти. Більше того, це порушує будь-яку спробу використовувати comments_templateфільтр або COMMENTS_TEMPLATEконстанту для налаштування шаблону. Що не головне, але, як я вже сказав, я хотів залишитися максимально сумісним з ядром.
gmazzap

@ gmazzap hmmm ... ні з якої причини я не зміг додати підтримку фільтрів і константи в моїй обгортці, але він потрапляє в мікроуправління.
Рарст

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;

І: Немає помилок → працює.

EDIT: Як в коментарях зазначив @toscho , все ж є кращий спосіб зробити це:

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

Примітка. Відповідно до примітки користувачів на документах php.net , 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

Я думаю, що написання тимчасового файлу гірше, ніж доставка порожнього шаблону. Виправлений порожній шаблон буде кешований в опкод. Файл Temp потрібно буде записати на диск, проаналізувати холодно (без коду), а потім видалити. Краще мінімізувати звернення диска без поважних причин.
Рарст

@Rarst Питання ніколи не було "що краще" . Питання зведено до відсутності файлу шаблону :)
kaiser

1
tempnam( sys_get_temp_dir(), 'comments.php' )пишеться один раз , ви можете повторно використовувати ім'я файлу, і файл порожній , тому він не використовує багато ресурсів. Плюс це легко зрозуміти у вашому коді. Напевно найкраще рішення, імхо.
фуксія

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, тому не потрібно винаходити колесо;) Btw, мені подобається такий підхід, не впевнений, що я буду використовувати це, оскільки метод виключення змушує мене почувати себе щасливим злом: D
gmazzap


@kaiser nah, я знайшов це, тому що я RTFM: P phpunit.de/manual/current/en/…
gmazzap
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.