remove_action o remove_filter con clases externas?

59

En una situación en la que un complemento ha encapsulado sus métodos dentro de una clase y luego ha registrado un filtro o acción contra uno de esos métodos, ¿cómo elimina la acción o el filtro si ya no tiene acceso a la instancia de esa clase?

Por ejemplo, suponga que tiene un complemento que hace esto:

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

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

new MyClass();

Teniendo en cuenta que ahora no tengo forma de acceder a la instancia, ¿cómo puedo cancelar el registro de la clase? Esto: remove_action( "plugins_loaded", array( MyClass, 'my_action' ) );no parece ser el enfoque correcto, al menos, no parece funcionar en mi caso.

Tom Auger
fuente
NOTARIO PÚBLICO. ¿A continuación A funciona para ti?
kaiser

Respuestas:

16

Lo mejor que puedes hacer aquí es usar una clase estática. El siguiente código debe ser instructivo:

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 ejecuta este código desde un complemento, debe notar que el método de StaticClass y la función se eliminarán de wp_footer.

mfields
fuente
77
Punto tomado, pero no todas las clases pueden convertirse simplemente para ser estáticas.
Geert
Acepté esta respuesta porque responde la pregunta más directamente, aunque la respuesta de Otto es la mejor práctica. Noto aquí que no creo que necesite declarar explícitamente static. Según mi experiencia (aunque podría estar equivocado), puedes tratar la función como si fuera una matriz estática ('MyClass', 'member_function') y a menudo funciona sin la palabra clave 'static'.
Tom Auger
@TomAuger no, no puedes, SOLO si se agrega como una clase estática puedes usar la remove_actionfunción, de lo contrario no funcionará ... es por eso que tuve que escribir mi propia función para manejar cuando no es una clase estática. Esta respuesta sólo sería el mejor si su pregunta fue sobre su propio código, de lo contrario se intenta eliminar otro filtro / acción por parte de otra persona código base y no se puede cambiar a la electricidad estática
Smyles
78

Cada vez que un complemento crea un new MyClass();, debe asignarlo a una variable con un nombre único. De esa manera, la instancia de la clase es accesible.

Entonces, si él estaba haciendo $myclass = new MyClass();, entonces podrías hacer esto:

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

Esto funciona porque los complementos se incluyen en el espacio de nombres global, por lo que las declaraciones de variables implícitas en el cuerpo principal de un complemento son variables globales.

Si el complemento no guarda el identificador de la nueva clase en algún lugar , técnicamente, eso es un error. Uno de los principios generales de la Programación Orientada a Objetos es que los objetos a los que no hace referencia alguna variable en algún lugar están sujetos a limpieza o eliminación.

Ahora, PHP en particular no hace esto como lo haría Java, porque PHP es una implementación de OOP a medias. Las variables de instancia son solo cadenas con nombres de objeto únicos, algo así. Solo funcionan debido a la forma en que la interacción del nombre de la función variable funciona con el ->operador. Tan solo hacer new class()puede funcionar perfectamente, solo estúpidamente. :)

Entonces, en resumen, nunca lo hagas new class();. Haga $var = new class();y haga que $ var sea accesible de alguna manera para que otros bits lo hagan referencia.

Editar: años después

Una cosa que he visto hacer muchos complementos es usar algo similar al patrón "Singleton". Crean un método getInstance () para obtener la única instancia de la clase. Esta es probablemente la mejor solución que he visto. Plugin de ejemplo:

class ExamplePlugin
{
    protected static $instance = NULL;

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

La primera vez que se llama a getInstance (), crea una instancia de la clase y guarda su puntero. Puede usar eso para enganchar acciones.

Un problema con esto es que no puede usar getInstance () dentro del constructor si usa tal cosa. Esto se debe a que el nuevo llama al constructor antes de establecer la instancia $, por lo que llamar a getInstance () desde el constructor conduce a un bucle infinito y lo rompe todo.

Una solución alternativa es no usar el constructor (o, al menos, no usar getInstance () dentro de él), sino tener explícitamente una función "init" en la clase para configurar sus acciones y demás. Me gusta esto:

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

Con algo como esto, al final del archivo, después de que la clase se haya definido y tal, crear instancias del complemento se vuelve tan simple como esto:

ExamplePlugin::init();

Init comienza a agregar sus acciones, y al hacerlo llama a getInstance (), que crea una instancia de la clase y se asegura de que solo exista una de ellas. Si no tiene una función init, debería hacer esto para instanciar la clase inicialmente en su lugar:

ExamplePlugin::getInstance();

Para abordar la pregunta original, eliminar ese gancho de acción desde el exterior (también conocido como, en otro complemento) se puede hacer así:

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

Ponga eso en algo conectado al plugins_loadedgancho de acción y deshacerá la acción que está siendo enganchada por el complemento original.

Otón
fuente
3
+1 Tru dat. Esta es claramente una mejor práctica. Todos deberíamos esforzarnos por escribir nuestro código de complemento de esa manera.
Tom Auger
3
+1 estas instrucciones realmente me ayudaron a eliminar un filtro en una clase de patrón singleton.
Devin Walker
+1, pero creo que generalmente deberías conectarte wp_loaded, no plugins_loaded, lo que puede llamarse demasiado pronto.
EML
44
No, plugins_loadedsería el lugar correcto. La wp_loadedacción ocurre después de la initacción, por lo que si su complemento realiza alguna acción init(y la mayoría lo hace), entonces desea inicializar el complemento y configurarlo antes de eso. El plugins_loadedgancho es el lugar adecuado para esa fase de construcción.
Otto
13

2 pequeñas funciones PHP para permitir la eliminación de filtro / acción con clase "anónima": https://github.com/herewithme/wp-filters-extras/

aquí conmigo
fuente
Funciones muy chulas. ¡Gracias por publicar eso aquí!
Tom Auger
Como se mencionó anteriormente, en mi publicación a continuación, estos se romperán en WordPress 4.7 (a menos que el repositorio se actualice, pero no en 2 años)
sMyles
1
Solo notando que el repositorio wp-filters-extras ha sido actualizado para v4.7 y la clase WP_Hook.
Dave Romsey
13

Aquí hay una función ampliamente documentada que creé para eliminar filtros cuando no tiene acceso al objeto de clase (funciona con WordPress 1.2+, incluyendo 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
fuente
2
Pregunta: ¿has probado esto en 4.7? Ha habido algunos cambios en la forma en que se registran las devoluciones de llamada en los filtros que son completamente nuevos. No he examinado su código en profundidad, pero es algo que quizás desee consultar: make.wordpress.org/core/2016/09/08/…
Tom Auger
sí, estoy seguro de que esto se romperá en 4.7
gmazzap
Ahh! No, no lo hice, pero gracias. Definitivamente lo investigaré y lo actualizaré para que sea compatible (si es necesario)
SMyles
1
@TomAuger gracias por el aviso! He actualizado la función, probé trabajando en WordPress 4.7+ (con compatibilidad con versiones anteriores aún mantenida)
sMyles
1
Acabo de actualizar esto para usar el método de eliminación interna central (para manejar la iteración media y evitar advertencias de php)
sMyles
2

Las soluciones anteriores parecen anticuadas, tuve que escribir las mías ...

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

Esta función se basa en la respuesta @Digerkam. Se agregó comparar si $def['function'][0]es una cadena y finalmente funcionó para mí.

También el uso $wp_filter[$tag]->remove_filter()debería hacerlo más estable.

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

Ejemplo de uso:

Coincidencia exacta

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

Cualquier prioridad

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

Cualquier clase y cualquier prioridad

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

Esta no es una respuesta genérica, sino una específica para el tema Avada y WooCommerce , que creo que otras personas pueden encontrar útil:

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