77

In a situation where a plugin has encapsulated its methods within a class and then registered a filter or action against one of those methods, how do you remove the action or the filter if you no longer have access to that class' instance?

For example, suppose you have a plugin that does this:

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

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

new MyClass();

Noting that I now have no way of accessing the instance, how do I unregister the class? This: remove_action( "plugins_loaded", array( MyClass, 'my_action' ) ); doesn't seem to be the right approach - at least, didn't seem to work in my case.

1
  • N/P. Does below A work for you?
    – kaiser
    Dec 9, 2011 at 21:20

9 Answers 9

16

Here's a extensively documented function I created for removing filters when you don't have access to the class object (works with WordPress 1.2+, including 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 ) {
    return remove_class_filter( $tag, $class_name, $method_name, $priority );
}
6
  • 2
    Question - have you tested this in 4.7? There's been some changes to the way callbacks are registered in filters that's brand new. I haven't looked at your code in-depth, but it's something you might want to check out: make.wordpress.org/core/2016/09/08/…
    – Tom Auger
    Sep 15, 2016 at 20:59
  • yep, quite sure this will break in 4.7
    – gmazzap
    Sep 15, 2016 at 22:01
  • Ahh! No I didn't but thank you I will def look into this and update this so it's compatible (if need be)
    – sMyles
    Sep 17, 2016 at 19:54
  • 1
    @TomAuger thanks for the heads up! I've updated the function, tested working on WordPress 4.7+ (with backwards compatibility still maintained)
    – sMyles
    Sep 20, 2016 at 0:10
  • 1
    Just updated this to use the core internal removal method (to handle mid-iteration and prevent php warnings)
    – sMyles
    Feb 27, 2017 at 23:07
97

Whenever a plugin creates a new MyClass();, it should assign it to a uniquely named variable. That way, the instance of the class is accessible.

So if he was doing $myclass = new MyClass();, then you could do this:

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

This works because plugins are included in the global namespace, so implicit variable declarations in the main body of a plugin are global variables.

If the plugin doesn't save the identifier of the new class somewhere, then technically, that's a bug. One of the general principles of Object Oriented Programming is that objects which are not being referenced by some variable somewhere are subject to cleanup or elimination.

Now, PHP in particular doesn't do this like Java would, because PHP is sorta a half-arsed OOP implementation. The instance variables are just strings with unique object names in them, sort of thing. They only work because of the way the variable function name interaction works with the -> operator. So just doing new class() can indeed work perfectly, just stupidly. :)

So, bottom line, never do new class();. Do $var = new class(); and make that $var accessible in some way for other bits to reference it.

Edit: years later

One thing I've seen a lot of plugins doing is to use something similar to the "Singleton" pattern. They create a getInstance() method to get the single instance of the class. This is probably the best solution I've seen. Example plugin:

