remove_action atau remove_filter dengan kelas eksternal?

59

Dalam situasi di mana plugin telah merangkum metode-metodenya di dalam kelas dan kemudian mendaftarkan filter atau tindakan terhadap salah satu metode itu, bagaimana Anda menghapus aksi atau filter jika Anda tidak lagi memiliki akses ke instance kelas itu?

Misalnya, Anda memiliki plugin yang melakukan ini:

class MyClass {
    function __construct() {
       add_action( "plugins_loaded", array( $this, 'my_action' ) );
    }

    function my_action() {
       // do stuff...
    }
}

new MyClass();

Memperhatikan bahwa saya sekarang tidak memiliki cara untuk mengakses instance, bagaimana saya membatalkan registrasi kelas? Ini: remove_action( "plugins_loaded", array( MyClass, 'my_action' ) );tampaknya bukan pendekatan yang tepat - setidaknya, sepertinya tidak berhasil dalam kasus saya.

Tom Auger
sumber
N / P. Apakah di bawah ini cocok untuk Anda?
kaiser

Jawaban:

16

Hal terbaik untuk dilakukan di sini adalah menggunakan kelas statis. Kode berikut harus instruksional:

class MyClass {
    function __construct() {
        add_action( 'wp_footer', array( $this, 'my_action' ) );
    }
    function my_action() {
        print '<h1>' . __class__ . ' - ' . __function__ . '</h1>';
    }
}
new MyClass();


class MyStaticClass {
    public static function init() {
        add_action( 'wp_footer', array( __class__, 'my_action' ) );
    }
    public static function my_action() {
        print '<h1>' . __class__ . ' - ' . __function__ . '</h1>';
    }
}
MyStaticClass::init();

function my_wp_footer() {
    print '<h1>my_wp_footer()</h1>';
}
add_action( 'wp_footer', 'my_wp_footer' );

function mfields_test_remove_actions() {
    remove_action( 'wp_footer', 'my_wp_footer' );
    remove_action( 'wp_footer', array( 'MyClass', 'my_action' ), 10 );
    remove_action( 'wp_footer', array( 'MyStaticClass', 'my_action' ), 10 );
}
add_action( 'wp_head', 'mfields_test_remove_actions' );

Jika Anda menjalankan kode ini dari plugin, Anda harus memperhatikan bahwa metode StaticClass dan juga fungsinya akan dihapus dari wp_footer.

ladang
sumber
7
Poin yang diambil, tetapi tidak semua kelas hanya dapat dikonversi menjadi statis.
Geert
Saya menerima jawaban ini karena itu menjawab pertanyaan paling langsung, meskipun tanggapan Otto adalah praktik terbaik. Saya perhatikan di sini bahwa saya tidak berpikir Anda perlu menyatakan statis secara eksplisit. Sudah pengalaman saya (walaupun saya bisa salah) bahwa Anda bisa memperlakukan fungsi seolah-olah itu array statis ('MyClass', 'member_function') dan sering berfungsi tanpa kata kunci 'statis'.
Tom Auger
@ TomAuger tidak Anda tidak bisa, HANYA jika itu ditambahkan sebagai kelas statis dapat Anda gunakan remove_actionfungsi, jika tidak itu tidak akan berhasil ... itu sebabnya saya harus menulis fungsi saya sendiri untuk menangani ketika itu bukan kelas statis. Jawaban ini hanya akan menjadi yang terbaik jika pertanyaan Anda mengenai kode Anda sendiri, jika tidak, Anda akan mencoba untuk menghapus filter / tindakan lain dari basis kode orang lain dan tidak dapat mengubahnya ke statis
sMyles
78

Setiap kali plugin membuat new MyClass();, itu harus menetapkannya ke variabel bernama unik. Dengan begitu, instance kelas dapat diakses.

Jadi jika dia melakukannya $myclass = new MyClass();, maka Anda bisa melakukan ini:

global $myclass;
remove_action( 'wp_footer', array( $myclass, 'my_action' ) );

Ini berfungsi karena plugin termasuk dalam namespace global, jadi deklarasi variabel implisit di bagian utama plugin adalah variabel global.

Jika plugin tidak menyimpan pengenal kelas baru di suatu tempat , maka secara teknis, itu bug. Salah satu prinsip umum Pemrograman Berorientasi Objek adalah bahwa objek yang tidak dirujuk oleh beberapa variabel di suatu tempat dapat dibersihkan atau dihilangkan.

