remove_action ou remove_filter avec des classes externes?

59

Dans une situation où un plugin a encapsulé ses méthodes dans une classe, puis enregistré un filtre ou une action contre l'une de ces méthodes, comment supprimer l'action ou le filtre si vous n'avez plus accès à l'instance de cette classe?

Par exemple, supposons que vous ayez un plugin qui fait ceci:

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

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

new MyClass();

Notant que je n'ai maintenant aucun moyen d'accéder à l'instance, comment puis-je annuler l'enregistrement de la classe? Cela: remove_action( "plugins_loaded", array( MyClass, 'my_action' ) );ne semble pas être la bonne approche - du moins, cela ne semblait pas fonctionner dans mon cas.

Tom Auger
la source
N / P. Est-ce que ci-dessous fonctionne pour vous?
Kaiser

Réponses:

16

La meilleure chose à faire ici est d'utiliser une classe statique. Le code suivant devrait être instructif:

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

Si vous exécutez ce code à partir d'un plugin, vous remarquerez que la méthode de StaticClass ainsi que la fonction seront supprimées de wp_footer.

Mfields
la source
7
Point pris, mais toutes les classes ne peuvent pas simplement être converties en statiques.
Geert le
J'ai accepté cette réponse car elle répondait le plus directement à la question, bien que la réponse d'Otto soit la meilleure pratique. Je remarque ici que je ne pense pas que vous deviez déclarer explicitement statique. D'après mon expérience (bien que je puisse me tromper), vous pouvez simplement traiter la fonction comme s'il s'agissait d'un tableau statique ('MyClass', 'member_function') et cela fonctionne souvent sans le mot clé 'static'.
Tom Auger le
@TomAuger non, vous ne pouvez pas, SEULEMENT si elle est ajoutée en tant que classe statique, vous pouvez utiliser la remove_actionfonction, sinon cela ne fonctionnera pas ... c'est pourquoi j'ai dû écrire ma propre fonction pour qu'elle puisse gérer quand ce n'est pas une classe statique. Cette réponse ne serait la meilleure si votre question concernait votre propre code, sinon vous essayez de supprimer un autre filtre / action de la base de code de quelqu'un d'autre et vous ne pouvez pas le changer en statique
sMyles
78

Chaque fois qu'un plugin crée un new MyClass();, il doit l'affecter à une variable portant un nom unique. De cette façon, l'instance de la classe est accessible.

Donc, s'il le faisait $myclass = new MyClass();, vous pourriez faire ceci:

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

Cela fonctionne car les plugins sont inclus dans l'espace de noms global. Par conséquent, les déclarations de variables implicites dans le corps principal d'un plugin sont des variables globales.

Si le plugin n'enregistre pas l'identifiant de la nouvelle classe quelque part , techniquement, c'est un bug. L'un des principes généraux de la programmation orientée objet est que les objets qui ne sont pas référencés par une variable quelque part sont sujets à nettoyage ou à élimination.

Maintenant, PHP en particulier ne fait pas cela comme le ferait Java, parce que PHP est en quelque sorte une implémentation POO à demi-arsée. Les variables d'instance sont juste des chaînes avec des noms d'objet uniques, en quelque sorte. Ils fonctionnent uniquement en raison de la manière dont l'interaction du nom de la fonction variable fonctionne avec l' ->opérateur. Donc, faire new class()peut parfaitement fonctionner, simplement bêtement. :)

Donc, en bout de ligne, ne faites jamais new class();. Faites $var = new class();et rendez ce $ var accessible d'une certaine manière pour que d'autres bits le référencent.

Edit: ans plus tard

Une chose que j'ai vu beaucoup de plugins est d'utiliser quelque chose de similaire au motif "Singleton". Ils créent une méthode getInstance () pour obtenir l'instance unique de la classe. C'est probablement la meilleure solution que j'ai vue. Exemple de plugin:

