remove_action или remove_filter с внешними классами?

59

В ситуации, когда плагин инкапсулировал свои методы в классе, а затем зарегистрировал фильтр или действие для одного из этих методов, как удалить действие или фильтр, если у вас больше нет доступа к экземпляру этого класса?

Например, предположим, что у вас есть плагин, который делает это:

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

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

new MyClass();

Отмечая, что теперь у меня нет возможности получить доступ к экземпляру, как мне отменить регистрацию класса? Это: remove_action( "plugins_loaded", array( MyClass, 'my_action' ) );не кажется правильным подходом - по крайней мере, в моем случае это не сработало.

Том Оже
источник
N / P. Работает ли ниже А у вас?
Кайзер

Ответы:

16

Лучше всего использовать статический класс. Следующий код должен быть инструктивным:

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

Если вы запускаете этот код из плагина, вы должны заметить, что метод StaticClass, а также функция будут удалены из wp_footer.

mfields
источник
7
Точка взята, но не все классы могут быть просто преобразованы в статические.
Герт
Я принял этот ответ, потому что он отвечает на вопрос наиболее прямо, хотя ответ Отто - лучшая практика. Здесь я отмечаю, что не думаю, что вам нужно явно объявлять static. Мой опыт (хотя я могу ошибаться) заключается в том, что вы можете просто обращаться с функцией, как если бы это был статический массив («MyClass», «member_function»), и она часто работает без ключевого слова «static».
Том Ожер
@ TomAuger нет, вы не можете, ТОЛЬКО если он добавлен как статический класс, вы можете использовать remove_actionфункцию, иначе она не будет работать ... поэтому я должен был написать свою собственную функцию для обработки, когда это не статический класс. Этот ответ будет лучшим, только если ваш вопрос касается вашего собственного кода, в противном случае вы будете пытаться удалить другой фильтр / действие из чужой кодовой базы и не сможете изменить его на статический
sMyles
78

Всякий раз, когда плагин создает new MyClass();, он должен назначить его переменной с уникальным именем. Таким образом, экземпляр класса доступен.

Так что, если он делал $myclass = new MyClass();, то вы могли бы сделать это:

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

Это работает, потому что плагины включены в глобальное пространство имен, поэтому неявные объявления переменных в основной части плагина являются глобальными переменными.

Если плагин не сохраняет идентификатор нового класса где-то , то технически это ошибка. Одним из общих принципов объектно-ориентированного программирования является то, что объекты, на которые не ссылается какая-либо переменная, подлежат очистке или удалению.

В частности, PHP, в частности, делает это не так, как Java, потому что PHP - это наполовину реализация ООП. Переменные экземпляра - это просто строки с уникальными именами объектов, что-то вроде этого. Они работают только из-за способа взаимодействия имени переменной функции с ->оператором. Так что просто делать new class()действительно может работать отлично, просто глупо. :)

Итак, суть, никогда не делай new class();. Сделайте $var = new class();и сделайте этот $ var доступным для других битов, чтобы ссылаться на него.

Изменить: годы спустя

Одна вещь, которую я видел во многих плагинах, это использование чего-то похожего на шаблон «Синглтон». Они создают метод getInstance (), чтобы получить единственный экземпляр класса. Это, наверное, лучшее решение, которое я видел. Пример плагина:

class ExamplePlugin
{
    protected static $instance = NULL;

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

При первом вызове getInstance () он создает экземпляр класса и сохраняет его указатель. Вы можете использовать это, чтобы подключить действия.

Одна из проблем заключается в том, что вы не можете использовать getInstance () внутри конструктора, если используете такую ​​вещь. Это потому, что new вызывает конструктор перед установкой $ instance, поэтому вызов getInstance () из конструктора приводит к бесконечному циклу и прерывает все.

Один из обходных путей - не использовать конструктор (или, по крайней мере, не использовать внутри него getInstance ()), но явно иметь функцию «init» в классе для настройки ваших действий и тому подобного. Нравится:

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

Что-то вроде этого, в конце файла, после того, как класс был полностью определен и тому подобное, создание плагина становится таким простым:

ExamplePlugin::init();

Init начинает добавлять ваши действия и при этом вызывает getInstance (), который создает экземпляр класса и проверяет, существует ли только одно из них. Если у вас нет функции инициализации, вы должны сделать это для первоначального создания экземпляра класса:

ExamplePlugin::getInstance();

Чтобы ответить на исходный вопрос, удаление этого хука действия извне (иначе в другом плагине) может быть сделано следующим образом:

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

Поместите это во что-то, зацепленное за plugins_loadedкрючок действия, и оно отменит действие, зацепленное оригинальным плагином.

эфирное масло
источник
3
+1 Тру дат. Это явно лучшая практика. Мы все должны постараться написать наш код плагина таким образом.
Том Оджер
3
+1 эти инструкции действительно помогли мне удалить фильтр в классе одноэлементных шаблонов.
Девин Уокер,
+1, но я думаю, что вы, как правило, должны цепляться wp_loaded, а не plugins_loaded, что может быть названо слишком рано.
EML
4
Нет, plugins_loadedбыло бы правильное место. wp_loadedДействие происходит после initдействия, так что если ваш плагин принимает какие - либо действия на init(и большинство из них), то вы хотите , чтобы инициализировать плагин и установить его до этого. plugins_loadedКрюк является правильным местом для этого этапа строительства.
Отто
13

2 небольшие функции PHP, позволяющие удалить фильтр / действие с помощью «анонимного» класса: https://github.com/herewithme/wp-filters-extras/

здесь со мной
источник
Очень крутые функции. Спасибо за размещение здесь!
Том Ожер
Как уже упоминалось, в моем посте ниже они будут нарушены в WordPress 4.7 (если только репо не будет обновлено, но не будет обновлено через 2 года)
sMyles
1
Просто отметив, что репозиторий wp-filters-extras действительно обновлен для v4.7 и класса WP_Hook.
Дейв Ромси,
13

Вот широко документированная функция, которую я создал для удаления фильтров, когда у вас нет доступа к объекту класса (работает с WordPress 1.2+, включая 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
источник
2
Вопрос - вы проверяли это в 4.7? Произошли некоторые изменения в том, как обратные вызовы регистрируются в совершенно новых фильтрах. Я не смотрел на свой код в глубину, но это то , что вы можете проверить: make.wordpress.org/core/2016/09/08/...
Tom Оже
да, совершенно уверен, что это сломается в 4.7
gmazzap
Ааа! Нет, я не сделал, но спасибо, я определю это и
обновлю
1
@ TomAuger спасибо за головы! Я обновил функцию, протестировал работу на WordPress 4.7+ (с обратной совместимостью все еще поддерживается)
sMyles
1
Просто обновил это, чтобы использовать метод внутреннего удаления ядра (для обработки средней итерации и предотвращения предупреждений php)
sMyles
2

Вышеуказанные решения выглядят как устаревшие, пришлось написать свое ...

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
источник
0

Эта функция основана на ответе @Digerkam. Добавлено сравнение, если $def['function'][0]строка, и это, наконец, работает для меня.

Также использование $wp_filter[$tag]->remove_filter()должно сделать его более стабильным.

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

Пример использования:

Точное совпадение

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

Любой приоритет

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

Любой класс и любой приоритет

add_action('plugins_loaded', function() {
    remove_class_action('plugins_loaded', '', 'my_action');
});
Jonny
источник
0

Это не общий ответ, а специфический для темы Avada и WooCommerce , который, как я думаю, может оказаться полезным для других людей:

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
источник