Sekarang, PHP khususnya tidak melakukan ini seperti Java, karena PHP adalah implementasi OOP setengah-arsed. Variabel instance hanya string dengan nama objek unik di dalamnya, semacam itu. Mereka hanya bekerja karena cara interaksi nama fungsi variabel bekerja dengan ->operator. Jadi melakukan sesuatu new class()memang dapat bekerja dengan sempurna, hanya dengan bodoh. :)

Jadi, intinya, jangan pernah lakukan new class();. Lakukan $var = new class();dan buat agar $ var dapat diakses dengan cara tertentu untuk bit lain untuk merujuknya.

Edit: bertahun-tahun kemudian

Satu hal yang saya lihat banyak plugin lakukan adalah menggunakan sesuatu yang mirip dengan pola "Singleton". Mereka membuat metode getInstance () untuk mendapatkan instance tunggal dari kelas. Ini mungkin solusi terbaik yang pernah saya lihat. Plugin contoh:

class ExamplePlugin
{
    protected static $instance = NULL;

    public static function getInstance() {
        NULL === self::$instance and self::$instance = new self;
        return self::$instance;
    }
}

Pertama kali getInstance () dipanggil, instantiate kelas dan menyimpan pointer-nya. Anda dapat menggunakannya untuk mengaitkan tindakan.

Satu masalah dengan ini adalah Anda tidak bisa menggunakan getInstance () di dalam konstruktor jika Anda menggunakan hal seperti itu. Ini karena baru memanggil konstruktor sebelum menetapkan $ instance, jadi memanggil getInstance () dari konstruktor mengarah ke loop tak terbatas dan merusak segalanya.

Salah satu solusinya adalah tidak menggunakan konstruktor (atau, setidaknya, tidak menggunakan getInstance () di dalamnya), tetapi untuk secara eksplisit memiliki fungsi "init" di kelas untuk mengatur tindakan Anda dan semacamnya. Seperti ini:

public static function init() {
    add_action( 'wp_footer', array( ExamplePlugin::getInstance(), 'my_action' ) );
}

Dengan sesuatu seperti ini, di akhir file, setelah kelas telah ditentukan dan semacamnya, instantiating plugin menjadi sesederhana ini:

ExamplePlugin::init();

Init mulai menambahkan tindakan Anda, dan dengan melakukan hal itu memanggil getInstance (), yang membuat instance kelas dan memastikan hanya satu saja yang ada. Jika Anda tidak memiliki fungsi init, Anda akan melakukan ini untuk membuat instance kelas pada awalnya sebagai gantinya:

ExamplePlugin::getInstance();

Untuk menjawab pertanyaan awal, menghapus kait tindakan dari luar (alias, di plugin lain) kemudian dapat dilakukan seperti:

remove_action( 'wp_footer', array( ExamplePlugin::getInstance(), 'my_action' ) );

Masukkan itu ke dalam sesuatu yang dikaitkan dengan plugins_loadedkait tindakan dan itu akan membatalkan tindakan yang dikaitkan oleh plugin asli.

Otto
sumber
3
+1 Tru dat. Ini jelas merupakan praktik terbaik. Kita semua harus berusaha untuk menulis kode plugin seperti itu.
Tom Auger
3
Memberi +1 petunjuk ini benar-benar membantu saya menghapus filter dalam kelas pola tunggal.
Devin Walker
+1, tapi saya pikir Anda harus menghubungkan ke umum wp_loaded, bukan plugins_loaded, yang mungkin disebut terlalu dini.
EML
4
Tidak, plugins_loadedakan menjadi tempat yang benar. The wp_loadedaksi terjadi setelah inittindakan, jadi jika plugin anda mengambil tindakan apapun pada init(dan kebanyakan lakukan), maka Anda ingin menginisialisasi plugin dan mengaturnya sebelum itu. The plugins_loadedkait adalah tempat yang tepat untuk itu tahap konstruksi.
Otto
13

2 fungsi PHP kecil untuk memungkinkan penghapusan filter / aksi dengan kelas "anonim": https://github.com/herewithme/wp-filters-extras/

di sini bersamaku
sumber
Fungsi yang sangat keren. Terima kasih telah memposting di sini!
Tom Auger
Seperti yang disebutkan di posting saya di bawah ini, ini akan rusak di WordPress 4.7 (kecuali repo diperbarui, tetapi belum dalam 2 tahun)
sMyles
1
Hanya mencatat bahwa repo wp-filter-extras memang telah diperbarui untuk v4.7 dan kelas WP_Hook.
Dave Romsey
13

Berikut adalah fungsi yang terdokumentasi secara luas yang saya buat untuk menghapus filter ketika Anda tidak memiliki akses ke objek kelas (berfungsi dengan WordPress 1.2+, termasuk 4.7+):

