Удалить действия/фильтры, добавленные через анонимные функции

Вопрос или проблема

Это чертовски плохая практика, однако я должен сказать. Провел последние два часа, ища решение для удаления действий и фильтров, добавленных через анонимные функции.

Это код, использованный в родительской теме, и мне нужно его удалить.

/**
 * Добавить пользовательские колонки в админскую сетку комментариев
 * Оценка, установленная пользователем.
 */
add_filter( 'manage_edit-comments_columns', function( $default ) {
    $columns['smr_comment_rate']  = __( 'Оценка', 'txtdmn' );

    return array_slice( $default, 0, 3, true ) + $columns + array_slice( $default, 2, NULL, true );
});

Получил ответ toscho, много с ним экспериментировал, но безрезультатно. Есть ли другой способ, который позволит удалить действия/фильтры, добавленные через анонимные функции?

Спасибо

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

Я покажу, как удалить их все, используя функцию, сильно основанную на той, что в ответе @toscho:

/**
 * Удалить объектный фильтр.
 *
 * @param  string $tag                Имя хука.
 * @param  string $class              Имя класса. Используйте 'Closure' для анонимных функций.
 * @param  string|void $method        Имя метода. Оставьте пустым для анонимных функций.
 * @param  string|int|void $priority  Приоритет
 * @return void
 */
function remove_object_filter( $tag, $class, $method = NULL, $priority = NULL ) {
  $filters = $GLOBALS['wp_filter'][ $tag ];
  if ( empty ( $filters ) ) {
    return;
  }
  foreach ( $filters as $p => $filter ) {
    if ( ! is_null($priority) && ( (int) $priority !== (int) $p ) ) continue;
    $remove = FALSE;
    foreach ( $filter as $identifier => $function ) {
      $function = $function['function'];
      if (
        is_array( $function )
        && (
          is_a( $function[0], $class )
          || ( is_array( $function ) && $function[0] === $class )
        )
      ) {
        $remove = ( $method && ( $method === $function[1] ) );
      } elseif ( $function instanceof Closure && $class === 'Closure' ) {
        $remove = TRUE;
      }
      if ( $remove ) {
        unset( $GLOBALS['wp_filter'][$tag][$p][$identifier] );
      }
    }
  }
}

Я переименовал функцию remove_object_filter, потому что она может удалять все типы объектных фильтров: статические методы классов, динамические методы объектов и замыкания.

Аргумент $priority является необязательным, но при удалении замыканий его всегда следует использовать, иначе функция удалит любое замыкание, добавленное к фильтру, вне зависимости от приоритета, потому что когда $priority пропущен, все фильтры, использующие целевой класс/метод или замыкание, будут удалены.

Как использовать

// удалить статический метод
remove_object_filter( 'a_filter_hook', 'AClass', 'a_static_method', 10 );

// удалить динамический метод
remove_object_filter( 'a_filter_hook', 'AClass', 'a_dynamic_method', 10 );

// удалить замыкание
remove_object_filter( 'a_filter_hook', 'Closure', NULL, 10 );

Анонимные фильтры и действия могут быть удалены нативно с использованием следующего:

remove_filter( $tag, function(){}, $priority )

При генерации уникального идентификатора с помощью spl_object_hash() анонимные функции сопоставимы друг с другом, поэтому полный объект замыкания не нужно создавать заново.

Если несколько фильтров или действий подключены к одному тегу с одинаковым приоритетом, то будет удален последний фильтр или действие, которое было добавлено. Если нужно оставить один, вам нужно будет удалить все фильтры до нужного, а затем повторно добавить остальные по мере необходимости.

// Фильтр, который был добавлен и должен быть удален
add_filter( 'manage_edit-comments_columns', function( $default ) {
    $columns['smr_comment_rate']  = __( 'Оценка', 'txtdmn' );

    return array_slice( $default, 0, 3, true ) + $columns + array_slice( $default, 2, NULL, true );
} );

// Удаляет последний анонимный фильтр, который был добавлен
remove_filter( 'manage_edit-comments_columns', function(){} );

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

ИЗМЕНЕНИЕ

remove_filter, использующий анонимную функцию, по-видимому, не работает с последними версиями WordPress и PHP. Функция _wp_filter_build_unique_id была обновлена с WordPress 5.3.0, и она удалила некоторые обходные пути с использованием `spl_object_hash`, что, в свою очередь, мешает удалять фильтры таким образом.

Единственный способ, который я сейчас вижу для удаления фильтров, это вручную изменить глобальную переменную $wp_filter.

global $wp_filter;
unset( $wp_filter[ $tag ]->callbacks[ $priority ][ $identifier ] );
// или
foreach ( $wp_filter[ $tag ]->callbacks[ $priority ] as $identifier => $callback ) {
    // Сравнивайте идентификатор по мере необходимости
}
// или
array_pop( $wp_filter[ $tag ]->callbacks[ $priority ] );

Может кому-то это будет нужно: я немного модифицировал код функции remove_object_filter в ответе @gmazzap, чтобы она работала с WP 4.7+:

/**
 * Удалить объектный фильтр.
 *
 * @param  string $tag                Имя хука.
 * @param  string $class              Имя класса. Используйте 'Closure' для анонимных функций.
 * @param  string|void $method        Имя метода. Оставьте пустым для анонимных функций.
 * @param  string|int|void $priority  Приоритет
 * @return void
 */
