Empêcher comments_template () de charger comments.php

9

Je développe un thème WordPress à l'aide d'un moteur de template. Je souhaite que mon code soit aussi compatible que possible avec les fonctionnalités de base de WP.

Un peu de contexte d'abord

Mon premier problème était de trouver un moyen de résoudre le modèle à partir d'une requête WP. J'ai résolu ce problème en utilisant l'une de mes bibliothèques, Brain \ Hierarchy .

En ce qui concerne get_template_part() et les autres fonctions qui chargent des partiels tels que get_header() , get_footer() et similaires, il était assez facile d’écrire des fonctionnalités partielles dans le moteur de gabarit.

Le problème

Mon problème est maintenant de savoir comment charger le modèle de commentaires.

La fonction WordPress comments_template() est une fonction de ~ 200 lignes qui fait beaucoup de choses, ce que je vouloir faire aussi bien pour la compatibilité de base maximale.

Cependant, dès que j'appelle comments_template() , un fichier est require d, il est le premier de:

  • le fichier dans la constante COMMENTS_TEMPLATE , si défini
  • comments.php dans le dossier du thème, si trouvé
  • /theme-compat/comments.php dans WP inclut le dossier comme solution de secours de dernier recours

En bref, il est impossible d'empêcher la fonction de charger un fichier PHP, ce qui n'est pas souhaitable pour moi, car je dois rendre mes modèles et ne pas simplement utiliser require .

Solution actuelle

Pour le moment, je fournis un fichier comments.php vide et j'utilise 'comments_template'

Quelque chose comme ça:

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 question

Cela fonctionne, est compatible avec le noyau, mais ... y a-t-il un moyen de le faire fonctionner sans avoir à envoyer un comments.php vide?

Parce que je ne l'aime pas.

    
posée gmazzap 30.05.2016 - 20:09

4 réponses

4

Vous n'êtes pas sûr que la solution suivante soit meilleure que la solution dans OP, disons simplement qu'il s'agit d'une solution alternative, probablement plus bidon.

Je pense que vous pouvez utiliser une exception PHP pour arrêter l'exécution de WordPress lorsque le filtre 'comments_template' est appliqué.

Vous pouvez utiliser une classe d'exception personnalisée comme DTO pour transporter le modèle.

Ceci est un brouillon pour l'exception:

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

Avec cette classe d'exception disponible, votre fonction devient:

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

Le bloc finally requiert PHP 5.5 +.

Fonctionne de la même manière et ne nécessite pas de modèle vide.

    
réponse donnée gmazzap 30.05.2016 - 20:29
4

J'ai déjà eu affaire à cela auparavant et ma solution était la suivante: elle peut se désorganiser en exigeant un fichier, tant qu'elle ne fait rien .

Voici le code pertinent de mon projet de création de modèles 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';
}

Je laisse comments_template() suivre les mouvements pour configurer des éléments globaux, mais je le nourris dans un fichier PHP vide dans require , puis je passe à mon modèle Twig actuel pour la sortie.

Notez que cela nécessite de pouvoir intercepter l'appel initial comments_template() , ce que je peux faire puisque mon modèle Twig appelle une abstraction intermédiaire plutôt qu'une fonction PHP réelle.

Même si je dois toujours envoyer un fichier vide, je le fais dans la bibliothèque et implémenter theme ne doit pas du tout s'en soucier.

    
réponse donnée Rarst 30.05.2016 - 20:55
3

Solution: Utilisez un fichier temporaire - avec un nom de fichier unique

Après de nombreux sauts et avoir parcouru les recoins les plus sales de PHP, j'ai reformulé la question:

  

Comment PHP peut-il tromper pour retourner TRUE à file_exists( $file ) ?

comme le code dans le noyau est juste

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

Ensuite, la question a été résolue plus rapidement:

$template = tempnam( __DIR__, '' );

et c'est tout. Peut-être vaudrait-il mieux utiliser wp_upload_dir() à la place:

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