https://gist.github.com/tripflex/c6518efc1753cf2392559866b4bd1a53

/**
 * Remove Class Filter Without Access to Class Object
 *
 * In order to use the core WordPress remove_filter() on a filter added with the callback
 * to a class, you either have to have access to that class object, or it has to be a call
 * to a static method.  This method allows you to remove filters with a callback to a class
 * you don't have access to.
 *
 * Works with WordPress 1.2+ (4.7+ support added 9-19-2016)
 * Updated 2-27-2017 to use internal WordPress removal for 4.7+ (to prevent PHP warnings output)
 *
 * @param string $tag         Filter to remove
 * @param string $class_name  Class name for the filter's callback
 * @param string $method_name Method name for the filter's callback
 * @param int    $priority    Priority of the filter (default 10)
 *
 * @return bool Whether the function is removed.
 */
function remove_class_filter( $tag, $class_name = '', $method_name = '', $priority = 10 ) {
    global $wp_filter;

    // Check that filter actually exists first
    if ( ! isset( $wp_filter[ $tag ] ) ) return FALSE;

    /**
     * If filter config is an object, means we're using WordPress 4.7+ and the config is no longer
     * a simple array, rather it is an object that implements the ArrayAccess interface.
     *
     * To be backwards compatible, we set $callbacks equal to the correct array as a reference (so $wp_filter is updated)
     *
     * @see https://make.wordpress.org/core/2016/09/08/wp_hook-next-generation-actions-and-filters/
     */
    if ( is_object( $wp_filter[ $tag ] ) && isset( $wp_filter[ $tag ]->callbacks ) ) {
        // Create $fob object from filter tag, to use below
        $fob = $wp_filter[ $tag ];
        $callbacks = &$wp_filter[ $tag ]->callbacks;
    } else {
        $callbacks = &$wp_filter[ $tag ];
    }

    // Exit if there aren't any callbacks for specified priority
    if ( ! isset( $callbacks[ $priority ] ) || empty( $callbacks[ $priority ] ) ) return FALSE;

    // Loop through each filter for the specified priority, looking for our class & method
    foreach( (array) $callbacks[ $priority ] as $filter_id => $filter ) {

        // Filter should always be an array - array( $this, 'method' ), if not goto next
        if ( ! isset( $filter[ 'function' ] ) || ! is_array( $filter[ 'function' ] ) ) continue;

        // If first value in array is not an object, it can't be a class
        if ( ! is_object( $filter[ 'function' ][ 0 ] ) ) continue;

        // Method doesn't match the one we're looking for, goto next
        if ( $filter[ 'function' ][ 1 ] !== $method_name ) continue;

        // Method matched, now let's check the Class
        if ( get_class( $filter[ 'function' ][ 0 ] ) === $class_name ) {

            // WordPress 4.7+ use core remove_filter() since we found the class object
            if( isset( $fob ) ){
                // Handles removing filter, reseting callback priority keys mid-iteration, etc.
                $fob->remove_filter( $tag, $filter['function'], $priority );

            } else {
                // Use legacy removal process (pre 4.7)
                unset( $callbacks[ $priority ][ $filter_id ] );
                // and if it was the only filter in that priority, unset that priority
                if ( empty( $callbacks[ $priority ] ) ) {
                    unset( $callbacks[ $priority ] );
                }
                // and if the only filter for that tag, set the tag to an empty array
                if ( empty( $callbacks ) ) {
                    $callbacks = array();
                }
                // Remove this filter from merged_filters, which specifies if filters have been sorted
                unset( $GLOBALS['merged_filters'][ $tag ] );
            }

            return TRUE;
        }
    }

    return FALSE;
}

/**
 * Remove Class Action Without Access to Class Object
 *
 * In order to use the core WordPress remove_action() on an action added with the callback
 * to a class, you either have to have access to that class object, or it has to be a call
 * to a static method.  This method allows you to remove actions with a callback to a class
 * you don't have access to.
 *
 * Works with WordPress 1.2+ (4.7+ support added 9-19-2016)
 *
 * @param string $tag         Action to remove
 * @param string $class_name  Class name for the action's callback
 * @param string $method_name Method name for the action's callback
 * @param int    $priority    Priority of the action (default 10)
 *
 * @return bool               Whether the function is removed.
 */
