¿remove_action o remove_filter con clases externas?
En una situación donde un plugin ha encapsulado sus métodos dentro de una clase y luego registró un filter o action contra uno de esos métodos, ¿cómo se elimina la action o el filter si ya no tienes acceso a la instancia de esa clase?
Por ejemplo, supongamos que tienes un plugin que hace esto:
class MyClass {
function __construct() {
add_action( "plugins_loaded", array( $this, 'my_action' ) );
}
function my_action() {
// hacer cosas...
}
}
new MyClass();
Teniendo en cuenta que ahora no tengo forma de acceder a la instancia, ¿cómo desregistro la clase? Esto: remove_action( "plugins_loaded", array( MyClass, 'my_action' ) );
no parece ser el enfoque correcto - al menos, no pareció funcionar en mi caso.
Cuando un plugin crea un new MyClass();
, debería asignarlo a una variable con un nombre único. De esta manera, la instancia de la clase es accesible.
Así que si estaba haciendo $myclass = new MyClass();
, entonces podrías hacer esto:
global $myclass;
remove_action( 'wp_footer', array( $myclass, 'my_action' ) );
Esto funciona porque los plugins se incluyen en el espacio de nombres global, por lo que las declaraciones implícitas de variables en el cuerpo principal de un plugin son variables globales.
Si el plugin no guarda el identificador de la nueva clase en algún lugar, entonces técnicamente, eso es un error. Uno de los principios generales de la Programación Orientada a Objetos es que los objetos que no están siendo referenciados por 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 POO a medias. Las variables de instancia son simplemente cadenas con nombres de objetos ú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 ->
. Así que simplemente hacer new class()
puede funcionar perfectamente, aunque de manera estúpida. :)
Así que, en resumen, nunca hagas new class();
. Haz $var = new class();
y haz que esa $var sea accesible de alguna manera para que otras partes puedan referenciarla.
Edición: años después
Algo que he visto hacer mucho a los plugins 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. Ejemplo de plugin:
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(), instancia la clase y guarda su puntero. Puedes usar eso para enganchar acciones.
Un problema con esto es que no puedes usar getInstance() dentro del constructor si usas algo así. Esto se debe a que new llama al constructor antes de establecer $instance, por lo que llamar a getInstance() desde el constructor lleva a un bucle infinito y rompe todo.
Una solución 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 tus acciones y demás. Algo así:
public static function init() {
add_action( 'wp_footer', array( ExamplePlugin::getInstance(), 'my_action' ) );
}
Con algo así, al final del archivo, después de que la clase ha sido definida y todo, instanciar el plugin se vuelve tan simple como esto:
ExamplePlugin::init();
Init comienza a añadir tus acciones, y al hacerlo llama a getInstance(), que instancia la clase y se asegura de que solo exista una de ellas. Si no tienes una función init, harías esto para instanciar la clase inicialmente en su lugar:
ExamplePlugin::getInstance();
Para abordar la pregunta original, eliminar ese gancho de acción desde fuera (es decir, en otro plugin) puede hacerse así:
remove_action( 'wp_footer', array( ExamplePlugin::getInstance(), 'my_action' ) );
Pon eso en algo enganchado al gancho de acción plugins_loaded
y deshará la acción enganchada por el plugin original.

+1 Totalmente cierto. Claramente esta es una buena práctica. Todos deberíamos esforzarnos por escribir el código de nuestros plugins de esa manera.

+1 Estas instrucciones realmente me ayudaron a eliminar un filtro en una clase con patrón singleton.

+1, pero creo que en general deberías engancharte a wp_loaded
, no a plugins_loaded
, que puede llamarse demasiado temprano.

No, plugins_loaded
sería el lugar correcto. La acción wp_loaded
ocurre después de la acción init
, así que si tu plugin realiza alguna acción en init
(y la mayoría lo hace), entonces querrás inicializar el plugin y configurarlo antes de eso. El hook plugins_loaded
es el lugar adecuado para esa fase de construcción.

Puede que sea un poco tarde para responder. Pero gracias por este aporte. Después de buscar durante horas, descubrimos que no respetamos los principios generales de la Programación Orientada a Objetos, que dice que los objetos que no están siendo referenciados por alguna variable en algún lugar están sujetos a limpieza o eliminación.

Lo mejor que puedes hacer aquí es usar una clase estática. El siguiente código debería 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 ejecutas este código desde un plugin, deberías notar que el método de la StaticClass así como la función serán eliminados de wp_footer.

Entendido el punto, pero no todas las clases pueden simplemente convertirse a estáticas.

