Cegah comments_template () untuk memuat comments.php

9

Saya sedang mengembangkan tema WordPress menggunakan mesin template. Saya ingin kode saya menjadi kompatibel sebanyak mungkin dengan fungsionalitas WP core.

Beberapa konteks dulu

Masalah pertama saya adalah menemukan cara untuk menyelesaikan template mulai dari permintaan WP. Saya memecahkan yang menggunakan perpustakaan saya, Brain \ Hierarchy .

Mengenai get_template_part()dan fungsi-fungsi lain yang memuat parsial suka get_header(), get_footer()dan serupa, itu cukup mudah untuk menulis wrapper ke templat fungsionalitas mesin parsial.

Masalah

Masalah saya sekarang adalah bagaimana memuat template komentar.

Fungsi WordPress comments_template()adalah fungsi ~ 200 baris yang melakukan banyak hal, yang ingin saya lakukan juga untuk kompatibilitas inti maksimum.

Namun, begitu saya menelepon comments_template(), file required, itu adalah yang pertama dari:

  • file dalam konstanta COMMENTS_TEMPLATE, jika ditentukan
  • comments.php di folder tema, jika ditemukan
  • /theme-compat/comments.php di WP menyertakan folder sebagai fallback pilihan terakhir

Singkatnya, tidak ada cara untuk mencegah fungsi memuat file PHP, yang tidak diinginkan bagi saya, karena saya perlu membuat template saya dan tidak hanya menggunakan require.

Solusi saat ini

Saat ini, saya mengirimkan comments.phpfile kosong dan saya menggunakan 'comments_template'hook filter, untuk mengetahui template yang ingin dimuat WordPress, dan menggunakan fitur dari mesin template saya untuk memuat template.

Sesuatu seperti ini:

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

Pertanyaan

Ini berfungsi, apakah core kompatibel, tetapi ... adakah cara untuk membuatnya berfungsi tanpa harus mengirimkan yang kosong comments.php?

Karena saya tidak suka itu.

gmazzap
sumber

Jawaban:

4

Tidak yakin solusi berikut ini lebih baik daripada solusi di OP, anggap saja ini adalah solusi alternatif, mungkin lebih hackish.

Saya pikir Anda dapat menggunakan pengecualian PHP untuk menghentikan eksekusi WordPress ketika 'comments_template'filter diterapkan.

Anda bisa menggunakan kelas pengecualian khusus sebagai DTO untuk membawa templat.

Ini adalah konsep pengecualian:

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

Dengan kelas pengecualian ini tersedia, fungsi Anda menjadi:

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

The finallyblok membutuhkan PHP 5.5+.

Bekerja dengan cara yang sama, dan tidak memerlukan templat kosong.

gmazzap
sumber
4

Saya telah bergumul dengan ini sebelumnya dan solusi saya adalah - itu bisa melumpuhkan dirinya sendiri membutuhkan file, asalkan tidak melakukan apa - apa.

Berikut ini adalah kode yang relevan dari proyek templating Meadow saya :

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

Saya membiarkan comments_template()melalui gerakan untuk mengatur global dan semacamnya, tetapi memberi makan file PHP kosong untuk requiredan pindah ke template Twig sebenarnya untuk output.

Perhatikan bahwa ini harus dapat mencegat comments_template()panggilan awal , yang dapat saya lakukan karena template Twig saya memanggil abstraksi perantara daripada fungsi PHP yang sebenarnya.

Meskipun saya masih harus mengirimkan file kosong untuk itu, saya melakukannya di perpustakaan dan menerapkan tema tidak harus peduli sama sekali.

Jarang
sumber
Terpilih, terima kasih. Saya sudah melihat pendekatan Anda sejak saya menggunakan Meadow sebelumnya. Apa yang saya tidak suka di sini adalah fakta bahwa template kosong harus tetap dikirim. Selain itu, ini mematahkan setiap upaya untuk menggunakan comments_templatefilter atau COMMENTS_TEMPLATEkonstan untuk menyesuaikan template. Yang tidak penting, tetapi, seperti yang saya katakan, saya ingin tetap sebisa mungkin kompatibel dengan inti.
gmazzap
@ gmazzap hmmm ... tidak ada alasan saya tidak bisa menambahkan dukungan untuk filter & konstan dalam pembungkus saya, tetapi masuk ke manajemen mikro.
Paling lambat
3

Solusi: Gunakan file sementara - dengan nama file unik

Setelah banyak lompatan dan merangkak ke sudut-sudut paling kotor dari PHP, saya mengulang pertanyaan itu menjadi:

Bagaimana seseorang bisa mengelabui PHP ke kembali TRUEuntuk file_exists( $file )?

sebagai kode dalam inti saja

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

Kemudian pertanyaan itu diselesaikan lebih cepat:

$template = tempnam( __DIR__, '' );

dan hanya itu. Mungkin lebih baik digunakan wp_upload_dir()sebagai gantinya:

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

Pilihan lain mungkin menggunakan get_temp_dir()wraps WP_TEMP_DIR. Petunjuk: Anehnya kembali ke /tmp/sehingga file tidak akan diawetkan antara reboot, yang /var/tmp/akan. Seseorang dapat melakukan perbandingan string sederhana di akhir dan memeriksa nilai kembali dan kemudian memperbaikinya jika diperlukan - yang tidak dalam hal ini:

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

Sekarang untuk dengan cepat menguji apakah ada kesalahan yang dilemparkan untuk file sementara tanpa konten:

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

Dan: Tidak Ada Kesalahan → berfungsi.

EDIT: Seperti @toscho tunjukkan dalam komentar, masih ada cara yang lebih baik untuk melakukannya:

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

