WP Plugins: How to remove a Filter

Worth to know for wordpress plugin authors: Making your plugin to safely unregister or remove a hook (filter or action) is not possible with the wordpress plugin API. Why? you might ask yourself. Even the name of the remove_filter() function suggests it and the codex documentation does say so as well.

But: It does not work, because this function does not work as intended. And beyond your own hooks, it has the potential to hinder the blog from working properly as it has the potential to remove other hooks then the specified one.

Does not is not exactly right, it does, but only sometimes. That’s specifically a bummer if you write plugins yourself, because you can not safely rely on it. For example, if running your plugin makes use of PHP 5.2 or greater it works. But it does not if it’s running on PHP 4 or PHP 5 lower than 5.2. And next to that, there are cases when even using that higher PHP version is not enough – I know this sounds a bit esoteric.

These deficiencies have not been properly documented for various reasons, but that should not be the upfront topic of my post, I’ll spare us the history how it came to that point for the moment. Instead let’s take a look on how to deal with the known deficiencies practically incl. code examples to more safely write plugins that need to remove their own filters.

The options

  1. Do not use remove_filter().
  2. Raise your plugin PHP requirements to PHP 5.2 and above.

 
The second option might be the overall better looking one. It will save you some of the hassles wordpress has and it might be good practice for your plugin users as well. And the day is near, that this PHP version becomes the requirement. But that might not be an option and it does not solve some other problems with remove_filter().

So it’s not totally save to insist on PHP 5.2. Don’t take me wrong, I don’t want to slow you down to go out there and propagate PHP 5.2+ use, that’s always a good idea. A better one even to look for the current PHP version, 5.3, as PHP 5.2 has reached the end of active support in July.

But let’s consider the first option to just not rely on remove_filter() for your own hooks. Luckily, it is nothing complicated to deal with. I give two examples which are easy to use. The first one is for a filter to be used one-time only. The second example is a filter that can be switched off and on to offer more control.

Creating a one-time Filter

One scenario remove_filter() is often used for is to create a filter that works only once. Normally in the filter, the code unregisters itself. So how to disable it after it’s first use without relying on the remove_filter() function? Easy, just do it your own:

function my_filter($input) {
  static $count=0;
  if ($count++)
    return $input;

  // your filter code goes here
  $output = 'filtered';

  return $output;
}

This code-example is a function to be used as a hook that can only be called once. To be precise, it can be called multiple times, but it will do the filtering only once. In case it’s called the second, third or any other time, it will short-circuit itself. To achieve this, it has a static variable $count that keeps track of the number of calls. Static variables keep their value between function calls while being initialized on first use (here: static $count=0;).

On every function call, $count is checked for being TRUE (that is non-zero for integers). And this is only the case for the first time, because in each call, it’s incremented by one after the check. So only the first time the function is called $count is FALSE and the function won’t return immediately. This filter will only do the filtering once. Mission accomplished.

Multi-Pass removable Filter

A one-time filter is nice and easy to write. But using the filter from the first example more than once is not possible. You might need more control, so slightly more logic needs to be added to finer control the filter being on or off. Take a look on the following code, which is a blueprint for such a behaviour:

function my_filter($input, $control = NULL) {
  static $disabled = false;
  (true === $control || false === $control) && ($disabled = !$control);
  if ($disabled) return $input;

  // your filter code goes here
  $output = 'filtered';

  return $output;
}

There is an optional parameter at the end of the filter function definition used to control whether or not the filter is enabled. When invoked as a wordpress plugin API pointcut, this parameter will be NULL. But you can call your filter with that additional parameter to switch it off or on. This is done by using FALSE to disable or TRUE to enable it. See this demonstration code:

// filter is enabled by default
echo my_filter('test 1'); # echoes "filtered"

// disable filter
my_filter(0, false);
echo my_filter('test 2'); # echoes "test 2"

// enable filter
my_filter(0, true);
echo my_filter('test 3'); # echoes "filtered"

Instead of using the buggy remove_filter() wordpress API function, you can control the activity of the filter properly regardless of how well or bad the API works. The key element are static variables again to store the filters state. This method can be useful for other filter functionality as well.

