The idea of hooks
WordPress, its themes and plugins are full of named points where other code can join in. These points are called hooks. You never edit WordPress core to change something; you attach a function ("callback") to a hook. There are two kinds:
- Actions say "something is happening now". Your function runs and does something: enqueue a script, send an email, add a column. It returns nothing.
- Filters say "here is a value, change it if you want". Your function receives the value and returns the new one: a title, a list of CSS classes, an excerpt length.
Everything else in the Hooks and plugins section builds on this lesson.
Your first action
Hooks need a place to live. For small experiments, use a must-use plugin: any PHP file in wp-content/mu-plugins/ loads automatically on every request, with nothing to activate. (The next lesson makes a normal plugin.)
<?php
// Runs when WordPress prints the footer of the front end.
add_action( 'wp_footer', function () {
echo '<p class="footer-note">Thanks for visiting.</p>';
} );GET /Thanks for visiting.wp_footer is an action WordPress fires when it prints the end of the page. Adding a callback made it run there. The first argument of add_action() is the hook name, the second is the callback: an anonymous function as here, or the name of a function as a string.
Your first filter
A filter gets a value and must return one. This one changes the "Read more" text on excerpts:
<?php
add_filter( 'excerpt_more', function ( string $more ): string {
return ' [read on]';
} );
add_filter( 'the_title', function ( string $title, int $post_id ): string {
return $post_id === 1 ? $title . ' (welcome!)' : $title;
}, 10, 2 );<?php
echo get_the_title( 1 ), "\n";
echo apply_filters( 'excerpt_more', ' [...]' ), "\n";Hello world! (welcome!)
[read on]Two things to learn here:
- A filter must return. If the callback returned nothing, the title would become empty.
- The fourth argument of
add_filter()(here2) says how many arguments your callback accepts. Thethe_titlefilter passes the title and the post ID; by default your function would receive only the first.
Priority and order
Several callbacks can attach to one hook. Their order is set by the priority, the third argument (default 10). Lower numbers run first; equal priorities run in the order they were added.
<?php
add_filter( 'my_text', fn( $t ) => $t . ' B', 20 );
add_filter( 'my_text', fn( $t ) => $t . ' A', 5 );
add_filter( 'my_text', fn( $t ) => $t . ' C' ); // default 10
echo apply_filters( 'my_text', 'start' ), "\n";start A C BUse a high number (such as 99) to run after other code, or a low one to run before. You need this to override a theme or plugin that sets the same thing.
Making your own hooks
You can add hooks to your own code so that others (or your future self) can extend it without editing it:
<?php
function shop_price_label( float $price ): string {
/**
* Filters the text shown for a price.
*
* @param string $label The formatted price.
* @param float $price The raw price.
*/
$label = apply_filters( 'shop_price_label', '£' . number_format( $price, 2 ), $price );
do_action( 'shop_price_shown', $price ); // an action: "this happened"
return $label;
}
add_filter( 'shop_price_label', fn( $label, $price ) => $price == 0 ? 'Free' : $label, 10, 2 );
add_action( 'shop_price_shown', fn( $price ) => print( "price shown: $price\n" ) );
echo shop_price_label( 4.5 ), "\n";
echo shop_price_label( 0 ), "\n";price shown: 4.5
£4.50
price shown: 0
Freeapply_filters( 'hook', $value, ...$extra ) runs the callbacks and returns the final value. do_action( 'hook', ...$args ) runs the callbacks and returns nothing. Prefix your hook names with your plugin name (shop_) so they never clash.
Removing and checking hooks
<?php
function add_exclamation( $t ) { return $t . '!'; }
add_filter( 'my_text', 'add_exclamation' );
echo has_filter( 'my_text', 'add_exclamation' ) ? "attached\n" : "not attached\n";
remove_filter( 'my_text', 'add_exclamation' );
echo has_filter( 'my_text', 'add_exclamation' ) ? "attached\n" : "not attached\n";attached
not attachedTo remove a hook you must use the same name, callback and priority that were used to add it. That is why an anonymous function is hard to remove: keep important callbacks as named functions or class methods. Other helpers: did_action( 'init' ) tells you how often an action has fired, and current_filter() names the hook that is running.
Hooks you will use constantly
| Hook | Type | Fires |
|---|---|---|
init |
action | after WordPress has loaded; register post types and taxonomies here |
wp_enqueue_scripts |
action | when front-end assets should be added |
after_setup_theme |
action | when a theme sets up features |
save_post |
action | after a post is saved |
admin_menu |
action | when the admin menu is built |
the_content |
filter | on a post's content before it is shown |
the_title |
filter | on a post's title |
body_class |
filter | on the list of classes for <body> |
The full list lives in the WordPress developer reference. The most useful habit is to search it for the hook you need, then read what arguments it passes.
Common mistakes
- Forgetting to
returnin a filter. - Echoing in a filter instead of returning.
- Not accepting enough arguments: add the count as the fourth argument.
- Hooking too early or too late:
get_the_title()beforeinitworks, but registering a post type afterinitdoes not. - Anonymous callbacks you later need to remove.
Exercise
Add a filter on body_class that adds the class has-hooks to every page, then check the <body> tag.
Show one possible answer
add_filter( 'body_class', function ( array $classes ): array {
$classes[] = 'has-hooks';
return $classes;
} );body_class passes an array of classes; add one and return the array.
Key points
- Hooks are named points in WordPress where your code can run (actions) or change a value (filters).
- add_action( 'hook', $callback ) runs your function at that point; add_filter( 'hook', $callback ) receives a value and must return it.
- The priority (default 10) sets the order, the last argument sets how many values your function receives.
- Plugins and themes can offer their own hooks with do_action() and apply_filters(), and remove others' with remove_action() and remove_filter().