Impedisci commenti_template () per caricare commenti.php


9

Sto sviluppando un tema WordPress usando un motore di template. Voglio che il mio codice sia il più compatibile possibile con la funzionalità core di WP.

Prima un po 'di contesto

Il mio primo problema era trovare un modo per risolvere il modello partendo da una query WP. Ho risolto quello usando una mia biblioteca, Brain \ Hierarchy .

Per quanto riguarda get_template_part()e altre funzioni che i carichi parziali piace get_header(), get_footer()e simili, è stato abbastanza facile da involucro in scrittura a template engine funzionalità parziale.

Il problema

Il mio problema è ora come caricare il modello di commenti.

La funzione WordPress comments_template()è una funzione di ~ 200 linee che fa molte cose, che voglio fare anche per la massima compatibilità di base.

Tuttavia, non appena chiamo comments_template(), un file è required, è il primo di:

  • il file nella costante COMMENTS_TEMPLATE, se definito
  • comments.php nella cartella del tema, se trovato
  • /theme-compat/comments.php in WP include la cartella come fallback dell'ultima risorsa

In breve, non c'è modo di impedire alla funzione di caricare un file PHP, il che non è desiderabile per me, perché devo renderizzare i miei modelli e non semplicemente usarli require.

Soluzione attuale

Al momento, sto spedendo un comments.phpfile vuoto e sto usando 'comments_template'un hook di filtro, per sapere quale modello WordPress vuole caricare e utilizzare le funzionalità del mio motore di template per caricare il modello.

Qualcosa come questo:

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

La domanda

Funziona, è compatibile con il core, ma ... c'è un modo per farlo funzionare senza dover spedire un vuoto comments.php?

Perché non mi piace.

Risposte:


4

Non sono sicuro che la seguente soluzione sia migliore della soluzione in OP, diciamo solo che è una soluzione alternativa, probabilmente più hacker.

Penso che puoi usare un'eccezione PHP per interrompere l'esecuzione di WordPress quando 'comments_template'viene applicato il filtro.

È possibile utilizzare una classe di eccezioni personalizzata come DTO per trasportare il modello.

Questa è una bozza per l'eccezione:

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

Con questa classe di eccezioni disponibile, la tua funzione diventa:

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

Il finallyblocco richiede PHP 5.5+.

Funziona allo stesso modo e non richiede un modello vuoto.


4

Ho già lottato con questo in precedenza e la mia soluzione era: può eliminarsi richiedendo file, purché non faccia nulla.

Ecco il codice pertinente del mio progetto di templating 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';
}

Ho lasciato comments_template()andare i movimenti per impostare i globi e così via, ma gli ho dato un file PHP vuoto e sono passato requireal mio modello Twig per l'output.

Si noti che questo richiede di essere in grado di intercettare la comments_template()chiamata iniziale , cosa che posso fare poiché il mio modello Twig sta chiamando l'astrazione intermedia piuttosto che l'effettiva funzione PHP.

Mentre devo ancora spedire file vuoti per questo, lo faccio in libreria e l'implementazione del tema non deve preoccuparsene affatto.


Grazie, votato. Ho già visto il tuo approccio da quando ho usato Meadow prima. Quello che non mi è piaciuto qui è il fatto che un modello vuoto deve essere spedito comunque. Inoltre, ciò interrompe qualsiasi tentativo di utilizzare il comments_templatefiltro o la COMMENTS_TEMPLATEcostante per personalizzare il modello. Il che non è fondamentale, ma, come ho già detto, volevo rimanere il più possibile compatibile con il core.
gmazzap

@gmazzap hmmm ... nessun motivo per cui non sono riuscito ad aggiungere il supporto per filtro e costante nel mio wrapper, ma entra in microgestione.
Rarst

3

Soluzione: utilizzare un file temporaneo, con un nome file univoco

Dopo molti salti e gattonando negli angoli più sporchi di PHP, ho riformulato la domanda semplicemente:

Come si può ingannare PHP a tornare TRUEper file_exists( $file )?

come è giusto il codice nel core

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

Quindi la domanda è stata risolta più rapidamente:

$template = tempnam( __DIR__, '' );

e basta. Forse sarebbe meglio usare wp_upload_dir()invece:

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

Un'altra opzione potrebbe essere quella di utilizzare get_temp_dir()quali involucri WP_TEMP_DIR. Suggerimento: ricade stranamente in /tmp/modo che i file non vengano conservati tra i riavvii, il che /var/tmp/farebbe. Si può fare un semplice confronto di stringhe alla fine e controllare il valore restituito e quindi risolverlo nel caso fosse necessario - che non è in questo caso:

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

Ora per verificare rapidamente se vi sono errori generati per un file temporaneo senza contenuto:

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

E: nessun errore → funzionante.

EDIT: Come ha sottolineato @toscho nei commenti, c'è ancora un modo migliore per farlo:

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