class ExamplePlugin
{
    protected static $instance = NULL;

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

La première fois que getInstance () est appelé, il instancie la classe et enregistre son pointeur. Vous pouvez l'utiliser pour accrocher des actions.

Un problème avec ceci est que vous ne pouvez pas utiliser getInstance () dans le constructeur si vous utilisez une telle chose. En effet, new appelle le constructeur avant de définir l'instance $. L'appel de getInstance () à partir du constructeur conduit donc à une boucle infinie et interrompt tout.

Une solution de contournement consiste à ne pas utiliser le constructeur (ou, du moins, à ne pas utiliser getInstance () dans celui-ci), mais à avoir explicitement une fonction "init" dans la classe pour configurer vos actions, etc. Comme ça:

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

Avec quelque chose comme ceci, à la fin du fichier, une fois que la classe a été définie et telle, instancier le plugin devient aussi simple que cela:

ExamplePlugin::init();

Init commence à ajouter vos actions et appelle ainsi getInstance (), qui instancie la classe et s'assure que seul l'un d'entre eux existe. Si vous n'avez pas de fonction init, faites ceci pour instancier initialement la classe:

ExamplePlugin::getInstance();

Pour répondre à la question initiale, supprimer ce crochet d'action de l'extérieur (c'est-à-dire dans un autre plugin) peut alors être effectué comme suit:

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

Mettez cela dans quelque chose qui est accroché au plugins_loadedcrochet d’action et cela annulera l’action accrochée par le plugin original.

Otto
la source
3
+1 Tru dat. C'est clairement une meilleure pratique. Nous devrions tous nous efforcer d’écrire notre code de plugin de cette façon.
Tom Auger le
3
+1 ces instructions m'ont vraiment aidé à supprimer un filtre dans une classe de motifs singleton.
Devin Walker
+1, mais je pense que vous devriez généralement vous accrocher à wp_loaded, pas plugins_loaded, qui peut être appelé trop tôt.
EML le
4
Non, plugins_loadedserait le bon endroit. L' wp_loadedaction se produit après l' initaction. Par conséquent, si votre plug-in effectue une action sur init(et la plupart le font), vous souhaitez alors l'initialiser et le configurer avant. Le plugins_loadedcrochet est le bon endroit pour cette phase de construction.
Otto
13

2 petites fonctions PHP permettant de supprimer les filtres / actions avec la classe "anonymous": https://github.com/herewithme/wp-filters-extras/

ici avec moi
la source
Fonctions très cool. Merci d'avoir posté ça ici!
Tom Auger
Comme mentionné plus haut dans mon message ci-dessous, ceux-ci entreront en ligne de compte dans WordPress 4.7 (sauf si le rapport est mis à jour, mais ne l'a pas encore été en 2 ans)
sMyles
1
Il suffit de noter que le référentiel wp-filters-extras a bien été mis à jour pour la v4.7 et la classe WP_Hook.
Dave Romsey
13

Voici une fonction très documentée que j'ai créée pour supprimer les filtres lorsque vous n'avez pas accès à l'objet de classe (fonctionne avec WordPress 1.2+, y compris 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
la source
2
Question - Avez-vous testé cela en 4.7? Certains changements ont été apportés à la manière dont les rappels sont enregistrés dans des filtres tout neufs. Je n'ai pas regardé votre code en profondeur, mais c'est quelque chose que vous voudrez peut-être consulter: make.wordpress.org/core/2016/09/08/…
Tom Auger
oui, bien sûr, ça va casser dans 4.7
gmazzap
Ahh! Non, je ne l'ai pas fait, mais merci, je vais regarder dans ceci et le mettre à jour afin qu'il soit compatible (si besoin est)
sMyles
1
@TomAuger merci pour le heads up! J'ai mis à jour la fonction, testé en travaillant sur WordPress 4.7+ (avec une compatibilité ascendante toujours maintenue)
sMyles
1
Nous venons de mettre à jour cette
option
2

Les solutions ci-dessus semblent obsolètes, j'ai dû écrire la mienne ...

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
la source
0

Cette fonction est basée sur la réponse @Digerkam. Ajouté compare if $def['function'][0]is string et ça a finalement fonctionné pour moi.

Aussi utiliser $wp_filter[$tag]->remove_filter()devrait le rendre plus stable.

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

Exemple d'utilisation:

Correspondance exacte

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

Toute priorité

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

Toute classe et toute priorité

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

Ce n'est pas une réponse générique, mais une spécifique au thème Avada et à WooCommerce , que d'autres personnes pourraient trouver utile:

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
la source