Have you ever looked at the add_action function in WordPress? Here it is:

function add_action($tag, $function_to_add, $priority = 10, $accepted_args = 1) {
	return add_filter($tag, $function_to_add, $priority, $accepted_args);
}

I know, right? Some people’s minds just got blown.

What are Filters?

A filter is defined as a function that takes in some kind of input, modifies it, and then returns it. This is an extremely handy little concept that PHP itself uses in a ton of different ways. About half the string functions qualify as a ‘filter’ function.

Look at strrev(). It’s a simple-stupid example. It takes a string as an argument, and then returns the reverse of that string. You could use it as a filter function in WordPress, easily. Like, to reverse all your titles.

add_filter('the_title', 'strrev');

Some filters take more than one argument, but the first argument is always the thing to be modified and returned. PHP adheres to this concept too. Take the substr() function. The first argument is the string, the second and third are the start and optional length values. The returned value is the part of the string you want.

What are Actions?

An action is just a place where you call a function, and you don’t really care what it returns. The function is performing some kind of action just by being called. If you hook a function to the init action, then it runs whenever do_action(‘init’) is called.

Now, some actions have arguments too, but again, there’s still no return value.

So in a sense, a WordPress action is just a filter without the first argument and without a return value.

So why have them both?

Because there is still a conceptual difference between an action and a filter.

Filters filter things. Actions do not. And this is critically important when you’re writing a filter.

A filter function should never, ever, have unexpected side effects.

Take a quick example. Here’s a thread on the WordPress support forums where a person found that using my own SFC plugin in combination with a contact form emailer plugin caused the email from the form to be sent 3-5 times.

Why did it do this? Basically, because the contact form plugin is sending an email inside a filter function.

One of the things SFC does is to build a description meta from the content on the page. It also looks through that content for images and video, in order to build meta information to send to Facebook. In order for this to happen at the right time, the plugin must call the_content filter.

See, what if somebody puts a link to a Flickr picture on their page? In that case, oEmbed will kick in and convert that link into a nice and pretty embedded image. Same for YouTube videos. Or maybe somebody is using a gallery and there’s lots of pictures on the resulting page, but the only thing in the post_content is the gallery shortcode.

In order to get those images from the content, SFC has to do apply_filters(‘the_content’,$post_content). This makes all the other plugins and all the other bits of the system process that $post_content and return the straight HTML. Then it can go and look for images, look for video, even make a pretty 1000 character excerpt to send to Facebook.

But SFC can’t possibly know that doing apply_filters(‘the_content’,…) will cause some other plugin to go and send a freakin’ email. That’s totally unexpected. It’s just trying to filter some content. That would be like calling the strrev() function and having it make a phone call. Totally crazy.

Shortcodes

Shortcodes are a type of filter. They take in content from the shortcode, they return replacement content of some sort. They are filters, by definition. Always, always keep that in mind.

Also keep in mind that shortcodes are supposed to return the replacement content, not just echo it out.

Conclusion

So plugin authors, please, please, I’m begging you, learn this lesson well.

Filters are supposed to filter. Actions are supposed to take action.

When you mix the two up, then you cause pain for the rest of the world trying to interact with your code. My desk is starting to get covered in dents from me repeatedly banging my head into it because of this.

Shortlink:

