Impedir que comments_template () carregue 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 WP. Eu resolvi aquele usando uma biblioteca minha, Brain \ Hierarchy .

Com relação get_template_part()e outras funções que cargas parciais gosto get_header(), get_footer()e semelhantes, que era muito fácil escrever wrapper para a funcionalidade parcial modelo de motor.

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, o que eu quero fazer também para obter a compatibilidade máxima do núcleo.

No entanto, assim que eu chamo comments_template(), um arquivo é required, é 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 fallback de último recurso

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

Solução atual

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

Algo assim:

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 vazio comments.php?

Porque eu não gosto disso.

Respostas:


4

Não tenho certeza de que a solução a seguir seja melhor que a do OP, digamos que seja uma solução alternativa, provavelmente mais hackiana.

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

Você pode usar uma classe de exceção personalizada como um DTO para transportar 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 finallybloco requer PHP 5.5+.

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


4

Eu lutei com isso antes e minha solução foi - ele pode se nocautear exigindo arquivos, desde que não faça nada.

Aqui está o código relevante do meu projeto de modelagem 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';
}

Eu deixei comments_template()passar os movimentos para configurar globais e afins, mas o alimento com um arquivo PHP vazio requiree passo para o meu modelo Twig real para saída.

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

Embora eu ainda precise enviar um arquivo vazio para ele, faço isso na biblioteca e o tema de implementação não precisa se preocupar com isso.


Voto a favor, obrigado. Eu já vi sua abordagem desde que usei o Meadow antes. O que eu não gostei aqui é o fato de que um modelo em branco precisa ser enviado de qualquer maneira. Além disso, isso interrompe qualquer tentativa de usar comments_templatefiltro ou COMMENTS_TEMPLATEconstante para personalizar o modelo. O que não é crucial, mas, como eu disse, queria permanecer o máximo possível compatível com o núcleo.
gmazzap

@gmazzap hmmm ... não há razão para não poder adicionar suporte para filtro e constante no meu invólucro, mas ele entra em micro-gerenciamento.
Rarst

3

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

Depois de muitos saltos e rastejando pelos cantos mais sujos do PHP, reformulei a pergunta para:

Como se pode enganar PHP a voltar TRUEpara 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()quais bandagens WP_TEMP_DIR. Dica: estranhamente, ele volta para /tmp/que os arquivos não sejam preservados entre as reinicializações, o que /var/tmp/ocorreria. Pode-se fazer uma comparação simples de strings no final e verificar o valor de retorno e, em seguida, corrigir isso no caso de ser necessário - o que não é o caso:

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

Agora, para testar rapidamente se há erros gerados para um arquivo temporário sem conteúdo:

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

E: Sem erros → trabalhando.

EDIT: Como @toscho apontou nos comentários, ainda há uma maneira melhor de fazer isso:

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

Nota: De acordo com uma nota de usuário nos documentos do php.net , o sys_get_temp_dir()comportamento difere entre os sistemas. Portanto, o resultado obtém a barra à direita removida e adicionada novamente. Como o principal erro 22267 foi corrigido, agora 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 mesmo.

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

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


Para ajudar outras pessoas a entrar no modo explorador e a encontrá-las (mal em 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 para um arquivo temporário usando php://temp. Nos documentos do PHP:

A única diferença entre os dois é que php://memorysempre armazenará seus dados na memória, enquanto php://temputilizará um arquivo temporário assim que 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 sys_get_temp_dir()função.

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: Use um wrapper de fluxo personalizado

Em seguida, pensei em criar um wrapper de fluxo personalizado e registrá-lo usandostream_wrapper_register() . Então eu poderia usar um modelo virtual desse fluxo para induzir o núcleo a acreditar que temos um arquivo. Exemplo de código abaixo (eu já excluí a classe completa e o histórico não possui 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 voltou NULLem file_exists().


Testado com PHP 5.6.20


Eu acho que sua tentativa 3 deve funcionar em teoria. No seu wrapper de fluxo personalizado, você implementou stream_stat()? Eu acho que isso é o file_exists()que chamará para fazer a sua verificação ... php.net/manual/en/streamwrapper.stream-stat.php
Alain Schlesser

Votado porque é muito bom e não muito hackish. No entanto, como meu código deve ser usado em diferentes configurações, receio que a permissão de gravação possa ser um problema. Além disso, arquivos temporários precisam ser excluídos, que não é assim tão fácil na mosca , porque não é fácil de interceptar o caminho completo retornado por tempnam(). Usar um trabalho cron funcionará, mas é uma sobrecarga adicional ...
gmazzap

Eu acho que escrever arquivo temporário é pior do que enviar modelo vazio. O modelo vazio fixo será armazenado em cache no código de operação. O arquivo temporário deverá ser gravado no disco, analisado a frio (sem código de operação) e excluído. É melhor minimizar as ocorrências do disco sem uma boa razão.
Rarst

@Rarst A questão nunca foi "o que é melhor" em termos de desempenho. Pergunta se resumia a não ter o arquivo de modelo :)
kaiser

11
tempnam( sys_get_temp_dir(), 'comments.php' )é gravado uma vez , você pode reutilizar o nome do arquivo, e o arquivo está vazio , portanto, ele não usa muitos recursos. Além disso, é fácil entender o seu código. De longe a melhor solução, imho.
fuxia

3

Como o @AlainSchlesser sugeriu seguir a rota (e como as coisas não funcionais sempre me incomodam ), tentei criar um wrapper de fluxo para arquivos virtuais. Não consegui resolvê-lo (leia-se: lendo os valores de retorno nos documentos) sozinho, mas o resolvi com a ajuda do @HPierce on 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 o file_exists()check-in principal retorna TRUEe require $filenão gera erro.

Devo observar que estou muito feliz com o resultado, pois pode ser realmente útil nos testes de unidade.


11
Ótimas descobertas! Eu gosto mais dessa abordagem ;-) Tenho certeza de que há outras partes do núcleo às quais isso pode ser aplicado.
birgire

11
Votado e obrigado! Para testes de unidade, já existe o github.com/mikey179/vfsStream; portanto, não há necessidade de reinventar a roda;) Btw, eu gosto dessa abordagem, não tenho certeza se vou usar isso porque o método de exceção me faz sentir feliz e mal: D
gmazzap


@kaiser nah, eu achei porque eu RTFM: P phpunit.de/manual/current/en/...
gmazzap
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.