web-dev-qa-db-fra.com

remove_action ou remove_filter avec des classes externes?

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? 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.

57
Tom Auger

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 plug-in, vous remarquerez que la méthode de StaticClass ainsi que la fonction seront supprimées de wp_footer.

15
mfields

Chaque fois qu'un plug-in crée une 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 faisait $myclass = new MyClass();, alors 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 bogue. 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 le fait pas comme le ferait Java, car PHP est en quelque sorte une implémentation à moitié arsée OOP. Les variables d'instance sont juste des chaînes avec des noms d'objet uniques, en quelque sorte. Ils ne fonctionnent que parce que l'interaction nom de fonction variable fonctionne avec l'opérateur ->. Donc, faire new class() peut en effet fonctionner parfaitement, 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 lié au hook action plugins_loaded et cela annulera l'action qui est accrochée par le plugin d'origine.

75
Otto

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

13
herewithme

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

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]) ;
                            }
                        }
                    }
                }
            }
        }
    }
}
2
Digerkam

Cette fonction est basée sur la réponse @Digerkam. Ajout de comparer si $def['function'][0] est une chaîne et que cela a finalement fonctionné pour moi.

Utiliser également $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');
});
0
Jonny