30 Comments

  1. Could your plugin rely on facebook metatags, so it dosent have to filter the_content? (besides injecting your plugins functionality)

    The “In order to get those images from the content, SFC has to do apply_filters(‘the_content’,$post_content).” part could be skipped if your or another plugin used some meta property=”og:xyz” injection to the header…

    Do you really need to be able to select between 20 images from a webpage for the thumbnail?
    I saw someone else complaining that if the_content is used in a widget it will also be eligible for inclusion on the SFC.. I dident check this, its just an observation :)

    Anyways, good job describing the functionality of WP and making whatever you do!

    • That sorta misses the whole point of the plugin. The goal is to be “simple”. It builds the meta tags so you don’t have to build them yourself. I don’t want to friggin micromanage my Facebook posts through manually creating tag values and such. The data is there, and I want the code to do it for me. Simple.

      I have no idea what “the_content is used in a widget” even means. That’s just random words strung together as far as I can tell.

  2. Nice post Otto. It is a good example of just because you can do something, it doesn’t necessarily mean you should!

    Plugin conflicts would be drastically reduced if all developers tried to keep up with best practices, and refactor their code when necessary. It makes sense as your support requests will be probably be reduced in the long term.

    I try to do this with my own code if I find something that could be done better, or is just plain wrong. It’s a pride thing. And I take the view that I am continually learning rather than assuming I have nothing new to learn, just because my code ‘seems to’ work OK. Or that a Plugin conflict must be someone else’s fault!

  3. Thanks for the post Otto! I have no formal coding experience… I just learn by reading other people’s stuff, and trying to figure out how to make what I want happen. I’ve built a few themes over the years, and now started with some plugins. Sometimes I emulate good sources, and I’m finding out, sometimes I emulate bad ones. I’ve had some really unexpected outcomes due to dabbling in things I didn’t understand. So I appreciate you taking the time to explain some of this stuff!

  4. Thanks for clarifying that. It’s quite basic though.

  5. I don’t think I ever found this issue confusing. Filters filter stuff, actions add stuff to stuff. Pretty simple I’d have thought. You can kinda use one for the other, but they’re still quite different.

    • One would think it’s quite obvious, but the number of plugins I run across using filters to “do stuff” instead of acting as filters is quite large. Even Automatticians make this basic mistake from time to time.

  6. A filter function should never, ever, have unexpected side effects.

    While in principle I agree completely, I disagree in practice, at least for one set of special cases. The case where a filter should have side-effects is in the case an action hook has been requested on Trac, the core WordPress team dismisses the need for it, and the only ways to achieve the client-requested functionality other than to hack core is to implement within an existing filter.

    But I digress. ;-)

    • I just came across a perfect example of a situation where there is a filter but no action available and thus it forces us to use the filter as an action: the 'parent_file' filter that is called before the admin menu is rendered.

      Speaking of the admin menus, they are pretty much devoid of hooks so it’s very difficult to modify with them based on client requests. The lack of hooks force some pretty ugly and non-robust use of ob_start() and ob_get_clean(). I’ve been hoping since before v3.0 that the admin menus would finally get some hook love, but alas, still no attention to date.

      • We all agree that the existing admin menu system is horrific. All the core team pretty much thinks so from what I’ve gathered.

        If you watch the presentation me and Nacin gave at WCSF, about halfway through somebody asked what the worst part of WP was. Ryan chimed in from off camera and said it was the admin menus. Lots of heads nodded at that point.

        Patches welcome, although frankly I think wholesale restructuring of large portions of that would be welcome. It’s not a matter of “adding hooks”, that whole thing needs a rethink. Just remember that backward compatibility *really* matters on that area, virtually all plugins create menus in some manner.

        • @Otto – Thanks for the comment.

          I’ve probably wasted more time on the admin menus than I’ve wasted on probably any other part of WordPress. Because it is so convoluted my takeaway is that it would be practically impossible to change it in a backward compatible manner, which kind of leaves us stuck.

          OTOH, we could do the following: Create a new admin menu system w/o worrying about backward compatibility. Add a wp_option of 'admin_menus_version' (or similar) that defaults to 3.3. Then deprecate all the older admin menu hooks for when 'admin_menus_version' is set to 3.4 (this assumes v3.4 will add new admin menus), and issue deprecation warnings on screen if a plugin calls any of the deprecated admin menu hooks.

          That would allow people who want to use plugins that use the new menus to do show with the caveat that they can’t use plugins that do it the older way, and WordPress itself can support both old and new versions of the admin menu.

          I know this is an unprecedented approach for WordPress, but is there any other part of WordPress that is so badly unable to be evolved cleanly? FWIW.

        • Also, just because it’s really bad and needs to be rewritten doesn’t mean that a few hooks couldn’t ease the pain greatly until we do get a rewrite. You don’t shoot your grandma just because she has a broken leg. Just sayin…

  7. [...] WordPress has two observer-style hook types: actions and filters. According to WP, actions are immutable and filters are mutable. Here’s a good post to explain it: http://ottopress.com/2011/actions-and-filters-are-not-the-same-thing/ [...]

  8. Hi, ever since I did the update, whenever I send a blog post to wordpress it doesn’t come up on fans’ walls at all. It just comes up on the page. So I have to delete it and then manually post a link. Am I missing something here?

  9. [...] Ottopress Share this:ShareEmailDiggFacebook This entry was posted in code, WordPress by Aaron Holbrook. Bookmark the permalink. [...]

  10. Hey,

    That’s interesting & solve my confusion regarding filter & action.

    Thanks.

  11. [...] how a filter is semantically used as an action in this case, sorry, Otto, we be hackin’. Don’t forget to pass the $object out, since the loop is expecting [...]

  12. [...] to output error messages is show_password_fields. Although filters should not have side-effect, big no-no, I have chosen to break the [...]

  13. [...] Although both will call your functions the same way, and you actually could — but never should! — apply add_action() to lists of filters and vice versa, or use apply_filters() instead of do_action() or even do_action() instead of apply_filters(), keeping them functionally and semantically separate is absolutely critical. As Samuel Wood says in “Actions and Filters Are Not the Same Thing”: [...]

  14. You mention that shortcodes are filters and this is the cause of the issue I have which led me here.

    There’s a common pattern in some plugins where they define a function as follows

    function myFormPlugin() {
    if(form parameters present) {
    process form;
    }
    display form;
    }

    add_shortcode(‘myForm’, ‘myFormPlugin’);

    This will cause problems dues to the issue you raise.

    My question is, what is the correct pattern for this?

    • The correct pattern is to not process the form in the shortcode, but to move that out to a separate function and attach it to the “init” action hook instead.

      Things like processing forms should not be in filters, they should be in actions.

      • That’s fine, but how do you then report errors that result in processing the form?

        For example how would a custom registration form inform a new user that the username they chose is already taken?

        • You save the error somewhere, then have the filter output it when appropriate.

          The point is that the filter can run more than once, and it should not have side effects. If you’re processing the form from inside the filter, then you may be processing the form more than once.

  15. [...] Although both will call your functions the same way, and you actually could — but never should! — apply add_action() to lists of filters and vice versa, or use apply_filters() instead of do_action() or even do_action() instead of apply_filters(), keeping them functionally and semantically separate is absolutely critical. As Samuel Wood says in “Actions and Filters Are Not the Same Thing”: [...]

  16. Hi Otto, thank you for the blog post. The examples are very illustrative of why actions are not filters.

    That leaves me with one question…

    Since add_action is just a wrapper around add_filter, how does the add_filter function know if it’s really adding a filter or action?

    Thanks!

  17. […] Although both will call your functions the same way, and you actually could — but never should! — apply add_action() to lists of filters and vice versa, or use apply_filters() instead of do_action() or even do_action() instead of apply_filters(), keeping them functionally and semantically separate is absolutely critical. As Samuel Wood says in “Actions and Filters Are Not the Same Thing”: […]

  18. […] Otto on WordPress: Actions and filters are NOT the same thing… […]

Leave a Reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Need to post PHP code? Wrap it in [php] and [/php] tags.