Catatan: Menurut catatan pengguna pada dokumen php.net , sys_get_temp_dir()perilaku ini berbeda di antara sistem. Oleh karena itu hasilnya menghilangkan garis miring dihapus, lalu ditambahkan lagi. Karena bug inti # 22267 diperbaiki, ini juga dapat bekerja pada server Win / IIS sekarang.

Fungsi refactored Anda (tidak diuji):

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 No. 1: tmpfile()akan kembali NULL. Ya benar

Bonus No. 2: file_exists( __DIR__ )akan kembali TRUE. Ya, sungguh ... kalau-kalau Anda lupa.

^ Ini mengarah ke bug aktual di WP core.


Untuk membantu orang lain dalam mode penjelajah dan menemukan (bagian yang tidak berdokumen), saya akan dengan cepat meringkas apa yang saya coba:

Percobaan 1: File sementara di memori

Upaya pertama yang saya lakukan adalah membuat aliran ke file sementara, menggunakan php://temp. Dari dokumen PHP:

Satu-satunya perbedaan antara keduanya adalah bahwa php://memoryakan selalu menyimpan datanya dalam memori, sedangkan php://tempakan menggunakan file sementara setelah jumlah data yang disimpan mencapai batas yang telah ditentukan (standarnya adalah 2 MB). Lokasi file sementara ini ditentukan dengan cara yang sama seperti sys_get_temp_dir()fungsinya.

Kode:

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

Temuan: Tidak, tidak berfungsi.

Percobaan 2: Gunakan file sementara

Ada tmpfile(), jadi mengapa tidak menggunakannya ?!

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

Ya, itu banyak tentang cara pintas ini.

Percobaan 3: Gunakan pembungkus aliran khusus

Selanjutnya saya pikir saya bisa membangun pembungkus aliran khusus dan mendaftar menggunakanstream_wrapper_register() . Lalu saya bisa menggunakan templat virtual dari aliran itu untuk menipu inti agar percaya bahwa kita memiliki file. Contoh kode di bawah ini (saya sudah menghapus seluruh kelas dan sejarah tidak memiliki langkah yang cukup ...)

class TemplateStreamWrapper
{
    public $context;

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

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

Sekali lagi, ini kembali NULLpada file_exists().


Diuji dengan PHP 5.6.20

kaisar
sumber
Saya pikir Percobaan 3 Anda harus bekerja secara teori. Di pembungkus aliran khusus Anda, apakah Anda menerapkan stream_stat()? Saya pikir inilah yang file_exists()akan dipanggil untuk memeriksa ... php.net/manual/en/streamwrapper.stream-stat.php
Alain Schlesser
Terpilih karena cukup baik dan tidak terlalu meretas. Namun, karena kode saya dimaksudkan untuk digunakan dalam berbagai pengaturan, saya khawatir bahwa izin menulis dapat menjadi masalah. Selain itu, file sementara perlu dihapus, yang tidak mudah dengan cepat , karena tidak mudah untuk mencegat jalur penuh yang dikembalikan oleh tempnam(). Menggunakan pekerjaan cron akan berhasil, tapi itu overhead tambahan ...
gmazzap
Saya pikir menulis file sementara lebih buruk daripada mengirim template kosong. Memperbaiki template kosong akan di-cache ke opcode. File temp harus ditulis ke disk, parsing dingin (tanpa opcode), lalu dihapus. Lebih baik meminimalkan hit disk tanpa alasan yang jelas.
Paling lambat
@ Pertama Pertanyaannya tidak pernah adalah "apa yang lebih baik" kinerja bijaksana. Pertanyaan mendidih untuk tidak memiliki file template :)
kaiser
1
tempnam( sys_get_temp_dir(), 'comments.php' )ditulis sekali , Anda dapat menggunakan kembali nama file, dan file tersebut kosong , sehingga tidak menggunakan banyak sumber. Plus mudah untuk dipahami dalam kode Anda. Sejauh ini solusi terbaik, imho.
fuxia
3

Sebagai @AlainSchlesser menyarankan untuk mengikuti rute (dan karena hal-hal yang tidak bekerja selalu mengganggu saya), saya mencoba membangun pembungkus aliran untuk file virtual. Saya tidak bisa menyelesaikannya (baca: membaca nilai-nilai pengembalian pada dokumen) sendiri, tetapi menyelesaikannya dengan bantuan @HPierce pada 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();
    }
}

Anda hanya perlu mendaftarkan kelas baru sebagai protokol baru:

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

Ini kemudian memungkinkan untuk membuat file virtual (tidak ada):

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

Fungsi Anda kemudian dapat di refactored ke:

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

sebagai file_exists()check in core kembali TRUEdan require $filetidak ada kesalahan melempar.

Saya harus mencatat bahwa saya cukup senang bagaimana ini ternyata karena mungkin sangat membantu dengan unit test.

kaisar
sumber
1
Temuan hebat! Saya paling suka pendekatan ini ;-) Saya yakin ada bagian lain dari inti yang bisa diterapkan.
birgire
1
Terpilih dan terima kasih! Untuk unit test sudah ada github.com/mikey179/vfsStream jadi tidak perlu menemukan kembali roda;) Btw, saya suka pendekatan ini, tidak yakin saya akan menggunakan ini karena metode pengecualian membuat saya merasa sangat jahat: D
gmazzap
@ ghazzap Saya sangat yakin ini adalah bagaimana Anda melihat ketika Anda tahu .
kaiser
@kaiser nah, saya menemukannya karena saya RTFM: P phpunit.de/manual/current/en/…
gmazzap