Verhindern Sie, dass comment_template () die Datei comment.php lädt

9

Ich entwickle ein WordPress-Theme mit einer Template-Engine. Ich möchte, dass mein Code so gut wie möglich mit der WP-Kernfunktionalität kompatibel ist.

Ein Kontext zuerst

Mein erstes Problem bestand darin, einen Weg zu finden, um die Vorlage ausgehend von einer WP-Abfrage aufzulösen . Ich habe dieses Problem mit einer meiner Bibliotheken, Brain \ Hierarchy, gelöst .

In Bezug auf get_template_part()und andere Funktionen, die Teilfunktionen wie und ähnliche laden get_header(), get_footer()war es ziemlich einfach, Wrapper in Teilfunktionen der Template-Engine zu schreiben.

Das Thema

Mein Problem ist jetzt, wie man eine Kommentarvorlage lädt.

Die WordPress-Funktion comments_template()ist eine ~ 200-Zeilen-Funktion, die viele Dinge erledigt, die ich auch für maximale Kernkompatibilität tun möchte.

Sobald ich jedoch anrufe comments_template(), ist eine Datei required, es ist die erste von:

  • die Datei in der Konstante COMMENTS_TEMPLATE, falls definiert
  • comments.php im Themenordner, falls gefunden
  • /theme-compat/comments.php in WP enthält Ordner als letzten Ausweg Fallback

Kurz gesagt, es gibt keine Möglichkeit, die Funktion zum Laden einer PHP-Datei zu verhindern, was für mich nicht wünschenswert ist, da ich meine Vorlagen rendern und nicht einfach verwenden muss require.

Aktuelle Lösung

Im Moment versende ich eine leere comments.phpDatei und verwende 'comments_template'Filter Hook, um zu wissen, welche Vorlage WordPress laden möchte, und verwende die Funktion meiner Vorlagen-Engine, um die Vorlage zu laden.

Etwas wie das:

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

Die Frage

Dies funktioniert, ist kernkompatibel, aber ... gibt es eine Möglichkeit, es zum Laufen zu bringen, ohne ein leeres Schiff versenden zu müssen comments.php?

Weil ich es nicht mag.

gmazzap
quelle

Antworten:

4

Wir sind uns nicht sicher, ob die folgende Lösung besser ist als die Lösung in OP. Sagen wir einfach, es handelt sich um eine alternative, wahrscheinlich hackigere Lösung.

Ich denke, Sie können eine PHP-Ausnahme verwenden, um die Ausführung von WordPress zu stoppen, wenn der 'comments_template'Filter angewendet wird.

Sie können eine benutzerdefinierte Ausnahmeklasse als DTO verwenden, um die Vorlage zu übertragen.

Dies ist ein Entwurf für die Ausnahme:

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

Mit dieser verfügbaren Ausnahmeklasse wird Ihre Funktion:

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

Der finallyBlock benötigt PHP 5.5+.

Funktioniert genauso und erfordert keine leere Vorlage.

gmazzap
quelle
4

Ich habe schon früher damit gerungen und meine Lösung war - es kann sich selbst ausschalten, wenn eine Datei benötigt wird, solange es nichts tut .

Hier ist relevanter Code aus meinem Meadow-Vorlagenprojekt :

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

Ich lasse comments_template()die Bewegungen zum Einrichten von Globals und dergleichen durchgehen, füttere sie jedoch mit einer leeren PHP-Datei requireund gehe zur Ausgabe zu meiner eigentlichen Twig-Vorlage über.

Beachten Sie, dass dies in der Lage sein muss, den ersten comments_template()Aufruf abzufangen , was ich tun kann, da meine Twig-Vorlage eher eine zwischengeschaltete Abstraktion als eine tatsächliche PHP-Funktion aufruft.

Während ich noch leere Dateien dafür versenden muss, mache ich das in der Bibliothek und das Implementieren von Themes muss sich überhaupt nicht darum kümmern.

