remove_action oder remove_filter mit externen Klassen?

59

Wie können Sie in einer Situation, in der ein Plugin seine Methoden in einer Klasse gekapselt und dann einen Filter oder eine Aktion für eine dieser Methoden registriert hat, die Aktion oder den Filter entfernen, wenn Sie keinen Zugriff mehr auf die Instanz dieser Klasse haben?

Angenommen, Sie haben ein Plugin, das dies ausführt:

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

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

new MyClass();

Wie kann ich die Registrierung der Klasse aufheben, da ich jetzt keine Möglichkeit mehr habe, auf die Instanz zuzugreifen? Dies remove_action( "plugins_loaded", array( MyClass, 'my_action' ) );scheint nicht der richtige Ansatz zu sein - zumindest schien es in meinem Fall nicht zu funktionieren.

Tom Auger
quelle
N / P. Funktioniert unten A für Sie?
Kaiser

Antworten:

16

Am besten verwenden Sie hier eine statische Klasse. Der folgende Code sollte anweisend sein:

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

Wenn Sie diesen Code von einem Plugin ausführen, sollten Sie beachten, dass die Methode der StaticClass sowie die Funktion von wp_footer entfernt werden.

Mfields
quelle
7
Punkt genommen, aber nicht alle Klassen können einfach in statische umgewandelt werden.
Geert
Ich habe diese Antwort akzeptiert, weil sie die Frage am direktesten beantwortet, obwohl Ottos Antwort die beste Vorgehensweise ist. Ich stelle hier fest, dass ich glaube nicht, dass Sie statisch explizit deklarieren müssen. Ich habe die Erfahrung gemacht (obwohl ich mich irren könnte), dass Sie die Funktion einfach so behandeln können, als wäre sie ein statisches Array ('MyClass', 'member_function') und sie funktioniert oft ohne das Schlüsselwort 'static'.
Tom Auger
@TomAuger Nein, das kannst du nicht, NUR wenn es als statische Klasse hinzugefügt wurde, kannst du die remove_actionFunktion verwenden, sonst funktioniert es nicht. Deshalb musste ich meine eigene Funktion schreiben, um damit umzugehen, wenn es keine statische Klasse ist. Diese Antwort ist nur dann am besten, wenn Ihre Frage Ihren eigenen Code
betrifft. Andernfalls
78

Jedes Mal, wenn ein Plugin ein erstellt new MyClass();, sollte es einer eindeutig benannten Variablen zugewiesen werden. Auf diese Weise kann auf die Instanz der Klasse zugegriffen werden.

Wenn er es also tun würde $myclass = new MyClass();, dann könnten Sie dies tun:

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

Dies funktioniert, weil Plugins im globalen Namespace enthalten sind, sodass implizite Variablendeklarationen im Hauptteil eines Plugins globale Variablen sind.

Wenn das Plugin den Bezeichner der neuen Klasse nicht irgendwo speichert , ist das technisch gesehen ein Fehler. Eines der allgemeinen Prinzipien der objektorientierten Programmierung ist, dass Objekte, auf die von keiner Variablen irgendwo verwiesen wird, bereinigt oder beseitigt werden müssen.

Insbesondere PHP tut dies nicht wie Java, da PHP eine halbherzige OOP-Implementierung ist. Die Instanzvariablen sind nur Strings mit eindeutigen Objektnamen. Sie funktionieren nur aufgrund der Art und Weise, wie die Interaktion mit dem Variablenfunktionsnamen mit dem ->Operator funktioniert . So einfach zu machen new class()kann in der Tat perfekt funktionieren, nur dumm. :)

Unterm Strich also niemals new class();. Machen Sie $var = new class();das $ var auf irgendeine Weise zugänglich, damit andere Bits darauf verweisen können.

Edit: Jahre später

Eine Sache, die ich bei vielen Plugins gesehen habe, ist die Verwendung eines ähnlichen Musters wie "Singleton". Sie erstellen eine getInstance () -Methode, um die einzelne Instanz der Klasse abzurufen. Dies ist wahrscheinlich die beste Lösung, die ich gesehen habe. Beispiel Plugin:

