Remove Actions/Filters added via Anonymous Functions

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.

Read More
/**
 * 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

Related posts

4 comments

  1. 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 );
    
  2. 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 ] );
    
  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 );
    
  4. 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).

Comments are closed.