防止comment_template()加载comment.php


9

我正在使用模板引擎开发WordPress主题。我希望我的代码与WP核心功能尽可能兼容。

一些上下文优先

我的第一个问题是找到一种从WP查询开始解析模板的方法。我使用我的图书馆Brain \ Herarchy解决了这个问题。

关于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

因为我不喜欢

Answers:


4

不能确定以下解决方案是否比OP中的解决方案,我们只能说这是一个替代方案,可能更糟糕。

我认为您可以在'comments_template'应用过滤器时使用PHP异常来停止WordPress执行。

您可以将自定义异常类用作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函数,所以我可以这样做。

虽然我仍然必须为此运送空文件,但是我在库中这样做并且实现主题根本不需要关心它。


已投票,谢谢。自从我以前使用Meadow以来,我已经看到了您的方法。我在这里不喜欢的是,无论如何都需要运输一个空白模板。此外,这中断了使用comments_template过滤器或COMMENTS_TEMPLATE常量自定义模板的任何尝试。这不是关键,但正如我所说,我想尽可能与Core保持兼容。
gmazzap

@gmazzap hmmm ...虽然没有理由我不能在包装器中添加对filter和constant的支持,但是它进入了微管理。
拉斯特

3

解决方案:使用临时文件-具有唯一的文件名

在跳了很多步并爬到PHP最肮脏的角落之后,我将问题改写为:

一个人怎么可以欺骗 PHP成回国TRUEfile_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()wraps 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 MB),它将使用一个临时文件。该临时文件的位置与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. …

同样,这回NULLfile_exists()


使用PHP 5.6.20测试


我认为您的尝试3应该在理论上起作用。在您的自定义流包装器中,您实现了stream_stat()吗?我认为这file_exists()将是进行检查的原因... php.net/manual/en/streamwrapper.stream-stat.php
Alain Schlesser

已投票,是因为它很好,但不是很扎实。但是,由于我的代码打算在不同的设置中使用,因此担心写权限可能会成为问题。此外,临时文件需要被删除,这是不容易的飞行,因为它不容易被拦截返回的完整路径tempnam()。使用cron作业可以工作,但是会产生额外的开销...
gmazzap

我认为写临时文件比运送空模板差。固定的空模板将被缓存到操作码中。临时文件将必须写入磁盘,冷解析(无操作码),然后删除。最好不要无故减少磁盘命中。
拉斯特

@Rarst这个问题从来都不是“什么更好”的性能明智的选择。问题归结为没有模板文件:)
kaiser

1
tempnam( sys_get_temp_dir(), 'comments.php' )写入一次后,您可以重复使用文件名,并且该文件为,因此它不占用很多资源。另外,您的代码也很容易理解。到目前为止,最好的解决方案,恕我直言。
fuxia

3

正如@AlainSchlesser建议遵循该路线(并且由于无法正常工作总是困扰我),我尝试为虚拟文件构建流包装器。我自己不能解决它(请阅读:阅读文档中的返回值),但是可以在SO@HPierce的帮助下解决它。

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我很确定这是发现时的样子
kaiser

@kaiser罗,我发现它,因为我RTFM:P phpunit.de/manual/current/en/...
gmazzap
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.