Empêcher comments_template () de charger comments.php

9

Je développe un thème WordPress à l'aide d'un moteur de modèle. Je veux 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 a été de trouver un moyen de résoudre le modèle à partir d'une requête WP. J'ai résolu celui-ci en utilisant une de mes bibliothèques, Brain \ Hierarchy .

En ce qui concerne get_template_part()et d' autres fonctions qui charge comme partials get_header(), get_footer()et similaire, il était assez facile à wrapper d'écriture sur le moteur de template fonctionnalité partielle.

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 veux aussi faire pour une compatibilité maximale avec le cœur.

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

  • le fichier dans la constante COMMENTS_TEMPLATE, si définie
  • comments.php dans le dossier du thème, s'il est trouvé
  • /theme-compat/comments.php dans WP inclut le dossier en dernier recours

En bref, il n'y a aucun moyen d'empêcher la fonction de charger un fichier PHP, ce qui n'est pas souhaitable pour moi, car j'ai besoin de rendre mes modèles et pas simplement d'utiliser require.

Solution actuelle

En ce moment, j'expédie un comments.phpfichier vide et j'utilise un 'comments_template'crochet de filtre, pour savoir quel modèle WordPress veut charger, et utiliser la fonctionnalité de mon moteur de modèle pour charger le modèle.

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 ... existe-t-il un moyen de le faire fonctionner sans avoir à expédier un vide comments.php?

Parce que je n'aime pas ça.

gmazzap
la source

Réponses:

4

Je ne suis pas sûr que la solution suivante soit meilleure que la solution dans OP, disons simplement que c'est une solution alternative, probablement plus hackish.

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

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

Ceci est un projet 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 finallybloc nécessite PHP 5.5+.

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

gmazzap
la source
4

J'ai déjà lutté avec cela auparavant et ma solution était - il peut se supprimer en exigeant un fichier, tant qu'il ne fait rien.

Voici le code pertinent de mon projet 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';
}

J'ai laissé comments_template()passer les motions pour configurer des globaux et autres, mais je l'ai alimenté dans un fichier PHP vide requireet je suis passé à mon modèle Twig réel pour la sortie.

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

Alors que je dois encore expédier un fichier vide pour cela, je le fais dans la bibliothèque et l'implémentation du thème n'a pas du tout à s'en soucier.

Rarst
la source
A voté, merci. J'ai déjà vu votre approche depuis que j'ai utilisé Meadow auparavant. Ce que je n'ai pas aimé ici, c'est le fait qu'un modèle vierge doit être expédié de toute façon. De plus, cela rompt toute tentative d'utilisation d'un comments_templatefiltre ou d'une COMMENTS_TEMPLATEconstante pour personnaliser le modèle. Ce qui n'est pas essentiel, mais, comme je l'ai dit, je voulais rester le plus possible compatible avec le core.
gmazzap
@gmazzap hmmm ... aucune raison pour laquelle je n'ai pas pu ajouter de support pour filter & constant dans mon wrapper, mais cela entre dans la microgestion.
Rarst
3

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

Après beaucoup de sauts et de ramper dans les coins les plus sales de PHP, j'ai reformulé la question juste:

Comment peut - on tromper PHP en retour TRUEpour 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. Il serait peut-être préférable d'utiliser à la wp_upload_dir()place:

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

Une autre option pourrait consister à utiliser les get_temp_dir()enveloppes WP_TEMP_DIR. Astuce: Cela revient étrangement à /tmp/ce que les fichiers ne soient pas conservés entre les redémarrages, ce qui /var/tmp/serait le cas. On peut faire une simple comparaison de chaînes à la fin et vérifier la valeur de retour, puis corriger cela au cas où cela serait nécessaire - ce qui n'est pas le cas:

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

Maintenant, pour tester rapidement s'il y a des erreurs lancées pour un fichier temporaire sans contenu:

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

Et: aucune erreur → travail.

EDIT: Comme @toscho l'a souligné dans les commentaires, il existe encore une meilleure façon de le faire:

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

Remarque: Selon une note des utilisateurs sur les documents php.net , le sys_get_temp_dir()comportement diffère selon les systèmes. Par conséquent, le résultat obtient la barre oblique de fin supprimée, puis ajoutée à nouveau. Comme le bogue de base # 22267 est corrigé, 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 Nr.1: tmpfile()reviendra NULL. Ouais vraiment.