Nota: secondo una nota degli utenti sui documenti php.net , il sys_get_temp_dir()comportamento differisce tra i sistemi. Pertanto, il risultato viene rimosso, quindi aggiunto di nuovo. Poiché il bug principale # 22267 è stato risolto, ora dovrebbe funzionare anche su server Win / IIS.

La tua funzione refactored (non testata):

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

Bonus n. 1: tmpfile()tornerà NULL. Sì davvero.

Bonus n. 2: file_exists( __DIR__ )tornerà TRUE. Sì, davvero ... nel caso te ne fossi dimenticato.

^ Questo porta a un vero bug nel core WP.


Per aiutare gli altri ad andare in modalità esploratore e a trovare quelli (da pezzi non documentati), riassumerò rapidamente ciò che ho provato:

Tentativo 1: file temporaneo in memoria

Il primo tentativo che ho fatto è stato quello di creare uno stream in un file temporaneo, usando php://temp. Dai documenti PHP:

L'unica differenza tra i due è che php://memorymemorizzerà sempre i suoi dati in memoria, mentre php://temputilizzerà un file temporaneo quando la quantità di dati memorizzati raggiunge un limite predefinito (il valore predefinito è 2 MB). Il percorso di questo file temporaneo è determinato allo stesso modo della sys_get_temp_dir()funzione.

Il codice:

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

Trovare: No, non funziona.

Tentativo 2: utilizzare un file temporaneo

Ecco tmpfile(), quindi perché non usarlo ?!

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

Sì, questo su questa scorciatoia.

Tentativo 3: utilizzare un wrapper di flusso personalizzato

Successivamente ho pensato di poter creare un wrapper di flusso personalizzato e registrarlo utilizzandostream_wrapper_register() . Quindi potrei usare un modello virtuale da quello stream per ingannare il core nel credere che abbiamo un file. Codice di esempio di seguito (ho già eliminato l'intera classe e la cronologia non ha abbastanza passaggi ...)

class TemplateStreamWrapper
{
    public $context;

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

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

Di nuovo, questo è tornato NULLsu file_exists().


Testato con PHP 5.6.20


Penso che il tuo tentativo 3 dovrebbe funzionare in teoria. Nel tuo wrapper di flusso personalizzato, hai implementato stream_stat()? Penso che questo è ciò file_exists()che chiamerà per fare il suo controllo ... php.net/manual/en/streamwrapper.stream-stat.php
Alain Schlesser

È stato votato perché è abbastanza carino e non molto hacker. Tuttavia, poiché il mio codice è destinato a essere utilizzato in diverse configurazioni, temo che l'autorizzazione alla scrittura possa essere un problema. Inoltre, i file temporanei devono essere eliminati, il che non è così facile al volo , perché non è facile intercettare l'intero percorso restituito da tempnam(). L'uso di un lavoro cron funzionerà, ma è un sovraccarico aggiuntivo ...
gmazzap

Penso che scrivere un file temporaneo sia peggio che spedire un modello vuoto. Il modello vuoto fisso verrà memorizzato nella cache in opcode. Il file temporaneo dovrà essere scritto sul disco, analizzato a freddo (senza codice operativo), quindi eliminato. È meglio ridurre al minimo gli hit del disco senza una buona ragione.
Rarst

@Rarst La domanda non è mai stata "ciò che è meglio" per quanto riguarda le prestazioni. Domanda ridotta a non avere il file modello :)
kaiser

1
tempnam( sys_get_temp_dir(), 'comments.php' )viene scritto una volta , è possibile riutilizzare il nome del file e il file è vuoto , quindi non utilizza molte risorse. Inoltre è facile da capire nel tuo codice. Di gran lunga la soluzione migliore, imho.
fuxia

3

Poiché @AlainSchlesser ha suggerito di seguire il percorso (e poiché le cose non funzionanti mi infastidiscono sempre), ho provato a creare un wrapper di flusso per i file virtuali. Non ho potuto risolverlo (leggi: leggere i valori di ritorno sui documenti) da solo, ma risolto con l'aiuto di @HPierce su 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();
    }
}

Devi solo registrare la nuova classe come nuovo protocollo:

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

Ciò consente quindi di creare un file virtuale (non esistente):

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

La tua funzione può quindi essere refactored per:

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

poiché il file_exists()check in core ritorna TRUEe require $filenon genera errori.

Devo notare che sono abbastanza contento di come sia potuto essere molto utile con i test unitari.


1
Grandi risultati! Mi piace molto questo approccio ;-) Sono sicuro che ci sono altre parti del core a cui questo potrebbe essere applicato.
birgire,

1
È stato votato e grazie! Per i test unitari c'è già github.com/mikey179/vfsStream, quindi non è necessario reinventare la ruota;) A proposito, mi piace questo approccio, non sono sicuro che lo userò perché il metodo dell'eccezione mi fa sentire felicemente malvagio: D
gmazzap

@gmazzap Sono molto sicuro che sia così che apparivi quando l'hai scoperto .
Kaiser

@kaiser nah, l'ho trovato perché ho RTFM: P phpunit.de/manual/current/en/…
gmazzap
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.