Multiple Instance Filters

If you’re using classes to write your plugin, you could do the same. But instead of static variables you can make use of private class variables to save such a state per object instance. This can make sense if you want to use the same filter functionality in different contexts. Because each instance has it’s own variables, this can be controlled per instance. You can naturally use static class member as well.

I do not provide an additional code example here. This is comparable to the principles shown above and it is normally the case that plugin authors who do code their plugins as classes (often considered as best-practice) normally make PHP 5.2 and above as a requirement for their plugin. They don’t need a workaround most times, because with PHP 5.2 and above remove_filter() works closely as documented – which is the second option on how to deal with the problem which already helps a lot to not run into problems later.

Documentation and Development Issues

As slightly pointed to in the introduction, these deficiencies are currently not properly documented. The sad part of the story is, that someone did took the time to leave a warning note in the documentation (I know that because it was me). But for some reason it has been removed. Viewing codex history needs a codex login, here is the part that has been removed:

This function might remove more then the specified filter(s).

Because of long-time unfixed bugs resulting in flaws of the underlying implementation, it is unpredictable which filters this function will remove when called. Usage might/will result in loss of other then the intended filter(s). Plugin authors should prevent the usage of this function if possible.

I can only guess what Nacin drove to remove the warning, he labeled the removal: “Revert bogus statement”. Bogus seems pretty misleading to me. Those statements might just looked bogus to him because it was me who made them. If so I don’t care much, it just might help to understand why this happened.

Regardless of the motivation, the more important part is, Plugin authors still get trapped by the annoying bugs. Those bugs are partially documented in ticket #10535. There is one note still in codex that warns about using the function very carefully and that the function does not give any erros in case it fails, but that’s not always enough as we could see in the ticket lately. Users are wondering again.

Let’s take a look back in history: In that ticket the problem came to attention of some developers months ago. While patching, it was clear that it is not properly solved, so to say, part of the problem was only fixed for PHP 5.2 and above. That’s the case because the patch is based on a specific function that only exists in PHP 5.2 and above: spl_object_hash(). From the top of my head in discussions that time I pointed to the function as something that might be helpful. And in the end it made it in ([12090]).

But it was clear that this function won’t work for PHP installs lower then 5.2. So naturally, it did not solve all of the problems in the underlying implementation. Something I found worth to note in codex later on as users still reported problems to me in support.

So why didn’t we fix it in the first place? From what I remember, it was not easy to find a proper fix – that easy it is. The filter system as many other internal stuff is not well documented and the related code is widely distributed. The docs got stuck somewhere in the first times implementor’s head maybe. No specification of how the hook registry is done or similar. I had my problems in the beginning to properly understand for writng a patch I must admit. There were considered multiple ways to solve the issue but none could eliminate the underlying cause. Some of our approaches were considered to change to much so to risk side-effects. Somewhat the normal chu-chu-a-go-go with patches, commits and flowers. In the end the patch did not had much then adding the SPL function. So to have at least a workaround for a larger part of the userbase and to take care of other stuff.

So this remains unfixed. And the use of a broken API function is not of much use for Plugin authors. With the situation AS-IS, this is what I think is the best thing you could do as a plugin author:

Summary

Save yourself the hassles and do not use remove_filter(). My post shows some code examples how to deal with it independently, feel free to adopt them for your own use or leave other ways in comments.

  • remove_filter() is broken.
  • There is no need to rely on remove_filter() for your hooks, you can do better on your own.
  • The API manual does not tell much about this.

 
Registering/Unregistering filters has not been done right in the first place and has side-effects you don’t want to deal with – even with PHP 5.2. In support we’ve seen third-party plugins de-registering other plugins filters. And that was not intended by these authors. You might imagine that this is a nightmare to drill down. In the end another story about how documentation can help or hinder you to write software.

Image credits: the waving cat

About these ads
This entry was posted in Hacking The Core, Plugin Plugout, Pressed and tagged , , , , , , , , , , , . Bookmark the permalink.

