Ahosting Logo
Knowledge Base

Understanding WordPress Hooks: Actions and Filters

The two kinds of hook do opposite thingsActionFilterFiresat a moment in the page lifecyclewhen a value is about to be usedExpectsyour code to do somethingyour code to return the valueIf you return nothingfinethe value becomes empty and somethingbreaksTypical usesend an email, register somethingchange a price, alter a titleA filter that forgets to return its value is the most common WordPress custom-code bug, and it usually shows upsomewhere unrelated.

Hooks are the mechanism behind every plugin. They let code run at chosen moments, and change values on their way past, without anyone editing WordPress itself or another plugin's files.

There are two kinds, and they are not interchangeable.

Actions: do something at a moment

add_action( 'init', 'my_function' );

function my_function() {
 // runs when WordPress reaches init
}

An action fires at a point in the request: WordPress is starting up, a post is being saved, the head is being written, and your function does whatever it does. It returns nothing.

Familiar ones: init early in every request, wp_enqueue_scripts for adding stylesheets and scripts, save_post after a post is stored, wp_footer for something at the end of the page.

Filters: change a value

add_filter( 'the_content', 'my_filter' );

function my_filter( $content ) {
 return $content . '<p>Thanks for reading.</p>';
}

A filter receives a value and must return one. The returned value is what everything downstream sees.

Forgetting to return is the mistake everyone makes once. A filter that returns nothing replaces the value with nothing, so post content disappears from the site with no error anywhere. If content has vanished after adding a snippet, this is almost always why.

Priority

add_filter( 'the_content', 'my_filter', 20 );

The third argument decides order when several functions attach to the same hook. Lower runs earlier; the default is 10.

This is the answer when your change is being overridden by something else: a plugin filtering the same value after you did wins, and running later. A higher number, puts you last.

Arguments

add_filter( 'the_title', 'my_title_filter', 10, 2 );

function my_title_filter( $title, $post_id ) {
 return $title;
}

The fourth argument states how many values you accept. Leave it out and only the first is passed, so a function expecting two receives one and errors.

This is the second most common hook mistake, and its symptom (a missing-argument error naming your own function) is at least clear.

Removing something

remove_action( 'wp_head', 'wp_generator' );

Hooks can be unhooked, which is how you disable behaviour without editing the code that added it.

Two conditions: the function name must match exactly, including the priority if it was not the default, and the removal must run after the thing was added. Removing a plugin's hook usually means doing it inside an init action rather than at file load, because the plugin has not registered anything yet when your file is read.

Where to put the code

Not in the theme's functions file. A site-specific plugin survives theme changes and can be switched off from the dashboard when a snippet misbehaves. MU-plugins and site-specific plugins explains setting one up.

Finding the right hook

The practical method is to search WordPress's own source for do_action and apply_filters near the behaviour you want to change. That tells you what is available, what is passed to it, and when it fires, which is more reliable than a snippet from a forum that may target an obsolete hook.

Plugins define their own hooks the same way. A well-written plugin documents them, and a plugin with none is one you cannot extend without modifying it, which is worth knowing before you depend on it. Choosing and vetting plugins sets out judging that.

Why this matters even if you do not write code

Nearly every snippet you will be handed is add_action or add_filter. Recognising which is which tells you whether it does something at a moment or changes a value, and reading the hook name tells you when.

That is usually enough to judge whether a snippet does what its description claims, and to debug it when it does not, using WP_DEBUG and the log.

See which code is attached before adding more

Debugging behaviour that nothing in your own code explains starts with listing what else is attached at that point.

wp eval 'global $wp_filter; $h="the_content";
  if(isset($wp_filter[$h])) foreach($wp_filter[$h] as $p=>$cbs) foreach($cbs as $c)
    echo $p, " ", (is_string($c["function"]) ? $c["function"] : "closure"), "\n";'

That prints every function attached to a hook, in the order they run. What appears is usually several plugins operating on the same content, each unaware of the others.

The order is the answer to most questions here. Code that runs before something else sees different data from code that runs after, and two plugins both modifying content produce a result that depends entirely on which went first.

Attach at the right moment, not the earliest one

Code that runs too early fails because the thing it needs does not exist yet, and the error rarely says so.

wp eval 'add_action("init", function(){ error_log("init: " . (function_exists("wc_get_product") ? "var" : "yok")); });'

Functions provided by a plugin are not available until that plugin has loaded, and the query being displayed is not known until later still. Attaching to the earliest available point is a common habit and a common cause of intermittent failure.

Choose the latest point that still does what you need. Code attached late runs with everything in place, and the cost of waiting is nothing compared to the cost of a function that exists on some requests and not others. Using mu-plugins and a site specific plugin covers where the code belongs.

Remove something that another plugin added

Detaching code you did not write requires matching exactly how it was attached, and a near match silently does nothing.

wp eval 'remove_action("wp_head", "wp_generator");'
wp eval 'global $wp_filter; print_r(array_keys((array)$wp_filter["wp_head"]->callbacks));'

The priority has to match the one used when attaching. A removal at the default priority does not detach something attached at a different one, and the call returns without complaint.

Timing matters equally. A removal that runs before the thing was attached does nothing, so the removal usually has to be attached to a later point than the code it is removing.

Anything attached to a closure cannot be removed

Code attached as an anonymous function has no name to reference, so there is no way to detach it from outside.

That is worth knowing for two reasons. It explains why some plugin behaviour cannot be overridden however correctly you write the removal. And it is an argument for using named functions in your own code, so somebody else can turn it off without editing your files.

wp eval 'global $wp_filter; foreach($wp_filter["the_content"]->callbacks as $p=>$cbs)
  foreach($cbs as $id=>$c) if(!is_string($c["function"])) echo "kapali: $p $id\n";'

Where a closure is genuinely in the way, the remaining options are to change what it operates on or to stop it running at all, both of which are cruder than detaching it would have been.