Prevenir comments_template () para cargar comments.php

9

Estoy desarrollando un tema de WordPress usando un motor de plantillas. Quiero que mi código sea lo más compatible posible con la funcionalidad principal de WP.

Un poco de contexto primero

Mi primer problema fue encontrar una manera de resolver la plantilla a partir de una consulta WP. Lo resolví utilizando una biblioteca mía, Brain \ Hierarchy .

Con respecto a get_template_part() y otras funciones que cargan parciales como get_header() , get_footer() y similares, fue bastante fácil escribir la envoltura en la funcionalidad parcial del motor de plantilla.

El problema

Mi problema ahora es cómo cargar la plantilla de comentarios.

La función de WordPress comments_template() es una función de ~ 200 líneas que hace muchas cosas, que Quiero hacer lo mismo para la máxima compatibilidad del núcleo.

Sin embargo, tan pronto como llamo a comments_template() , un archivo es require d, es el primero de:

  • el archivo en la constante COMMENTS_TEMPLATE , si está definido
  • comments.php en la carpeta de temas, si se encuentra
  • /theme-compat/comments.php en WP incluye la carpeta como último recurso de recurso

En resumen, no hay forma de evitar que la función cargue un archivo PHP, lo cual no es conveniente para mí, porque necesito renderizar mis plantillas y no simplemente usar require .

Solución actual

En este momento, estoy enviando un archivo comments.php vacío y estoy usando 'comments_template' gancho de filtro, para saber qué plantilla WordPress quiere cargar, y usar la función de mi motor de plantillas para cargar la plantilla.

Algo como esto:

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 pregunta

Esto funciona, es compatible con el núcleo, pero ... ¿hay alguna manera de hacerlo funcionar sin tener que enviar un comments.php vacío?

Porque no me gusta.

    
pregunta gmazzap 30.05.2016 - 20:09

4 respuestas

4

No estoy seguro de que la siguiente solución sea mejor que la solución en OP; digamos que es una solución alternativa, probablemente más intrincada.

Creo que puedes usar una excepción de PHP para detener la ejecución de WordPress cuando se aplica 'comments_template' filter.

Puede usar una clase de excepción personalizada como DTO para llevar la plantilla.

Este es un borrador para la excepción:

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 esta clase de excepción disponible, su función se convierte en:

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

El bloque finally requiere PHP 5.5+.

Funciona de la misma manera, y no requiere una plantilla vacía.

    
respondido por el gmazzap 30.05.2016 - 20:29
4

Ya he luchado con esto antes y mi solución fue que puede eliminarse si se requiere un archivo, siempre que no haga nada.

Aquí está el código relevante de mi proyecto de plantilla 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';
}

Dejé que comments_template() siga los movimientos para configurar los elementos globales y demás, pero le puse un archivo PHP vacío a require y me muevo a mi plantilla Twig real para la salida.

Tenga en cuenta que esto requiere poder interceptar el comments_template() call inicial, lo que puedo hacer ya que mi plantilla Twig está llamando a la abstracción intermedia en lugar de a la función PHP real.

Aunque todavía tengo que enviar un archivo vacío para él, lo hago en la biblioteca y la implementación del tema no tiene que preocuparme en absoluto.

    
respondido por el Rarst 30.05.2016 - 20:55
3

Solución: utilice un archivo temporal, con un nombre de archivo único

Después de muchos saltos y arrastrándome por los rincones más sucios de PHP, reformulé la pregunta para simplemente:

  

¿Cómo puede un truco PHP para devolver TRUE para file_exists( $file ) ?

como el código en el núcleo simplemente es

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

Entonces la pregunta se resolvió más rápido:

$template = tempnam( __DIR__, '' );

y eso es todo. Tal vez sería mejor usar wp_upload_dir() en su lugar:

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

Otra opción podría ser utilizar get_temp_dir() que ajusta WP_TEMP_DIR . Sugerencia: Extrañamente, se reduce a /tmp/ , por lo que los archivos no se conservarán entre reinicios, lo que /var/tmp/ lo haría. Se puede hacer una comparación de cadena simple al final y verificar el valor de retorno y luego corregirlo en caso de que sea necesario, lo que no es en este caso:

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

Ahora, para probar rápidamente si se producen errores para un archivo temporal sin contenido:

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

Y: Sin errores → trabajando.

EDITAR: Como @toscho señaló en los comentarios, todavía hay una forma mejor de hacerlo:

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

Nota: De acuerdo con una nota de los usuarios en los documentos de php.net , el comportamiento sys_get_temp_dir() difiere entre sistemas. Por lo tanto, el resultado hace que se elimine la barra diagonal y luego se vuelva a agregar. Como el error principal # 22267 está solucionado, esto también debería funcionar en los servidores Win / IIS ahora.

Su función refactorizada (no probada):

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 Nr.1: tmpfile() devolverá NULL . Sí, en serio.

Bonus Nr.2: file_exists( __DIR__ ) devolverá TRUE . Sí, en serio ... en caso de que lo hayas olvidado.

^ Esto conduce a un error real en el núcleo de WP.

Para ayudar a otros a ir en modo explorador y encontrarlos (mal para piezas indocumentadas), resumiré rápidamente lo que intenté:

Intento 1: archivo temporal en la memoria

El primer intento que hice fue crear una secuencia en un archivo temporal, usando php://temp . De los documentos PHP:

  

La única diferencia entre los dos es que php://memory siempre almacenará sus datos en la memoria, mientras que php://temp usará un archivo temporal una vez que la cantidad de datos almacenados alcance un límite predefinido (el valor predeterminado es 2 MB). La ubicación de este archivo temporal se determina de la misma manera que la función sys_get_temp_dir() .

El código:

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

Búsqueda: No, no funciona.

Intento 2: usar un archivo temporal

Hay tmpfile() , ¡¿por qué no usar eso?

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

Sí, mucho sobre este atajo.

Intento 3: usar un contenedor de flujo personalizado

Luego pensé que podría crear un contenedor de flujo personalizado y registrarlo con stream_wrapper_register() . Entonces podría usar una plantilla virtual de esa secuencia para engañar al núcleo y hacerle creer que tenemos un archivo. Código de ejemplo a continuación (ya eliminé la clase completa y el historial no tiene suficientes pasos ...)

class TemplateStreamWrapper
{
    public $context;

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

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

De nuevo, esto devolvió NULL en file_exists() .

Probado con PHP 5.6.20

    
respondido por el kaiser 31.05.2016 - 00:41
3

Como @AlainSchlesser sugirió seguir la ruta (y como cosas que no funcionan siempre me molestan), He intentado construir un contenedor de flujo para archivos virtuales. No pude resolverlo (lea: leer los valores de retorno en los documentos) por mi cuenta, pero lo resolví con la ayuda de @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();
    }
}

Solo necesita registrar la nueva clase como nuevo protocolo:

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

Esto permite crear un archivo virtual (no existente):

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

Tu función puede ser refactora a:

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

a medida que el cheque file_exists() en el núcleo devuelve TRUE y el require $file no produce ningún error.

Debo tener en cuenta que estoy bastante contento de cómo resultó esto, ya que podría ser realmente útil con las pruebas unitarias.

    
respondido por el kaiser 31.05.2016 - 23:12

Lea otras preguntas en las etiquetas