remove_action ou remove_filter com classes externas?

59

Em uma situação em que um plug-in encapsulou seus métodos dentro de uma classe e depois registrou um filtro ou ação contra um desses métodos, como você remove a ação ou o filtro se não tem mais acesso à instância dessa classe?

Por exemplo, suponha que você tenha um plugin que faça isso:

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

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

new MyClass();

Observando que agora não tenho como acessar a instância, como cancelo o registro da classe? Isso: remove_action( "plugins_loaded", array( MyClass, 'my_action' ) );não parece ser a abordagem correta - pelo menos, não parecia funcionar no meu caso.

Tom Auger
fonte
N / P. Abaixo um trabalho para você?
kaiser

Respostas:

16

A melhor coisa a fazer aqui é usar uma classe estática. O código a seguir deve ser instrutivo:

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

Se você executar esse código em um plug-in, observe que o método da StaticClass e a função serão removidos do wp_footer.

mfields
fonte
7
Ponto considerado, mas nem todas as classes podem simplesmente ser convertidas para serem estáticas.
Geert
Aceitei esta resposta porque ela responde mais diretamente à pergunta, embora a resposta de Otto seja a melhor prática. Observo aqui que não acho que você precise declarar explicitamente estático. Tem sido minha experiência (embora eu possa estar errado) que você pode apenas tratar a função como se fosse uma matriz estática ('MyClass', 'member_function') e geralmente funciona sem a palavra-chave 'static'.
Tom Auger
@ TomAuger não, você não pode, SOMENTE se for adicionado como uma classe estática, você pode usar a remove_actionfunção, caso contrário não funcionará ... é por isso que eu tive que escrever minha própria função para lidar quando não é uma classe estática. Essa resposta só seria o melhor se sua pergunta foi sobre o seu próprio código, caso contrário, você estará tentando remover outro filtro / ação de outra pessoa base de código e não pode mudá-lo para estático
Smyles
78

Sempre que um plug-in cria um new MyClass();, ele deve atribuí-lo a uma variável com nome exclusivo. Dessa forma, a instância da classe é acessível.

Então, se ele estava fazendo $myclass = new MyClass();, então você poderia fazer o seguinte:

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

Isso funciona porque os plug-ins estão incluídos no espaço para nome global, portanto, as declarações implícitas de variáveis ​​no corpo principal de um plug-in são variáveis ​​globais.

Se o plugin não salvar o identificador da nova classe em algum lugar , tecnicamente, isso é um bug. Um dos princípios gerais da Programação Orientada a Objetos é que objetos que não estão sendo referenciados por alguma variável em algum lugar estão sujeitos a limpeza ou eliminação.

Agora, o PHP em particular não faz isso como o Java faria, porque o PHP é meio que uma implementação de OOP meio arsada. As variáveis ​​de instância são apenas strings com nomes de objetos exclusivos, algo assim. Eles funcionam apenas devido à maneira como a interação do nome da função variável funciona com o ->operador. Então, apenas fazer new class()pode realmente funcionar perfeitamente, apenas estupidamente. :)

Então, linha de fundo, nunca faça new class();. Faça $var = new class();e torne esse $ var acessível de alguma maneira para outros bits fazerem referência a ele.

Edit: anos depois

Uma coisa que eu já vi muitos plugins fazendo é usar algo semelhante ao padrão "Singleton". Eles criam um método getInstance () para obter a única instância da classe. Esta é provavelmente a melhor solução que eu já vi. Exemplo de plug-in:

class ExamplePlugin
{
    protected static $instance = NULL;

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

A primeira vez que getInstance () é chamado, ele instancia a classe e salva seu ponteiro. Você pode usar isso para conectar ações.

Um problema com isso é que você não pode usar getInstance () dentro do construtor se usar isso. Isso ocorre porque o novo chama o construtor antes de definir a instância $, então chamar getInstance () do construtor leva a um loop infinito e quebra tudo.

Uma solução alternativa é não usar o construtor (ou, pelo menos, não usar getInstance () dentro dele), mas ter explicitamente uma função "init" na classe para configurar suas ações e tal. Como isso:

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

Com algo assim, no final do arquivo, após a classe ter sido definida e tal, a instanciação do plug-in torna-se tão simples quanto isto:

ExamplePlugin::init();

O Init começa a adicionar suas ações e, ao fazer isso, chama getInstance (), que instancia a classe e garante que apenas uma delas exista. Se você não tiver uma função init, faça isso para instanciar a classe inicialmente:

ExamplePlugin::getInstance();

Para resolver a questão original, a remoção desse gancho de ação de fora (também conhecido como outro plugin) pode ser feita da seguinte maneira:

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

Coloque isso em algo ligado ao plugins_loadedgancho de ação e ele desfará a ação que está sendo conectada pelo plug-in original.

Otto
fonte
3
+1 Tru dat. Essa é claramente uma prática recomendada. Todos nós devemos nos esforçar para escrever nosso código de plugin dessa maneira.
Tom Auger
3
Marcar com +1 essas instruções realmente me ajudou a remover um filtro em uma classe de padrão singleton.
Devin Walker
+1, mas acho que você geralmente deve se conectar a wp_loaded, não plugins_loaded, que pode ser chamado muito cedo.
EML
4
Não, plugins_loadedseria o lugar correto. A wp_loadedação acontece após a initação; portanto, se o seu plug-in executa alguma ação init(e a maioria faz), você deseja inicializar o plug-in e configurá-lo antes disso. O plugins_loadedgancho é o lugar certo para a fase de construção.
Otto
13

2 pequenas funções PHP para permitir a remoção de filtro / ação com a classe "anônimo": https://github.com/herewithme/wp-filters-extras/

aqui comigo
fonte
Funções muito legais. Obrigado por postar isso aqui!
Tom Auger
Como mencionado haver outras pessoas no meu post abaixo, estes irão quebrar em WordPress 4.7 (a menos que o repo é atualizado, mas não tem em 2 anos)
Smyles
11
Apenas observando que o repositório wp-Filters-extras foi realmente atualizado para a v4.7 e a classe WP_Hook.
Dave Romsey
13

Aqui está uma função amplamente documentada que eu criei para remover filtros quando você não tem acesso ao objeto de classe (funciona com WordPress 1.2+, incluindo 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
fonte
2
Pergunta - você testou isso na versão 4.7? Houve algumas alterações na maneira como os retornos de chamada são registrados em filtros novos. Não examinei seu código detalhadamente, mas é algo que você pode querer conferir: make.wordpress.org/core/2016/09/08/…
Tom Auger
sim, tenho certeza de que isso ocorrerá em 4.7
gmazzap
Ahh! Não, eu não fiz, mas obrigado vou def olhar para isso e atualizar esta por isso é compatível (se necessário)
Smyles
11
@ TomAuger obrigado pela atenção! Eu atualizei a função, testou trabalhando em WordPress 4.7+ (com compatibilidade com versões anteriores ainda mantida)
Smyles
11
Acaba de atualizar isso para usar o método de remoção interna do núcleo (para lidar com meados de iteração e evitar avisos PHP)
Smyles
2

As soluções acima parecem desatualizadas, tive que escrever as minhas ...

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

Esta função é baseada na resposta @Digerkam. Adicionado compare if $def['function'][0]é string e finalmente funcionou para mim.

O uso também $wp_filter[$tag]->remove_filter()deve torná-lo mais estável.

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

Exemplo de uso:

Combinação exata

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

Qualquer prioridade

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

Qualquer classe e qualquer prioridade

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

Esta não é uma resposta genérica, mas uma específica para o tema Avada e o WooCommerce , que acho que outras pessoas podem achar úteis:

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
fonte