class ExamplePlugin
{
    protected static $instance = NULL;

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

The first time getInstance() is called, it instantiates the class and saves its pointer. You can use that to hook in actions.

One problem with this is that you can't use getInstance() inside the constructor if you use such a thing. This is because the new calls the constructor before setting the $instance, so calling getInstance() from the constructor leads to an infinite loop and breaks everything.

One workaround is to not use the constructor (or, at least, not to use getInstance() within it), but to explicitly have an "init" function in the class to set up your actions and such. Like this:

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

With something like this, at the end of the file, after the class has been all defined and such, instantiating the plugin becomes as simple as this:

ExamplePlugin::init();

Init starts to add your actions, and in so doing it calls getInstance(), which instantiates the class and makes sure only one of them exists. If you don't have an init function, you would do this to instantiate the class initially instead:

ExamplePlugin::getInstance();

To address the original question, removing that action hook from the outside (aka, in another plugin) can then be done like so:

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

Put that in something hooked to the plugins_loaded action hook and it'll undo the action being hooked by the original plugin.

5
  • 3
    +1 Tru dat. This is clearly a best practice. We should all endeavour to write our plugin code that way.
    – Tom Auger
    Apr 24, 2012 at 17:41
  • 4
    +1 these instructions really helped me to remove a filter in a singleton pattern class. Mar 28, 2014 at 2:01
  • +1, but I think you should generally hook to wp_loaded, not plugins_loaded, which may be called too early.
    – EML
    Apr 28, 2015 at 13:00
  • 4
    No, plugins_loaded would be the correct place. The wp_loaded action happens after the init action, so if your plugin takes any actions on init (and most do), then you want to initialize the plugin and set it up before that. The plugins_loaded hook is the right place for that construction phase.
    – Otto
    Apr 28, 2015 at 13:26
  • This might be a little late to respond. But thank you for this input. After looking for hours we found out that we did not respect the general principles of Object Oriented Programming which is saying objects which are not being referenced by some variable somewhere are subject to cleanup or elimination. Jan 21, 2020 at 1:45
24

The best thing to do here is to use a static class. The following code should be instructional:

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

If you run this code from a plugin you should notice that the method of the StaticClass as well as the function will removed from wp_footer.

3
  • 10
    Point taken, but not all classes can simply be converted to be static.
    – Geert
    Feb 29, 2012 at 11:40
  • I accepted this answer because it answers the question most directly, though Otto's response is the best practice. I note here that I don't think you need to explicitly declare static. It's been my experience (though I could be wrong) that you can just treat the function as though it were static array( 'MyClass', 'member_function' ) and it often works without the 'static' keyword.
    – Tom Auger
    Apr 24, 2012 at 17:40
  • 1
    @TomAuger no you can't, ONLY if it's added as a static class can you use the remove_action function, otherwise it will not work...that's why I had to write my own function to handle when it's not a static class. This answer would only be the best if your question was regarding your own code, otherwise you will be trying to remove another filter/action from someone else's codebase and can't change it to static
    – sMyles
    Sep 20, 2016 at 0:23
24

2 small PHP functions for allow removing filter/action with "anonymous" class : https://github.com/herewithme/wp-filters-extras/

3
  • Very cool functions. Thanks for posting that here!
    – Tom Auger
    Nov 6, 2012 at 14:22
  • As mentioned be others in my post below, these will break in WordPress 4.7 (unless the repo gets updated, but hasn't in 2 years)
    – sMyles
    Sep 20, 2016 at 0:25
  • 1
    Just noting that the wp-filters-extras repo has indeed been updated for v4.7 and the WP_Hook class. May 4, 2017 at 6:28
6

Above solutions look like outdated, had to write my own...

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]) ;
                            }
                        }
                    }
                }
            }
        }
    }
}
2
  • 1
    In case you ares searching for a modern solution, this worked on me with WP 5.6 Dec 16, 2020 at 20:57
  • Confirmed, works in 5.6.2. Other solutions not working in 5.6 Mar 10, 2021 at 0:08
4

In cases like this the Wordpress adds a hash (unique id) to the function name and stores it in the global $wp_filter variable. So if you use remove_filter function nothing will happen. Even if you add the class name to the function name like remove_filter('plugins_loaded', ['MyClass', 'my_action']). All you can is to remove all the my_action hooks from the global $wp_filter variable manually.

Here is the function to do this:

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

use it like:

my_remove_filter('plugins_loaded', 'my_action');
2

This is not a generic answer, but one specific to Avada theme and WooCommerce, which I think other people may find helpful:

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

This function based on @Digerkam answer. Added compare if $def['function'][0] is string and it's finally worked for me.

Also using $wp_filter[$tag]->remove_filter() should make it more stable.

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

Example usage:

Exact match

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

Any priority

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

Any Class and any priority

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

I found this Gist which did what I wanted:

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) {
            /**
             * Allow only array & string notation for hooks, since we're
             * looking to remove an exact method of a class anyway. And the
             * method of the class is passed in as a string anyway.
             */
            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;
}
1

Your Answer

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

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