Bonus Nr.2: file_exists( __DIR__ )reviendra TRUE. Oui, vraiment… au cas où tu aurais oublié.

^ Cela conduit à un bug réel dans WP core.


Pour aider les autres à passer en mode explorateur et à les retrouver (mal à des pièces non documentées), je résumerai rapidement 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 vers un fichier temporaire à l'aide de php://temp. Depuis les documents PHP:

La seule différence entre les deux est qu'il php://memorystockera toujours ses données en mémoire, alors php://tempqu'il utilisera un fichier temporaire une fois que la quantité de données stockées atteindra 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 sys_get_temp_dir()fonction.

Le code:

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

Conclusion: Non, 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

Ouais, ça à propos de ce raccourci.

Tentative 3: utilisez un wrapper de flux personnalisé

Ensuite, je pensais pouvoir créer un wrapper de flux personnalisé et l' enregistrer à l'aidestream_wrapper_register() . Ensuite, je pourrais utiliser un modèle virtuel de ce flux pour inciter le noyau à croire que nous avons un fichier. Exemple de code ci-dessous (j'ai déjà supprimé la classe complète 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 est revenu NULLsur file_exists().


Testé avec PHP 5.6.20

kaiser
la source
Je pense que votre tentative 3 devrait fonctionner en théorie. Dans votre wrapper de flux personnalisé, avez-vous implémenté stream_stat()? Je pense que c'est ce qui file_exists()va appeler pour faire son chèque ... php.net/manual/en/streamwrapper.stream-stat.php
Alain Schlesser
Surévalué car est assez sympa et pas très hackish. Cependant, comme mon code est destiné à être utilisé dans différentes configurations, je crains que l'autorisation d'écriture ne soit un problème. De plus, les fichiers temporaires doivent être supprimés, ce qui n'est pas si simple à la volée , car il n'est pas facile d'intercepter le chemin complet renvoyé par tempnam(). Utiliser un travail cron fonctionnera, mais c'est un surcoût supplémentaire ...
gmazzap
Je pense que l'écriture d'un fichier temporaire est pire que l'envoi d'un modèle vide. Le modèle vide fixe sera mis en cache dans l'opcode. Le fichier temporaire devra être écrit sur le disque, analysé à froid (pas d'opcode), puis supprimé. Il vaut mieux minimiser les hits de disque sans raison valable.
Rarst
@Rarst La question n'a jamais été "ce qui est mieux" en termes de performances. La question se résumait à ne pas avoir le fichier modèle :)
kaiser
1
tempnam( sys_get_temp_dir(), 'comments.php' )est écrit une fois , vous pouvez réutiliser le nom de fichier et le fichier est vide , donc il n'utilise pas beaucoup de ressources. De plus, il est facile à comprendre dans votre code. De loin la meilleure solution, à mon humble avis.
fuxia
3

Comme @AlainSchlesser a suggéré de suivre l'itinéraire (et comme les choses qui ne fonctionnent pas me dérangent toujours), j'ai réessayé de créer un wrapper de flux pour les fichiers virtuels. Je n'ai pas pu le résoudre (lire: lire les valeurs de retour sur les documents) par moi-même, mais je l'ai résolu avec 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();
    }
}

Vous avez juste besoin d'enregistrer la nouvelle classe en tant que nouveau protocole:

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

Cela permet alors de créer un fichier virtuel (non existant):

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

Votre fonction peut alors être refactorisée pour:

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

comme le file_exists()retour dans le noyau retourne TRUEet require $filene génère aucune erreur.

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

kaiser
la source
1
Grands résultats! Je préfère cette approche ;-) Je suis sûr qu'il y a d'autres parties du noyau auxquelles cela pourrait s'appliquer.
birgire
1
A voté et merci! Pour les tests unitaires, il y a déjà github.com/mikey179/vfsStream donc pas besoin de réinventer la roue;) Btw, j'aime cette approche, je ne suis pas sûr que je vais l'utiliser parce que la méthode d'exception me fait me sentir heureux mal: D
gmazzap
@gmazzap Je suis très sûr que c'est à quoi vous ressembliez quand vous l'avez découvert .
kaiser
@kaiser nah, je l'ai trouvé parce que je RTFM: P phpunit.de/manual/current/en/…
gmazzap