Acepté esta respuesta porque responde a la pregunta de manera más directa, aunque la respuesta de Otto es la mejor práctica. Noto aquí que no creo que necesites declarar explícitamente static. Según mi experiencia (aunque podría estar equivocado), puedes tratar la función como si fuera estática array( 'MyClass', 'member_function' ) y a menudo funciona sin la palabra clave 'static'.

@TomAuger no, no puedes, SOLO si se agrega como una clase estática puedes usar la función remove_action
, de lo contrario no funcionará... por eso tuve que escribir mi propia función para manejar cuando no es una clase estática. Esta respuesta solo sería la mejor si tu pregunta fuera sobre tu propio código, de lo contrario estarás intentando eliminar otro filtro/acción del código de otra persona y no podrás cambiarlo a estático

2 pequeñas funciones PHP para permitir eliminar filtros/acciones con clases "anónimas": https://github.com/herewithme/wp-filters-extras/

Como mencionaron otros en mi publicación más abajo, estas funciones dejarán de funcionar en WordPress 4.7 (a menos que el repositorio se actualice, pero no ha tenido cambios en 2 años)

Aquí hay una función extensamente documentada que creé para eliminar filtros cuando no tienes acceso al objeto de clase (funciona con WordPress 1.2+, incluyendo 4.7+):
https://gist.github.com/tripflex/c6518efc1753cf2392559866b4bd1a53
/**
* Eliminar Filtro de Clase Sin Acceso al Objeto de Clase
*
* Para usar remove_filter() de WordPress en un filtro añadido con un callback
* a una clase, necesitas tener acceso a ese objeto de clase o que sea un método estático.
* Esta función te permite eliminar filtros con un callback a una clase a la que no tienes acceso.
*
* Funciona con WordPress 1.2+ (soporte para 4.7+ añadido el 19-09-2016)
* Actualizado el 27-02-2017 para usar la eliminación interna de WordPress en 4.7+ (para evitar advertencias PHP)
*
* @param string $tag Filtro a eliminar
* @param string $class_name Nombre de la clase del callback
* @param string $method_name Nombre del método del callback
* @param int $priority Prioridad del filtro (por defecto 10)
*
* @return bool Si la función fue eliminada
*/
function remove_class_filter( $tag, $class_name = '', $method_name = '', $priority = 10 ) {
global $wp_filter;
// Verificar si el filtro existe
if ( ! isset( $wp_filter[ $tag ] ) ) return FALSE;
/**
* Si la configuración del filtro es un objeto, significa que estamos en WordPress 4.7+
* y la configuración ya no es un array simple, sino un objeto que implementa ArrayAccess.
*
* Para mantener compatibilidad, establecemos $callbacks como referencia al array correcto
* (para que $wp_filter se actualice)
*
* @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 ) ) {
// Crear objeto $fob desde el tag del filtro
$fob = $wp_filter[ $tag ];
$callbacks = &$wp_filter[ $tag ]->callbacks;
} else {
$callbacks = &$wp_filter[ $tag ];
}
// Salir si no hay callbacks para la prioridad especificada
if ( ! isset( $callbacks[ $priority ] ) || empty( $callbacks[ $priority ] ) ) return FALSE;
// Buscar nuestro callback de clase y método
foreach( (array) $callbacks[ $priority ] as $filter_id => $filter ) {
// El filtro debe ser un array - array( $this, 'method' ), si no, siguiente
if ( ! isset( $filter[ 'function' ] ) || ! is_array( $filter[ 'function' ] ) ) continue;
// Si el primer valor no es un objeto, no puede ser una clase
if ( ! is_object( $filter[ 'function' ][ 0 ] ) ) continue;
// Si el método no coincide, siguiente
if ( $filter[ 'function' ][ 1 ] !== $method_name ) continue;
// Método coincide, verificar la clase
if ( get_class( $filter[ 'function' ][ 0 ] ) === $class_name ) {
// WordPress 4.7+ usa remove_filter() nativo
if( isset( $fob ) ){
// Maneja la eliminación, reordenamiento de prioridades, etc.
$fob->remove_filter( $tag, $filter['function'], $priority );
} else {
// Proceso de eliminación legacy (pre 4.7)
unset( $callbacks[ $priority ][ $filter_id ] );
// Si era el único filtro en esa prioridad, eliminarla
if ( empty( $callbacks[ $priority ] ) ) {
unset( $callbacks[ $priority ] );
}
// Si era el único filtro para ese tag, establecer array vacío
if ( empty( $callbacks ) ) {
$callbacks = array();
}
// Eliminar de merged_filters (indica si los filtros están ordenados)
unset( $GLOBALS['merged_filters'][ $tag ] );
}
return TRUE;
}
}
return FALSE;
}
/**
* Eliminar Acción de Clase Sin Acceso al Objeto de Clase
*
* Para usar remove_action() de WordPress en una acción añadida con un callback
* a una clase, necesitas tener acceso a ese objeto o que sea un método estático.
* Esta función permite eliminar acciones con callback a una clase inaccesible.
*
* Funciona con WordPress 1.2+ (soporte para 4.7+ añadido el 19-09-2016)
*
* @param string $tag Acción a eliminar
* @param string $class_name Nombre de la clase del callback
* @param string $method_name Nombre del método del callback
* @param int $priority Prioridad de la acción (por defecto 10)
*
* @return bool Si la función fue eliminada
*/
function remove_class_action( $tag, $class_name = '', $method_name = '', $priority = 10 ) {
return remove_class_filter( $tag, $class_name, $method_name, $priority );
}

Pregunta - ¿has probado esto en la versión 4.7? Ha habido algunos cambios en la forma en que se registran los callbacks en los filtros que son nuevos. No he revisado tu código en profundidad, pero es algo que quizás quieras revisar: https://make.wordpress.org/core/2016/09/08/wp_hook-next-generation-actions-and-filters/

¡Ahh! No lo había hecho pero gracias, definitivamente revisaré esto y actualizaré para que sea compatible (si es necesario)

@TomAuger ¡gracias por el aviso! He actualizado la función, probada y funcionando en WordPress 4.7+ (manteniendo la compatibilidad con versiones anteriores)

Las soluciones anteriores parecen estar desactualizadas, tuve que escribir mi propia función...
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]) ;
}
}
}
}
}
}
}
}