Selten
quelle
Upvoted, danke. Ich habe Ihren Ansatz bereits gesehen, seit ich Meadow verwendet habe. Was mir hier nicht gefallen hat, ist die Tatsache, dass eine leere Vorlage trotzdem versendet werden muss. Darüber hinaus wird jeder Versuch abgebrochen, comments_templateFilter oder COMMENTS_TEMPLATEKonstanten zum Anpassen der Vorlage zu verwenden. Das ist nicht entscheidend, aber wie gesagt, ich wollte so weit wie möglich mit dem Kern kompatibel bleiben.
gmazzap
@gmazzap hmmm ... kein Grund, warum ich Filter & Konstante in meinem Wrapper nicht unterstützen konnte, aber es geht um Mikromanagement.
Rarst
3

Lösung: Verwenden Sie eine temporäre Datei mit einem eindeutigen Dateinamen

Nach vielen Sprüngen und Kriechen in die schmutzigsten Ecken von PHP formulierte ich die Frage wie folgt:

Wie kann man Trick PHP in der Rückkehr TRUEzu file_exists( $file )?

wie der Code im Kern gerade ist

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

Dann wurde die Frage schneller gelöst:

$template = tempnam( __DIR__, '' );

und das ist es. Vielleicht wäre es besser, wp_upload_dir()stattdessen zu verwenden :

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

Eine andere Möglichkeit könnte sein, get_temp_dir()welche Wraps zu verwenden WP_TEMP_DIR. Hinweis: Es wird seltsamerweise darauf zurückgegriffen, /tmp/dass Dateien zwischen Neustarts nicht erhalten bleiben /var/tmp/. Man kann am Ende einen einfachen Zeichenfolgenvergleich durchführen und den Rückgabewert überprüfen und diesen dann beheben, falls er benötigt wird - was in diesem Fall nicht der Fall ist:

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

Um schnell zu testen, ob für eine temporäre Datei ohne Inhalt Fehler ausgegeben wurden:

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

Und: Keine Fehler → funktionieren.

EDIT: Wie @toscho in den Kommentaren betonte , gibt es noch einen besseren Weg, dies zu tun:

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

Hinweis: Laut einem Benutzerhinweis in php.net-Dokumenten unterscheidet sich das sys_get_temp_dir()Verhalten zwischen den Systemen. Daher wird der nachfolgende Schrägstrich entfernt und dann erneut hinzugefügt. Da der Kernfehler # 22267 behoben ist, sollte dies jetzt auch auf Win / IIS-Servern funktionieren.

Ihre überarbeitete Funktion (nicht getestet):

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()wird zurückkehren NULL. Ja wirklich.

Bonus Nr.2: file_exists( __DIR__ )wird zurückkehren TRUE. Ja, wirklich ... falls du es vergessen hast.

^ Dies führt zu einem tatsächlichen Fehler im WP-Kern.


Um anderen zu helfen, in den Explorer-Modus zu wechseln und diese zu finden (schlecht bis undokumentierte Teile), fasse ich schnell zusammen, was ich versucht habe:

Versuch 1: Temporäre Datei im Speicher

Der erste Versuch, den ich unternahm, bestand darin, mithilfe von einen Stream in eine temporäre Datei zu erstellen php://temp. Aus den PHP-Dokumenten:

Der einzige Unterschied zwischen den beiden besteht darin, dass php://memorydie Daten immer im Speicher gespeichert werden, während php://tempeine temporäre Datei verwendet wird, sobald die gespeicherte Datenmenge einen vordefinierten Grenzwert erreicht (der Standardwert beträgt 2 MB). Der Speicherort dieser temporären Datei wird auf dieselbe Weise wie die sys_get_temp_dir()Funktion bestimmt.

Der Code:

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

Finden: Nein, funktioniert nicht.

Versuch 2: Verwenden Sie eine temporäre Datei

Es gibt tmpfile(), also warum nicht nutzen ?!

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

Ja, so viel zu dieser Abkürzung.

Versuch 3: Verwenden Sie einen benutzerdefinierten Stream-Wrapper

