13

That's a freaking bad practice I must say. Spent last two hours finding a solution to remove actions and filters added via Anonymous functions.

This is the code used on a Parent Theme, and I need to remove it.

/**
 * Add custom columns to admin comments grid
 *  * Rate that user set.
 */
add_filter( 'manage_edit-comments_columns', function( $default ) {
    $columns['smr_comment_rate']  = __( 'Rate', 'txtdmn' );

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

Got toscho's answer, played with it heavily, but no help. So, is there any other alternative that will remove actions/filters added via anonymous functions?

Thanks

2

4 Answers 4

12

The problem is that you can't distinguish form an anonymous function and another, so yes, it is possible to remove a closure (i.e. anonymous function) but if more than one closure act on same filter at same priority you have to make a choice, remove them all, ore remove only one (without knowing exactly which).

I'll show how to remove them all using a function highly derived from the one in the @toscho answer you posted:

/**
 * Remove an object filter.
 *
 * @param  string $tag                Hook name.
 * @param  string $class              Class name. Use 'Closure' for anonymous functions.
 * @param  string|void $method        Method name. Leave empty for anonymous functions.
 * @param  string|int|void $priority  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] );
      }
    }
  }
}

I've renamed the function remove_object_filter because it can remove all types of object filters: static class methods, dynamic object methods and closures.

The $priority argument is optional, but when removing closures it should be always used, otherwise the function will remove any closure added to the filter, no matter at which priority, because when $priority is omitted, all the filters using the target class/method or closure are removed.

How to use

// remove a static method
remove_object_filter( 'a_filter_hook', 'AClass', 'a_static_method', 10 );

// remove a dynamic method
remove_object_filter( 'a_filter_hook', 'AClass', 'a_dynamic_method', 10 );

// remove a closure
remove_object_filter( 'a_filter_hook', 'Closure', NULL, 10 );
4
  • I've tried this one and many others, but it simply does not work
    – adamj
    Jul 7, 2017 at 3:25
  • @adamj since version 4.7, WordPress introduced new way to handle hooks, so this don't work anymore...
    – gmazzap
    Jul 7, 2017 at 10:05
  • do you know of any alternatives by any chance?
    – adamj
    Jul 10, 2017 at 7:27
  • 1
    @adamj I could update this for 4.7+, but no time now, and not sure when I'll have. It is fine to open a new question, where you link to this Q/A and say this is outdated, that way anyone can answer, so if I will have no time, maybe someone else will. As alternative, you could put a bounty on this Q, explaining that most upvoted answer here don't work in current version of WP...
    – gmazzap
    Jul 10, 2017 at 10:28
4

Anonymous filters and actions can be removed natively using the following:

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

When generating the unique id using spl_object_hash(), anonymous functions are comparable to one another, so the full closure object doesn't need to be re-created again.

If multiple filters or actions are connected to the same tag with the same priority, then it'll remove the latest filter or action which was added. If there's one you need to keep, you would have to remove all the filters up to the one you need removed, then re-add the others as necessary.

// Filter which was added and needs to be removed
add_filter( 'manage_edit-comments_columns', function( $default ) {
    $columns['smr_comment_rate']  = __( 'Rate', 'txtdmn' );

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

// Removes the last anonymous filter to be added
remove_filter( 'manage_edit-comments_columns', function(){} );

This will generally come back to best practices. I'll only ever use anonymous functions as part of a custom theme I'm developing for a client, where I don't want the filter to be overwritten or removed. In any public theme or plugin I develop, I'll use a factory to initialise a class, add all my filters and actions, then store the instance as a static variable.

EDIT

remove_filter using an anonymous function doesn't appear to be working with the latest versions of WordPress and PHP. The function _wp_filter_build_unique_id has been updated since WordPress 5.3.0, and it removed some `spl_object_hash' workarounds, which in turn prevent filters being removed in this manner.

The only way I can now see to remove filters is by manually adjusting the $wp_filter global variable.

global $wp_filter;
unset( $wp_filter[ $tag ]->callbacks[ $priority ][ $identifier ] );
// or
foreach ( $wp_filter[ $tag ]->callbacks[ $priority ] as $identifier => $callback ) {
    // Match identifier as necessary
}
// or
array_pop( $wp_filter[ $tag ]->callbacks[ $priority ] );
2
  • When using function(){} in remove_action() call, I get an error: PHP Recoverable fatal error: Object of class Closure could not be converted to string in I must be missing something or your answer doesn't work (any more)?
    – vlood
    May 5, 2020 at 14:01
  • 1
    @vlood, I can't seem to replicate the error on my end, so I'm unable to determine what's wrong with the limited information you provided. With that in mind, I've done some testing, and while no errors are being thrown, my filters don't appear to be removed any more. The change could be related to a WordPress update, but I think it may instead be related to the version of PHP in use. May 11, 2020 at 8:50
3

What if you add your filter, with the priority 11, so it goes after? That's ugly, but might work in your case.

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

    return $default;
}, 11, 1 );
3

Maybe someone need it: I modified a bit the code of remove_object_filter in @gmazzap answer, to make it works with WP 4.7+:

/**
 * Remove an object filter.
 *
 * @param  string $tag                Hook name.
 * @param  string $class              Class name. Use 'Closure' for anonymous functions.
 * @param  string|void $method        Method name. Leave empty for anonymous functions.
 * @param  string|int|void $priority  Priority
 * @return void
 */
function remove_object_filter( $tag, $class, $method = NULL, $priority = NULL ) {
    global $wp_version;
    $new_wp_filter_struct = false;

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

    if (version_compare( $wp_version, '4.7', '>=' ) && isset($filters->callbacks)) { // new 'wp_filter' structure (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] );
                }
            }
        }
    }
}

I tested it a bit with an anonymous function and seems to work. The code should be compatible also with WP version < 4.7 (but I didn't test it).

2
  • Do you mean PHP < 7.4? WordPress isn't near 7.4. Feb 25, 2021 at 18:26
  • @TonyDjukic: you are right, current Wordpress version is 5.6.2. It's my mistake: I simply reversed the two numbers, it's 4.7, not 7.4! I corrected my answer. Thanks :)
    – Carlo
    Feb 27, 2021 at 13:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.