En casos como este, WordPress añade un hash (identificador único) al nombre de la función y lo almacena en la variable global $wp_filter
. Por lo tanto, si utilizas la función remove_filter
, no ocurrirá nada. Incluso si añades el nombre de la clase al nombre de la función como remove_filter('plugins_loaded', ['MiClase', 'mi_accion'])
.
Todo lo que puedes hacer es eliminar manualmente todos los hooks mi_accion
de la variable global $wp_filter
.
Aquí está la función para hacer esto:
function mi_eliminar_filtro($tag, $nombre_funcion, $prioridad = 10){
global $wp_filter;
if( isset($wp_filter[$tag]->callbacks[$prioridad]) and !empty($wp_filter[$tag]->callbacks[$prioridad]) ){
$wp_filter[$tag]->callbacks[$prioridad] = array_filter($wp_filter[$tag]->callbacks[$prioridad], function($v, $k) use ($nombre_funcion){
return ( stripos($k, $nombre_funcion) === false );
}, ARRAY_FILTER_USE_BOTH );
}
}
Úsala así:
mi_eliminar_filtro('plugins_loaded', 'mi_accion');

Esta no es una respuesta genérica, sino 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' );

Encontré este Gist que hacía exactamente lo que necesitaba:
function remove_class_hook( $tag, $class_name = '', $method_name = '', $priority = 10 ) {
global $wp_filter;
$is_hook_removed = false;
if ( ! empty( $wp_filter[ $tag ]->callbacks[ $priority ] ) ) {
$methods = array_filter(wp_list_pluck(
$wp_filter[ $tag ]->callbacks[ $priority ],
'function'
), function ($method) {
/**
* Permitir solo notación de array y string para los hooks, ya que
* estamos buscando eliminar un método exacto de una clase. Y el
* método de la clase se pasa como string de todas formas.
*/
return is_string($method) || is_array($method);
});
$found_hooks = ! empty( $methods ) ? wp_list_filter( $methods, array( 1 => $method_name ) ) : array();
foreach( $found_hooks as $hook_key => $hook ) {
if ( ! empty( $hook[0] ) && is_object( $hook[0] ) && get_class( $hook[0] ) === $class_name ) {
$wp_filter[ $tag ]->remove_filter( $tag, $hook, $priority );
$is_hook_removed = true;
}
}
}
return $is_hook_removed;
}

Estoy bastante seguro de que TripFlex (el autor del gist que encontraste) ES de hecho @sMyles, cuya respuesta (https://wordpress.stackexchange.com/a/239431/3687) ahora es la respuesta aceptada.

Esta función está basada en la respuesta de @Digerkam. Se añadió una comparación para verificar si $def['function'][0]
es una cadena y finalmente funcionó para mí.
También, usar $wp_filter[$tag]->remove_filter()
debería hacerla 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');
});