Une autre option pourrait consister à utiliser get_temp_dir() qui enveloppe WP_TEMP_DIR . Astuce: étrangement, il tombe à /tmp/ , de sorte que les fichiers ne seront pas préservés entre les redémarrages, ce que /var/tmp/ ferait. On peut faire une simple comparaison de chaîne à la fin et vérifier la valeur de retour, puis le corriger au besoin - ce qui n’est pas le cas ici:

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

Maintenant, pour tester rapidement si des erreurs sont renvoyées pour un fichier temporaire sans contenu:

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

Et: Aucune erreur → ne fonctionne.

MODIFIER: Comme l'a souligné @toscho dans les commentaires, il existe toujours une meilleure méthode:

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

Remarque: selon la une note utilisateur sur les documents php.net , le comportement de sys_get_temp_dir() diffère d’un système à l’autre. Par conséquent, le résultat obtient la barre oblique finale supprimée, puis ajoutée à nouveau. Le bogue principal # 22267 est résolu, cela devrait également fonctionner sur les serveurs Win / IIS.

Votre fonction refactorisée (non testée):

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() renverra NULL . Oui, vraiment.

Bonus n ° 2: file_exists( __DIR__ ) renverra TRUE . Oui, vraiment… au cas où tu oublierais.

^ Ceci conduit à un bogue réel dans le noyau de WP.

Pour aider les autres à passer en mode Explorateur et à trouver ceux qui sont mal écrits ou non documentés, je vais rapidement résumer ce que j'ai essayé:

Tentative 1: fichier temporaire en mémoire

La première tentative que j'ai faite a été de créer un flux dans un fichier temporaire, à l'aide de php://temp . D'après la documentation PHP:

  

La seule différence entre les deux réside dans le fait que php://memory stockera toujours ses données en mémoire, alors que php://temp utilisera un fichier temporaire une fois que la quantité de données stockées atteint une limite prédéfinie (la valeur par défaut est 2 Mo). L'emplacement de ce fichier temporaire est déterminé de la même manière que la fonction sys_get_temp_dir() .

Le code:

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

Recherche: Nope, ne fonctionne pas.

Tentative 2: Utiliser un fichier temporaire

Il y a tmpfile() , alors pourquoi ne pas utiliser ça?!

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

Oui, c'est beaucoup à propos de ce raccourci.

Troisième tentative: utiliser un wrapper de flux personnalisé

Ensuite, je pensais pouvoir créer un wrapper de flux personnalisé et enregistrez-le avec stream_wrapper_register() . Ensuite, je pourrais utiliser un modèle virtuel de ce flux pour inciter le cœur à croire que nous avons un fichier. Exemple de code ci-dessous (j'ai déjà supprimé l'intégralité de la classe et l'historique n'a pas assez d'étapes…)

class TemplateStreamWrapper
{
    public $context;

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

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

Encore une fois, cela renvoie NULL sur file_exists() .

Testé avec PHP 5.6.20

    
réponse donnée kaiser 31.05.2016 - 00:41
3

Comme @AlainSchlesser a suggéré de suivre la route (et que les choses qui ne fonctionnent pas me bousillent toujours), J'ai réessayé la construction d'un wrapper de flux pour les fichiers virtuels. Je ne pouvais pas le résoudre (lire: lire les valeurs de retour sur la documentation) tout seul, mais je l'ai résolu à l'aide de @HPierce sur 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();
    }
}

Il vous suffit d’enregistrer la nouvelle classe en tant que nouveau protocole:

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

Ceci permet ensuite de créer un fichier virtuel (non existant):

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

Votre fonction peut alors être refactorisée en:

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

en tant que file_exists() dans le noyau renvoie TRUE et que require $file ne renvoie aucune erreur.

Je dois dire que je suis assez heureux de la façon dont cela s'est avéré, car cela pourrait être vraiment utile pour les tests unitaires.

    
réponse donnée kaiser 31.05.2016 - 23:12

Lire d'autres questions sur les étiquettes