Evite comments_template () para carregar comments.php

9

Estou desenvolvendo um tema WordPress usando um mecanismo de modelo. Quero que meu código seja o mais compatível possível com a funcionalidade principal do WP.

Algum contexto primeiro

Meu primeiro problema foi encontrar uma maneira de resolver o modelo a partir de uma consulta do WP. Eu resolvi aquele usando uma biblioteca minha, Brain \ Hierarchy .

Em relação a get_template_part() e outras funções que carregam parciais como get_header() , get_footer() e similares, foi muito fácil escrever o wrapper na funcionalidade parcial do mecanismo de modelo.

O problema

Meu problema agora é como carregar o modelo de comentários.

A função WordPress comments_template() é uma função de ~ 200 linhas que faz muitas coisas, que eu quer fazer o mesmo para máxima compatibilidade do núcleo.

No entanto, assim que eu chamar comments_template() , um arquivo será require d, é o primeiro de:

  • o arquivo na constante COMMENTS_TEMPLATE , se definido
  • comments.php na pasta do tema, se encontrado
  • /theme-compat/comments.php no WP inclui pasta como último recurso fallback

Em suma, não há como impedir que a função carregue um arquivo PHP, o que não é desejável para mim, porque eu preciso renderizar meus modelos e não simplesmente usar require .

Solução atual

No momento, estou enviando um arquivo comments.php vazio e estou usando 'comments_template' gancho de filtro, para saber qual modelo o WordPress deseja carregar e usar o recurso do meu mecanismo de modelo para carregar o modelo.

Algo parecido com isto:

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

A questão

Isso funciona, é compatível com o núcleo, mas ... existe uma maneira de fazê-lo funcionar sem ter que enviar um comments.php vazio?

Porque eu não gosto disso.

    
por gmazzap 30.05.2016 / 20:09

4 respostas

4

Não tenho certeza se a solução a seguir é melhor do que a solução em OP, digamos que é uma solução alternativa, provavelmente mais hackeana.

Acho que você pode usar uma exceção PHP para interromper a execução do WordPress quando o filtro 'comments_template' for aplicado.

Você pode usar uma classe de exceção personalizada como um DTO para carregar o modelo.

Este é um rascunho para a exceção:

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

Com esta classe de exceção disponível, sua função se torna:

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

O bloco finally requer o PHP 5.5 +.

Funciona da mesma forma e não requer um modelo vazio.

    
por gmazzap 30.05.2016 / 20:29
4

Eu já lutei com isso antes e a minha solução foi - ele pode acabar com o arquivo, desde que não faça nada.

Aqui está o código relevante do meu projeto de modelagem do Prado :

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

Eu deixo comments_template() passar pelos movimentos para configurar globais e semelhantes, mas alimentá-lo com o arquivo PHP vazio para require e passar para o meu modelo Twig real para saída.

Observe que isso requer a capacidade de interceptar a inicial comments_template() call, o que eu posso fazer já que meu modelo Twig está chamando abstração intermediária em vez de função PHP real.

Embora eu ainda precise enviar arquivos vazios para ele, faço isso na biblioteca e a implementação do tema não precisa se preocupar com isso.

    
por Rarst 30.05.2016 / 20:55
3

Solução: use um arquivo temporário - com um nome de arquivo exclusivo

Depois de muitos pulos e rastejando nos cantos mais sujos do PHP, eu reformulei a pergunta para:

  

Como um truque PHP pode retornar TRUE para file_exists( $file ) ?

como o código no núcleo é apenas

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

Então a questão foi resolvida mais rapidamente:

$template = tempnam( __DIR__, '' );

e é isso. Talvez seja melhor usar wp_upload_dir() :

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

Outra opção pode ser usar get_temp_dir() que agrupa WP_TEMP_DIR . Dica: Ele estranhamente volta para /tmp/ , então os arquivos não serão preservados entre as reinicializações, o que /var/tmp/ faria. Pode-se fazer uma comparação de cadeia simples no final e verificar o valor de retorno e, em seguida, corrigir isso caso seja necessário - o que não é nesse caso:

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

Agora, teste rapidamente se há erros lançados em um arquivo temporário sem conteúdo:

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

E: Sem erros → trabalhando.

EDITAR: Como @toscho apontou nos comentários, ainda há uma maneira melhor de fazê-lo:

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

Nota: De acordo com uma nota de usuários em documentos do php.net , o comportamento sys_get_temp_dir() difere entre os sistemas. Portanto, o resultado remove a barra final e, em seguida, é adicionado novamente. Como o bug principal # 22267 é fixo, isso também deve funcionar nos servidores Win / IIS.

Sua função refatorada (não testada):

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

Bônus Nr.1: tmpfile() retornará NULL . Sim, realmente.

Bônus Nr.2: file_exists( __DIR__ ) retornará TRUE . Sim, realmente… caso você tenha esquecido.

^ Isso leva a um erro real no núcleo do WP.

Para ajudar outras pessoas a entrar no modo de explorador e a encontrá-las (mal a partes não documentadas), resumirei rapidamente o que tentei:

Tentativa 1: arquivo temporário na memória

A primeira tentativa que fiz foi criar um fluxo em um arquivo temporário usando php://temp . Da documentação do PHP:

  

A única diferença entre os dois é que php://memory sempre armazenará seus dados na memória, enquanto php://temp usará um arquivo temporário quando a quantidade de dados armazenados atingir um limite predefinido (o padrão é 2 MB). A localização desse arquivo temporário é determinada da mesma maneira que a função sys_get_temp_dir() .

O código:

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

Encontrar: Não, não funciona.

Tentativa 2: usar um arquivo temporário

tmpfile() , então por que não usar isso?

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

Sim, muito sobre esse atalho.

Tentativa 3: usar um wrapper de fluxo personalizado

Em seguida, achei que poderia criar um wrapper de fluxo personalizado e registre-o usando stream_wrapper_register() . Então, eu poderia usar um modelo virtual desse fluxo para fazer o núcleo acreditar que temos um arquivo. Exemplo de código abaixo (eu já excluí a classe completa e o histórico não tem etapas suficientes…)

class TemplateStreamWrapper
{
    public $context;

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

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

Novamente, isso retornou NULL on file_exists() .

Testado com PHP 5.6.20

    
por kaiser 31.05.2016 / 00:41
3

Como @AlainSchlesser sugeriu seguir a rota (e como coisas que não funcionam sempre me incomodam), Eu tentei criar um wrapper de fluxo para arquivos virtuais. Não consegui resolvê-lo (leia-se: lendo os valores de retorno nos documentos) por conta própria, mas resolvi-o com a ajuda de @HPierce em 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();
    }
}

Você só precisa registrar a nova classe como novo protocolo:

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

Isso permite criar um arquivo virtual (não existente):

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

Sua função pode ser refatorada para:

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

como a file_exists() no core retorna TRUE e require $file não gera erros.

Eu tenho que notar que estou muito feliz com o resultado, já que pode ser muito útil em testes de unidade.

    
por kaiser 31.05.2016 / 23:12

Tags