12 Responses to WP Plugins: How to remove a Filter

  1. Denis says:

    Did you post a link to this in the wp wiki? If not, I think you should. :-)

  2. Jacob Santos says:

    I think a little bit of education is in order to prevent unintentional FUD from spreading.

    Functions should never have a problem with removing. They are “safe” in that while you can only have one function attached to any filter at a given priority at a time, that should be all you need. Static functions also do not suffer from the problems you are describing.

    What you might be running into is a failure to understand PHP and the way references with arrays work. I don’t know, I haven’t seen any examples of this not working the way it should that wasn’t explained by some obscure self-referencing that shouldn’t be used in the first place.

    You also have it backwards. Functions and static methods are ‘safe’ and dynamic class calls before PHP5.2 are not. Recommending using the dynamic class calls is not the best call, when below PHP5.2.

    • hakre says:

      We have situations where even removing a global function name as a filter removes other filters – regardless of what image the API creates. Next to that – and I think you write that as well but more complicated – wordpress running on a PHP version below 5.2 does not really know what it’s doing while removing a filter.

      A good implementation would take care for every possible case.

      The aspect you add that the same function should work with the same (default for example) priority on a hook is something worth to consider when talking about the API documentation. Thanks for pointing to it (while neglecting it per once). But you’re right, that’s a more general shortcoming that is not documented as well.

      But I made this post to put the focus on the fact that this single function just do not work as intended safely. If you call that FUD, that’s wrong. I show plugin authors how others and me could deal with the deficiencies regardless how broken the functionality is. That’s not FUD, it’s just a dissatisfying situation active developers have to deal with. It is dissatisfying. Period.

      I was not able to create a working patch as well. But I have no problem to openly talk about that and to keep things documented before letting other run into the same problems.

      • Jacob Santos says:

        It is FUD based on the lack of proof backing your assertion that the plugin system is broken. Also, it lacks proper dissection of the plugin system to describe what is wrong as so much as to where the problem is and provide enough information as to whether the fault is with the user (i.e. You) or with WordPress. I would rather help you solve your problem than for you to keep using this unnecessary hack to work around a perceived problem.

        One looking at simply remove_filter() or remove_action() will assume that its simplicity lacks something that disproves your point. However, remove_filter() does what it is supposed to do and nothing more. Provided the unit tests that goes with remove_filter(), I would say that using remove_filter() is not a proven case for your point.

        Provided that the same function is passed with the proper priority, remove_filter() will always remove the function. This is not to say that it will sometimes remove it, it is to say that without a doubt it will always remove the function provided that remove_filter() matches the add_filter().

        It is also to say that static methods works also, since they share much the same pass-through as functions.

        The problem with _wp_filter_build_unique_id() has always been that it has problems working with dynamic class references or array(&$this, ‘method).

        I will submit that if, in fact, a hook is not being removed, it is either because it is too soon as the hook has not been created or it is being removed too late or is attempted to be removed during the same time the hook is running and the removed hook is also of the current tag.

        • hakre says:

          If you’re eager, feel free to provide a working patch that solves all shortcomings for the moment. It was not only me who tried last time and we were not able to do so. It’s documented. I’m not complaining about that fact. If I saw that right, you wouldn’t be alone for a patch, someone else suggested a new one as well.

          From my technical background just in case you have not realized it, I’m pretty aware of the fact that part of the problem relies in other functions and datastructures not on the remove_filter() function solely. But that one is the API one plugin authors normally need to eliminate when they run into problems. I have not written that you always run into problems nor that you’re always lucky and it works.

          The bug has been accepted, I think that’s enough for the moment to show regardless of what I write that there is a problem. Maybe you want to call the ticket FUD as well? Go ahead.

  3. Alex M. says:

    I’ve used this type code in multiple plugins and never had an issue with it:

    http://viper007bond.pastebin.com/BSmsLNPA

    I’m not sure what you’re going on about.

  4. Pingback: Enabling Action and Filter Hook Removal from Class-based WordPress Plugins « HardcoreWP

  5. Eoin says:

    Reblogged this on Magp.ie and commented:

    Do not use remove_filter().

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s