Als nächstes dachte ich, ich könnte einen benutzerdefinierten Stream-Wrapper erstellen und ihn mit registrierenstream_wrapper_register() . Dann könnte ich eine virtuelle Vorlage aus diesem Stream verwenden, um den Kern zu täuschen, dass wir eine Datei haben. Beispielcode unten (Ich habe bereits die vollständige Klasse gelöscht und der Verlauf enthält nicht genügend Schritte…)

class TemplateStreamWrapper
{
    public $context;

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

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

Wieder kehrte diese NULLauf file_exists().


Getestet mit PHP 5.6.20

Kaiser
quelle
Ich denke, dass Ihr Versuch 3 theoretisch funktionieren sollte. Haben Sie in Ihrem benutzerdefinierten Stream-Wrapper implementiert stream_stat()? Ich denke, dass dies das ist, was file_exists()aufgerufen wird, um seine Prüfung durchzuführen
Alain Schlesser
Upvoted weil ist ganz nett und nicht sehr hackisch. Da mein Code jedoch für verschiedene Setups vorgesehen ist, befürchte ich, dass die Schreibberechtigung ein Problem darstellen könnte. Darüber hinaus müssen temporäre Dateien gelöscht werden, was nicht so einfach ist , im Fluge , weil es nicht einfach ist , den vollständigen Pfad von zurück abzufangen tempnam(). Die Verwendung eines Cron-Jobs wird funktionieren, aber es ist zusätzlicher Aufwand ...
gmazzap
Ich denke, das Schreiben einer temporären Datei ist schlimmer als der Versand einer leeren Vorlage. Behoben, dass leere Vorlagen in Opcode zwischengespeichert werden. Die temporäre Datei muss auf die Festplatte geschrieben, kalt analysiert (kein Opcode) und dann gelöscht werden. Es ist besser, Festplatten-Treffer ohne guten Grund zu minimieren.
Rarst
@Rarst Die Frage war nie "Was ist besser" in Bezug auf die Leistung. Die Frage lief darauf hinaus, dass die Vorlagendatei nicht vorhanden war :)
Kaiser
1
tempnam( sys_get_temp_dir(), 'comments.php' )Wird einmal geschrieben , können Sie den Dateinamen wiederverwenden , und die Datei ist leer , sodass nicht viele Ressourcen verwendet werden. Außerdem ist es in Ihrem Code leicht zu verstehen. Mit Abstand die beste Lösung, imho.
Fuxia
3

Als @AlainSchlesser vorschlug, der Route zu folgen (und da mich nicht funktionierende Dinge immer nerven), versuchte ich erneut, einen Stream-Wrapper für virtuelle Dateien zu erstellen. Ich konnte es nicht alleine lösen (lesen: Lesen der Rückgabewerte in den Dokumenten), aber ich löste es mit Hilfe von @HPierce auf 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();
    }
}

Sie müssen nur die neue Klasse als neues Protokoll registrieren:

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

Auf diese Weise können Sie eine virtuelle (nicht vorhandene) Datei erstellen:

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

Ihre Funktion kann dann überarbeitet werden in:

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

da der file_exists()check in core zurückkehrt TRUEund der require $filewirft keinen fehler.

Ich muss beachten, dass ich ziemlich glücklich bin, wie sich dies herausstellte, da es bei Unit-Tests wirklich hilfreich sein könnte.

Kaiser
quelle
1
Tolle Ergebnisse! Ich mag diesen Ansatz am liebsten ;-) Ich bin sicher, dass es andere Teile des Kerns gibt, auf die dies angewendet werden könnte.
Birgire
1
Upvoted und danke! Für Unit-Tests gibt es bereits github.com/mikey179/vfsStream, so dass das Rad nicht neu erfunden werden muss;) Übrigens gefällt mir dieser Ansatz, ich bin mir nicht sicher, ob ich ihn verwenden werde, da ich mich aufgrund der Ausnahmemethode glücklich böse fühle: D
gmazzap
@gmazzap Ich bin mir sehr sicher, dass du so ausgesehen hast, als du es herausgefunden hast .
Kaiser
@kaiser nah, ich habe es gefunden, weil ich RTFM: P phpunit.de/manual/current/en/…
gmazzap