Как удалить remove_action или remove_filter с внешними классами?
В ситуации, когда плагин инкапсулировал свои методы внутри класса и затем зарегистрировал фильтр или действие для одного из этих методов, как удалить действие или фильтр, если у вас больше нет доступа к экземпляру этого класса?
Например, предположим, у вас есть плагин, который делает следующее:
class MyClass {
function __construct() {
add_action( "plugins_loaded", array( $this, 'my_action' ) );
}
function my_action() {
// выполнить что-то...
}
}
new MyClass();
Учитывая, что теперь у меня нет способа доступа к экземпляру, как мне отменить регистрацию класса? Такой подход: remove_action( "plugins_loaded", array( MyClass, 'my_action' ) );
кажется неверным - по крайней мере, в моем случае он не сработал.
Когда плагин создает экземпляр класса через 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 доступна для других частей кода.
Обновление спустя годы:
Я часто встречал плагины, использующие что-то похожее на паттерн "Одиночка" (Singleton). Они создают метод 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(), который создает экземпляр класса и гарантирует его единственность. Если у вас нет функции init, можно просто вызвать:
ExamplePlugin::getInstance();
Чтобы ответить на исходный вопрос, удаление этого хука извне (например, из другого плагина) можно выполнить так:
remove_action( 'wp_footer', array( ExamplePlugin::getInstance(), 'my_action' ) );
Поместите этот код в функцию, подключенную к хуку plugins_loaded
, и он отменит действие, добавленное исходным плагином.

+1 Абсолютно верно. Это явно лучшая практика. Нам всем следует стремиться писать код плагинов именно таким образом.

+1 Эти инструкции действительно помогли мне удалить фильтр в классе с паттерном Singleton.

+1, но я считаю, что в целом следует использовать хук wp_loaded
, а не plugins_loaded
, который может вызываться слишком рано.

Нет, plugins_loaded
будет правильным местом. Действие wp_loaded
происходит после действия init
, поэтому если ваш плагин выполняет какие-либо действия на init
(а большинство так и делают), то вам нужно инициализировать плагин и настроить его до этого. Хук plugins_loaded
- это правильное место для этой фазы конструирования.

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

Лучшим решением здесь будет использование статического класса. Следующий код демонстрирует это:
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' );
Если запустить этот код из плагина, можно заметить, что метод статического класса, а также функция будут удалены из wp_footer.

Принято к сведению, но не все классы можно просто преобразовать в статические.

Я принял этот ответ, потому что он наиболее прямо отвечает на вопрос, хотя ответ Otto является лучшей практикой. Отмечу, что, по моему мнению, вам не нужно явно объявлять static. Из моего опыта (хотя я могу ошибаться) можно просто обращаться с функцией как со статической array( 'MyClass', 'member_function' ), и это часто работает без ключевого слова 'static'.

@TomAuger нет, вы не можете, ТОЛЬКО если она добавлена как статический класс, вы можете использовать функцию remove_action
, иначе она не сработает... поэтому мне пришлось написать свою собственную функцию для обработки случая, когда класс не статический. Этот ответ был бы лучшим, только если бы ваш вопрос касался вашего собственного кода, в противном случае вы будете пытаться удалить другой фильтр/действие из чужого кода и не сможете изменить его на статический

2 небольшие PHP-функции для удаления фильтров/действий с "анонимными" классами: https://github.com/herewithme/wp-filters-extras/

Как уже отмечали другие в моем посте ниже, эти функции перестанут работать в WordPress 4.7 (если репозиторий не будет обновлен, но он не обновлялся уже 2 года)