class ExamplePlugin
{
    protected static $instance = NULL;

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

Beim ersten Aufruf von getInstance () wird die Klasse instanziiert und der Zeiger gespeichert. Damit können Sie Aktionen einbinden.

Ein Problem dabei ist, dass Sie getInstance () nicht im Konstruktor verwenden können, wenn Sie so etwas verwenden. Dies liegt daran, dass der Konstruktor von new vor dem Festlegen der $ -Instanz aufgerufen wird. Daher führt der Aufruf von getInstance () aus dem Konstruktor zu einer Endlosschleife und unterbricht alles.

Eine Problemumgehung besteht darin, den Konstruktor nicht zu verwenden (oder zumindest getInstance () darin nicht zu verwenden), sondern explizit eine "init" -Funktion in der Klasse zu haben, um Ihre Aktionen und dergleichen einzurichten. So was:

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

Mit so etwas wird am Ende der Datei, nachdem alle Klassen definiert wurden, das Instanziieren des Plugins so einfach wie folgt:

ExamplePlugin::init();

Init fügt Ihre Aktionen hinzu und ruft dabei getInstance () auf, das die Klasse instanziiert und sicherstellt, dass nur eine von ihnen existiert. Wenn Sie keine init-Funktion haben, würden Sie dies tun, um die Klasse stattdessen zunächst zu instanziieren:

ExamplePlugin::getInstance();

Um die ursprüngliche Frage zu beantworten, können Sie diesen Aktionshaken von außen (auch bekannt als in einem anderen Plugin) folgendermaßen entfernen:

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

Setzen Sie das in etwas ein, das an den plugins_loadedAktionshaken angehängt ist, und es macht die Aktion rückgängig, die vom ursprünglichen Plugin angehängt wird.

Otto
quelle
3
+1 Tru dat. Dies ist eindeutig eine bewährte Methode. Wir sollten uns alle bemühen, unseren Plugin-Code so zu schreiben.
Tom Auger
3
+1 Diese Anweisungen haben mir wirklich geholfen, einen Filter in einer Singleton-Musterklasse zu entfernen.
Devin Walker
+1, aber ich denke, Sie sollten sich im Allgemeinen wp_loadednicht anschließen plugins_loaded, was möglicherweise zu früh aufgerufen wird.
EML
4
Nein, plugins_loadedwäre der richtige Ort. Die wp_loadedAktion findet nach der initAktion statt. Wenn Ihr Plugin also Aktionen ausführt init(und die meisten ausführen ), möchten Sie das Plugin initialisieren und davor einrichten. Der plugins_loadedHaken ist der richtige Ort für diese Bauphase.
Otto
13

2 kleine PHP-Funktionen zum Entfernen von Filtern / Aktionen mit "anonymer" Klasse: https://github.com/herewithme/wp-filters-extras/

hier mit mir
quelle
Sehr coole Funktionen. Danke, dass du das hier gepostet hast!
Tom Auger
Wie bereits erwähnt, brechen diese in WordPress 4.7 ab (es sei denn, das Repo wird aktualisiert, aber nicht in 2 Jahren)
sMyles
1
Ich stelle nur fest, dass das Repo von wp-filters-extras in der Tat für Version 4.7 und die WP_Hook-Klasse aktualisiert wurde.
Dave Romsey
13

Hier ist eine ausführlich dokumentierte Funktion, die ich zum Entfernen von Filtern erstellt habe, wenn Sie keinen Zugriff auf das Klassenobjekt haben (funktioniert mit WordPress 1.2+, einschließlich 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
quelle
2
Frage - haben Sie dies in 4.7 getestet? Es wurden einige Änderungen an der Art und Weise vorgenommen, in der Rückrufe in brandneuen Filtern registriert werden. Ich habe Ihren Code nicht eingehend untersucht, aber Sie sollten ihn überprüfen: make.wordpress.org/core/2016/09/08/…
Tom Auger
yep, ziemlich sicher, dass dies in 4.7 brechen wird
gmazzap
Ahh! Nein, das habe ich nicht getan, aber danke, ich werde dies auf
jeden Fall untersuchen
1
@ TomAuger danke für die Heads-up! Ich habe die Funktion aktualisiert und getestet, dass sie mit WordPress 4.7+ funktioniert (wobei die Abwärtskompatibilität weiterhin erhalten
bleibt
1
Dies wurde
soeben
2

Die obigen Lösungen sehen aus wie veraltet, mussten meine eigenen schreiben ...

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
quelle
0

Diese Funktion basiert auf @ Digerkam Antwort. Compare if $def['function'][0]is string hinzugefügt und es hat endlich für mich funktioniert.

Auch mit $wp_filter[$tag]->remove_filter()sollte es stabiler machen.

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

Anwendungsbeispiel:

Genaue Übereinstimmung

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

Beliebige Priorität

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

Jede Klasse und jede Priorität

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

Dies ist keine generische Antwort, sondern eine für das Avada-Thema und WooCommerce spezifische Antwort, die meines Erachtens für andere hilfreich sein kann:

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' );
Nabrown
quelle