Evite 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.

Algún contexto primero

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

En cuanto get_template_part()y otras funciones que las cargas parciales gustaría get_header(), get_footer()y similares, que era bastante fácil de envoltura de escritura a la funcionalidad parcial motor de plantillas.

La cuestión

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 también quiero hacer para obtener la máxima compatibilidad con el núcleo.

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

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

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

Solución actual

En este momento, estoy enviando un comments.phparchivo vacío y estoy usando el 'comments_template'enlace de filtro, para saber qué plantilla quiere cargar WordPress, y uso 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 vacío comments.php?

Porque no me gusta

gmazzap
fuente

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 agresiva.

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

Puede usar una clase de excepción personalizada como DTO para transportar 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 finallybloque requiere PHP 5.5+.

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

gmazzap
fuente
4

He luchado con esto antes y mi solución fue: puede eliminar el archivo requerido, siempre y cuando no haga nada.

Aquí hay un código relevante de mi proyecto de plantillas 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é comments_template()pasar los movimientos para configurar globales y tal, pero lo alimente con un archivo PHP vacío requirey pase a mi plantilla Twig real para la salida.

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

Si bien todavía tengo que enviar un archivo vacío para ello, lo hago en la biblioteca y la implementación del tema no tiene que preocuparse en absoluto.

Rarst
fuente
Votado, gracias. Ya vi tu enfoque desde que usé Meadow antes. Lo que no me gustó aquí es el hecho de que una plantilla en blanco debe enviarse de todos modos. Además, esto rompe cualquier intento de usar comments_templatefiltro o COMMENTS_TEMPLATEconstante para personalizar la plantilla. Lo cual no es fundamental, pero, como dije, quería mantener la mayor compatibilidad posible con el núcleo.
gmazzap
@gmazzap hmmm ... no hay razón para que no pueda agregar soporte para filtro y constante en mi contenedor, pero entra en microgestión.
Rarst
3

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

Después de muchos saltos y gatear en los rincones más sucios de PHP, reformulé la pregunta para que simplemente:

¿Cómo se puede engañar a PHP para que regrese TRUEa file_exists( $file )?

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

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

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

$template = tempnam( __DIR__, '' );

y eso es. 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 usar get_temp_dir()qué envolturas WP_TEMP_DIR. Sugerencia: extrañamente se recurre a él /tmp/para que los archivos no se conserven entre reinicios, lo que /var/tmp/sí. Se puede hacer una simple comparación de cadenas al final y verificar el valor de retorno y luego arreglarlo en caso de que sea necesario, lo cual no es así:

$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 señaló @toscho en los comentarios, todavía hay una mejor manera de hacerlo:

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

Nota: Según una nota de los usuarios en los documentos de php.net , el sys_get_temp_dir()comportamiento difiere entre sistemas. Por lo tanto, el resultado elimina la barra inclinada final y luego se agrega nuevamente. Como se corrigió el error central # 22267 , ahora también debería funcionar en los servidores Win / IIS.

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

Bonificación Nr.1: tmpfile()volverá NULL. Si, en serio.

Bonificación Nr.2: file_exists( __DIR__ )regresará TRUE. Sí, de verdad ... 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 a piezas indocumentadas), resumiré rápidamente lo que probé:

Intento 1: archivo temporal en la memoria

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

La única diferencia entre los dos es que php://memorysiempre almacenará sus datos en la memoria, mientras php://tempque usará un archivo temporal una vez que la cantidad de datos almacenados llegue a 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 sys_get_temp_dir()función.

El código:

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

Hallazgo: No, no funciona.

Intento 2: usar un archivo temporal

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

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

Sí, eso de este atajo.

Intento 3: usar un contenedor de flujo personalizado

Luego pensé que podría construir un envoltorio de flujo personalizado y registrarlo usandostream_wrapper_register() . Entonces podría usar una plantilla virtual de esa secuencia para engañar al núcleo para que crea que tenemos un archivo. Ejemplo de código 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. …

Una vez más, este volvió NULLa file_exists().


Probado con PHP 5.6.20

emperador
fuente
Creo que su intento 3 debería funcionar en teoría. En su envoltorio de flujo personalizado, ¿implementó stream_stat()? Creo que esto es lo file_exists()que llamará para hacer su verificación ... php.net/manual/en/streamwrapper.stream-stat.php
Alain Schlesser
Votado porque es bastante agradable y no muy hack. Sin embargo, dado que mi código está destinado a ser utilizado en diferentes configuraciones, me temo que el permiso de escritura podría ser un problema. Además, los archivos temporales deben eliminarse, lo que no es tan fácil sobre la marcha , porque no es fácil interceptar la ruta completa devuelta por tempnam(). El uso de un trabajo cron funcionará, pero es una sobrecarga adicional ...
gmazzap
Creo que escribir un archivo temporal es peor que enviar una plantilla vacía. La plantilla vacía fija se almacenará en caché en el código de operación. El archivo temporal deberá escribirse en el disco, analizarse en frío (sin código de operación) y luego eliminarse. Es mejor minimizar los golpes en el disco sin una buena razón.
Rarst
@Rarst La pregunta nunca fue "qué es mejor" en cuanto al rendimiento. La pregunta se redujo a no tener el archivo de plantilla :)
kaiser
1
tempnam( sys_get_temp_dir(), 'comments.php' )se escribe una vez , puede reutilizar el nombre del archivo y el archivo está vacío , por lo que no utiliza muchos recursos. Además, es fácil de entender en su código. Con mucho, la mejor solución, en mi humilde opinión.
fuxia
3

Como @AlainSchlesser sugirió seguir la ruta (y como las cosas que no funcionan siempre me molestan ), volví a intentar construir un contenedor de flujo para archivos virtuales. No pude resolverlo (léase: leer los valores de retorno en los documentos) por mi cuenta, pero lo resolví con la ayuda de @HPierce en 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 luego permite crear un archivo virtual (no existente):

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

Su función puede ser refactorizada 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 el file_exists()check in core regresa TRUEy require $fileno arroja ningún error.

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

emperador
fuente
1
Grandes hallazgos! Me gusta más este enfoque ;-) Estoy seguro de que hay otras partes del núcleo a las que esto podría aplicarse.
Birgire 01 de
1
¡Votado y gracias! Para las pruebas unitarias, ya hay github.com/mikey179/vfsStream, por lo que no es necesario reinventar la rueda;) Por cierto, me gusta este enfoque, no estoy seguro de usarlo porque el método de excepción me hace sentir felizmente malvado: D
gmazzap
@gmazzap Estoy muy seguro de que así es como te veías cuando te enteraste .
kaiser
@kaiser nah, lo encontré porque I RTFM: P phpunit.de/manual/current/en/…
gmazzap