Вот подробно документированная функция, которую я создал для удаления фильтров, когда у вас нет доступа к объекту класса (работает с WordPress 1.2+, включая версии 4.7+):
https://gist.github.com/tripflex/c6518efc1753cf2392559866b4bd1a53
/**
* Удаление фильтра класса без доступа к объекту класса
*
* Для использования стандартной функции WordPress remove_filter() на фильтре, добавленном с callback-функцией
* к классу, у вас должен быть доступ к объекту этого класса, либо это должен быть вызов
* статического метода. Данная функция позволяет удалять фильтры с callback-функцией к классу,
* к которому у вас нет доступа.
*
* Работает с WordPress 1.2+ (поддержка 4.7+ добавлена 19-09-2016)
* Обновлено 27-02-2017 для использования внутреннего механизма WordPress для версий 4.7+ (чтобы избежать вывода PHP-предупреждений)
*
* @param string $tag Фильтр для удаления
* @param string $class_name Имя класса для callback-функции фильтра
* @param string $method_name Имя метода для callback-функции фильтра
* @param int $priority Приоритет фильтра (по умолчанию 10)
*
* @return bool Флаг успешного удаления функции.
*/
function remove_class_filter( $tag, $class_name = '', $method_name = '', $priority = 10 ) {
global $wp_filter;
// Проверяем, существует ли указанный фильтр
if ( ! isset( $wp_filter[ $tag ] ) ) return FALSE;
/**
* Если конфигурация фильтра является объектом, значит мы используем WordPress 4.7+ и конфигурация
* больше не является простым массивом, а представляет собой объект, реализующий интерфейс ArrayAccess.
*
* Для обратной совместимости устанавливаем $callbacks равным корректному массиву по ссылке (чтобы обновить $wp_filter)
*
* @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 ) ) {
// Создаем объект $fob из тега фильтра для использования ниже
$fob = $wp_filter[ $tag ];
$callbacks = &$wp_filter[ $tag ]->callbacks;
} else {
$callbacks = &$wp_filter[ $tag ];
}
// Выходим, если нет callback-функций для указанного приоритета
if ( ! isset( $callbacks[ $priority ] ) || empty( $callbacks[ $priority ] ) ) return FALSE;
// Перебираем каждый фильтр указанного приоритета в поисках нашего класса и метода
foreach( (array) $callbacks[ $priority ] as $filter_id => $filter ) {
// Фильтр должен всегда быть массивом - array( $this, 'method' ), если нет - переходим к следующему
if ( ! isset( $filter[ 'function' ] ) || ! is_array( $filter[ 'function' ] ) ) continue;
// Если первое значение в массиве не объект, это не может быть класс
if ( ! is_object( $filter[ 'function' ][ 0 ] ) ) continue;
// Если метод не совпадает с искомым, переходим к следующему
if ( $filter[ 'function' ][ 1 ] !== $method_name ) continue;
// Метод совпал, проверяем класс
if ( get_class( $filter[ 'function' ][ 0 ] ) === $class_name ) {
// Для WordPress 4.7+ используем стандартную remove_filter(), так как мы нашли объект класса
if( isset( $fob ) ){
// Обрабатывает удаление фильтра, сброс ключей приоритета callback-функций во время итерации и т.д.
$fob->remove_filter( $tag, $filter['function'], $priority );
} else {
// Используем старый механизм удаления (до версии 4.7)
unset( $callbacks[ $priority ][ $filter_id ] );
// и если это был единственный фильтр в этом приоритете, удаляем приоритет
if ( empty( $callbacks[ $priority ] ) ) {
unset( $callbacks[ $priority ] );
}
// и если это единственный фильтр для данного тега, устанавливаем тег в пустой массив
if ( empty( $callbacks ) ) {
$callbacks = array();
}
// Удаляем этот фильтр из merged_filters, который указывает, были ли фильтры отсортированы
unset( $GLOBALS['merged_filters'][ $tag ] );
}
return TRUE;
}
}
return FALSE;
}
/**
* Удаление действия класса без доступа к объекту класса
*
* Для использования стандартной функции WordPress remove_action() на действии, добавленном с callback-функцией
* к классу, у вас должен быть доступ к объекту этого класса, либо это должен быть вызов
* статического метода. Данная функция позволяет удалять действия с callback-функцией к классу,
* к которому у вас нет доступа.
*
* Работает с WordPress 1.2+ (поддержка 4.7+ добавлена 19-09-2016)
*
* @param string $tag Действие для удаления
* @param string $class_name Имя класса для callback-функции действия
* @param string $method_name Имя метода для callback-функции действия
* @param int $priority Приоритет действия (по умолчанию 10)
*
* @return bool Флаг успешного удаления функции.
*/
function remove_class_action( $tag, $class_name = '', $method_name = '', $priority = 10 ) {
return remove_class_filter( $tag, $class_name, $method_name, $priority );
}

Вопрос - вы тестировали это в версии 4.7? Там были изменения в способе регистрации колбэков в фильтрах, это совсем новое. Я не углублялся в ваш код, но это то, что вам стоит проверить: https://make.wordpress.org/core/2016/09/08/wp_hook-next-generation-actions-and-filters/

Ой! Нет, я не проверял, но спасибо, я обязательно изучу это и обновлю код для совместимости (если потребуется)

@TomAuger спасибо за информацию! Я обновил функцию, протестировал работу на WordPress 4.7+ (с сохранением обратной совместимости)

Вышеуказанные решения выглядят устаревшими, пришлось написать своё...
// Функция для удаления действия класса из хука WordPress
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]) ;
}
}
}
}
}
}
}
}

В таких случаях WordPress добавляет хэш (уникальный идентификатор) к имени функции и сохраняет его в глобальной переменной $wp_filter
. Поэтому если вы используете функцию remove_filter
, ничего не произойдет. Даже если вы добавите имя класса к имени функции, например remove_filter('plugins_loaded', ['MyClass', 'my_action'])
.
Единственное, что можно сделать — это вручную удалить все хуки my_action
из глобальной переменной $wp_filter
.
Вот функция для этого:
function my_remove_filter($tag, $function_name, $priority = 10){
global $wp_filter;
if( isset($wp_filter[$tag]->callbacks[$priority]) and !empty($wp_filter[$tag]->callbacks[$priority]) ){
$wp_filter[$tag]->callbacks[$priority] = array_filter($wp_filter[$tag]->callbacks[$priority], function($v, $k) use ($function_name){
return ( stripos($k, $function_name) === false );
}, ARRAY_FILTER_USE_BOTH );
}
}
Используйте ее следующим образом:
my_remove_filter('plugins_loaded', 'my_action');

Это не универсальное решение, а конкретно для темы 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' );

Я нашёл этот Gist, который делал то, что мне нужно:
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) {
/**
* Разрешаем только массивы и строки для хуков, так как мы
* ищем конкретный метод класса для удаления. И метод класса
* в любом случае передаётся в виде строки.
*/
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;
}

Я почти уверен, что TripFlex (автор найденного gist) — это и есть @sMyles, чей ответ (https://wordpress.stackexchange.com/a/239431/3687) теперь является принятым ответом.

Эта функция основана на ответе @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');
});