function remove_class_action( $tag, $class_name = '', $method_name = '', $priority = 10 ) {
    remove_class_filter( $tag, $class_name, $method_name, $priority );
}
sMyles
sumber
2
Pertanyaan - sudahkah Anda menguji ini dalam 4.7? Sudah ada beberapa perubahan dalam cara callback terdaftar di filter yang baru. Saya belum melihat kode Anda secara mendalam, tetapi ini sesuatu yang mungkin ingin Anda periksa: make.wordpress.org/core/2016/09/08/...
Tom Auger
ya, cukup yakin ini akan pecah di 4,7
gmazzap
Ahh! Tidak, saya tidak, tetapi terima kasih, saya akan melihat dan memperbarui ini sehingga ini kompatibel (jika perlu)
sMyles
1
@TomAuger terima kasih atas perhatiannya! Saya telah memperbarui fungsi, diuji bekerja di WordPress 4.7+ (dengan kompatibilitas mundur masih dipertahankan)
sMyles
1
Baru saja memperbarui ini untuk menggunakan metode penghapusan internal inti (untuk menangani iterasi menengah dan mencegah peringatan php)
sMyles
2

Solusi di atas terlihat seperti ketinggalan zaman, harus menulis sendiri ...

function remove_class_action ($action,$class,$method) {
    global $wp_filter ;
    if (isset($wp_filter[$action])) {
        $len = strlen($method) ;
        foreach ($wp_filter[$action] as $pri => $actions) {
            foreach ($actions as $name => $def) {
                if (substr($name,-$len) == $method) {
                    if (is_array($def['function'])) {
                        if (get_class($def['function'][0]) == $class) {
                            if (is_object($wp_filter[$action]) && isset($wp_filter[$action]->callbacks)) {
                                unset($wp_filter[$action]->callbacks[$pri][$name]) ;
                            } else {
                                unset($wp_filter[$action][$pri][$name]) ;
                            }
                        }
                    }
                }
            }
        }
    }
}
Digerkam
sumber
0

Fungsi ini berdasarkan jawaban @Digerkam. Ditambahkan membandingkan jika $def['function'][0]adalah string dan akhirnya berhasil untuk saya.

Juga menggunakan $wp_filter[$tag]->remove_filter()harus membuatnya lebih stabil.

function remove_class_action($tag, $class = '', $method, $priority = null) : bool {
    global $wp_filter;
    if (isset($wp_filter[$tag])) {
        $len = strlen($method);

        foreach($wp_filter[$tag] as $_priority => $actions) {

            if ($actions) {
                foreach($actions as $function_key => $data) {

                    if ($data) {
                        if (substr($function_key, -$len) == $method) {

                            if ($class !== '') {
                                $_class = '';
                                if (is_string($data['function'][0])) {
                                    $_class = $data['function'][0];
                                }
                                elseif (is_object($data['function'][0])) {
                                    $_class = get_class($data['function'][0]);
                                }
                                else {
                                    return false;
                                }

                                if ($_class !== '' && $_class == $class) {
                                    if (is_numeric($priority)) {
                                        if ($_priority == $priority) {
                                            //if (isset( $wp_filter->callbacks[$_priority][$function_key])) {}
                                            return $wp_filter[$tag]->remove_filter($tag, $function_key, $_priority);
                                        }
                                    }
                                    else {
                                        return $wp_filter[$tag]->remove_filter($tag, $function_key, $_priority);
                                    }
                                }
                            }
                            else {
                                if (is_numeric($priority)) {
                                    if ($_priority == $priority) {
                                        return $wp_filter[$tag]->remove_filter($tag, $function_key, $_priority);
                                    }
                                }
                                else {
                                    return $wp_filter[$tag]->remove_filter($tag, $function_key, $_priority);
                                }
                            }

                        }
                    }
                }
            }
        }

    }

    return false;
}

Contoh penggunaan:

Benar-benar cocok

add_action('plugins_loaded', function() {
    remove_class_action('plugins_loaded', 'MyClass', 'my_action', 0);
});

Prioritas apa pun

add_action('plugins_loaded', function() {
    remove_class_action('plugins_loaded', 'MyClass', 'my_action');
});

Kelas apa pun dan prioritas apa pun

add_action('plugins_loaded', function() {
    remove_class_action('plugins_loaded', '', 'my_action');
});
Jonny
sumber
0

Ini bukan jawaban umum, tetapi satu khusus untuk tema Avada dan WooCommerce , yang saya pikir orang lain mungkin membantu:

function remove_woo_commerce_hooks() {
    global $avada_woocommerce;
    remove_action( 'woocommerce_single_product_summary', array( $avada_woocommerce, 'add_product_border' ), 19 );
}
add_action( 'after_setup_theme', 'remove_woo_commerce_hooks' );
tidak tertangkap
sumber