function remove_object_filter( $tag, $class, $method = NULL, $priority = NULL ) {
    global $wp_version;
    $new_wp_filter_struct = false;

    $filters = isset( $GLOBALS['wp_filter'][ $tag ] ) ? $GLOBALS['wp_filter'][ $tag ] : array();
    if ( empty ( $filters ) ) {
        return;
    }

    if (version_compare( $wp_version, '4.7', '>=') && isset($filters->callbacks)) { // новая структура 'wp_filter' (WP >= 4.7)
        $filters = $filters->callbacks;
        $new_wp_filter_struct = true;
    }

    foreach ( $filters as $p => $filter ) {
        if ( ! is_null($priority) && ( (int) $priority !== (int) $p ) ) continue;
        $remove = FALSE;
        foreach ( $filter as $identifier => $function ) {
            $function = $function['function'];
            if (
                is_array( $function )
                && (
                    is_a( $function[0], $class )
                    || ( is_array( $function ) && $function[0] === $class )
                )
            ) {
                $remove = ( $method && ( $method === $function[1] ) );
            } elseif ( $function instanceof Closure && $class === 'Closure' ) {
                $remove = TRUE;
            }
            if ( $remove ) {
                if ($new_wp_filter_struct) {
                    unset( $GLOBALS['wp_filter'][$tag]->callbacks[$p][$identifier] );
                    if (count($GLOBALS['wp_filter'][$tag]->callbacks[$p]) == 0) {
                        unset($GLOBALS['wp_filter'][$tag]->callbacks[$p]);
                    }
                }
                else {
                    unset( $GLOBALS['wp_filter'][$tag][$p][$identifier] );
                }
            }
        }
    }
}

Я немного протестировал это с анонимной функцией, и похоже, что это работает. Код также должен быть совместим с версиями WP < 4.7 (но я этого не тестировал).

Что если вы добавите свой фильтр с приоритетом 11, чтобы он оказался после? Это некрасиво, но может сработать в вашем случае.

add_filter( 'manage_edit-comments_columns', function( $default ) {
    unset( $default['smr_comment_rate'] );

    return $default;
}, 11, 1 );

Ответ или решение

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

Почему удаление анонимных фильтров является сложной задачей

Анонимные функции (или замыкания) не имеют имени, что делает их идентификацию затруднительной. WordPress использует массивы для хранения зарегистрированных фильтров и действий, но поскольку анонимные функции не имеют уникального идентификатора, вы не сможете удалить конкретную анонимную функцию напрямую, если не имеете ее первой копии.

Решение проблемы: удаление анонимных фильтров

Тем не менее, существует ряд подходов для решения этой проблемы.

  1. Удаление всех фильтров с помощью глобальной переменной:

    Вы можете получить доступ к глобальной переменной $wp_filter и программно удалить фильтры. Вот пример функции, которая поможет вам удалить анонимные функции и другие фильтры:

    function remove_object_filter( $tag, $class, $method = NULL, $priority = NULL ) {
       $filters = $GLOBALS['wp_filter'][$tag];
       if ( empty( $filters ) ) {
           return;
       }
       foreach ( $filters as $p => $filter ) {
           if ( !is_null($priority) && ( (int) $priority !== (int) $p ) ) continue;
           $remove = FALSE;
           foreach ( $filter as $identifier => $function ) {
               $function = $function['function'];
               if ( $function instanceof Closure && $class === 'Closure' ) {
                   $remove = TRUE;
               }
               if ( $remove ) {
                   unset( $GLOBALS['wp_filter'][$tag][$p][$identifier] );
               }
           }
       }
    }
  2. Использование remove_filter с анонимной функцией:

    В старых версиях WordPress вы могли попытаться использовать функцию remove_filter, хотя это будет работать не для всех случаев. На данный момент это может не сработать, так как WordPress обновил свою реализацию управления фильтрами:

    remove_filter( 'manage_edit-comments_columns', function() { }, 10 );

    Однако этот метод, учитывая последние обновления WordPress, может не дать ожидаемого результата.

  3. Добавление нового фильтра для переопределения поведения:

    Если вам нужно временно отключить анонимный фильтр, можно добавить новый фильтр с более высоким приоритетом, который будет переопределять поведение оригинального фильтра:

    add_filter( 'manage_edit-comments_columns', function( $default ) {
       // Убираем элемент из столбца
       unset( $default['smr_comment_rate'] );
       return $default;
    }, 11 );

Лучшие практики

Использование анонимных функций в коде может привести к трудностям в поддержке и понимании кода в будущем. Рекомендуется придерживаться следующих практик:

  • Избегайте анонимных функций в публичных темах и плагинах. Это упростит управление вашими фильтрами и действиями.
  • Используйте именованные функции или классы для добавления фильтров и действий. Это позволит вам легко обращаться к нужным фильтрам и действиям в дальнейшем.
  • Документируйте код. Приведите комментарии в коде для объяснения логики работы с фильтрами и действиями, особенно если вы используете сложные конструкции.

Следуя этим рекомендациям, вы обеспечите поддерживаемость кода и упростите задачу удаления фильтров в будущем.

Оцените материал
Добавить комментарий

Капча загружается...