WordPress Hooks System: Actions and Filters
Learning Objectives
- Understand the WordPress hooks system and how it enables extensibility
- Master the difference between actions and filters
- Learn how to use common WordPress hooks effectively
- Create custom hooks in your themes and plugins
- Understand hook priority and parameter passing
- Debug and troubleshoot hook-related issues
Introduction to WordPress Hooks
The WordPress hooks system is the foundation of WordPress extensibility. It allows developers to "hook into" WordPress core, themes, and plugins to modify or extend functionality without editing core files.
WordPress hooks come in two flavors:
- Actions:Allow you to execute custom code at specific points
- Filters:Allow you to modify data before it's used or displayed
Understanding Actions
Actions are hooks that allow you to add or execute custom code at specific points during WordPress execution. They don't return any value - they simply perform tasks.
Basic Action Syntax
<?php
// Adding an action hook
add_action( 'hook_name', 'your_function_name', $priority, $accepted_args );
// Creating an action hook
do_action( 'hook_name', $arg1, $arg2, ... );
// Basic example
function my_custom_footer_message() {
echo '<p>Thank you for visiting our site!</p>';
}
add_action( 'wp_footer', 'my_custom_footer_message' );
?>
Common WordPress Actions
<?php
// 1. init - Fires after WordPress has finished loading
add_action( 'init', 'my_init_function' );
function my_init_function() {
// Register custom post types, taxonomies, etc.
register_post_type( 'product', array(
'public' => true,
'label' => 'Products'
));
}
// 2. wp_enqueue_scripts - Proper way to add scripts and styles
add_action( 'wp_enqueue_scripts', 'my_theme_scripts' );
function my_theme_scripts() {
wp_enqueue_style( 'theme-style', get_stylesheet_uri() );
wp_enqueue_script( 'theme-script', get_template_directory_uri() . '/js/script.js', array('jquery'), '1.0.0', true );
}
// 3. save_post - Fires after a post is saved
add_action( 'save_post', 'my_save_post_function', 10, 3 );
function my_save_post_function( $post_id, $post, $update ) {
// Perform actions when a post is saved
if ( $update ) {
// This is an update to an existing post
update_post_meta( $post_id, '_last_modified_by', get_current_user_id() );
}
}
// 4. wp_head - Add content to the <head> section
add_action( 'wp_head', 'my_custom_head_content' );
function my_custom_head_content() {
echo '<meta name="custom-meta" content="value">';
}
// 5. admin_menu - Add admin menu items
add_action( 'admin_menu', 'my_admin_menu' );
function my_admin_menu() {
add_menu_page(
'Custom Settings', // Page title
'Custom Settings', // Menu title
'manage_options', // Capability
'custom-settings', // Menu slug
'my_settings_page' // Callback function
);
}
?>
Understanding Filters
Filters allow you to modify data before it's used. Unlike actions, filters must return a value - typically a modified version of the input.
Basic Filter Syntax
<?php
// Adding a filter hook
add_filter( 'hook_name', 'your_function_name', $priority, $accepted_args );
// Applying filters
$value = apply_filters( 'hook_name', $value, $arg2, $arg3, ... );
// Basic example
function modify_excerpt_length( $length ) {
return 20; // Return 20 words instead of default 55
}
add_filter( 'excerpt_length', 'modify_excerpt_length' );
?>
Common WordPress Filters
<?php
// 1. the_content - Modify post content before display
add_filter( 'the_content', 'my_content_filter' );
function my_content_filter( $content ) {
if ( is_single() ) {
$content .= '<p>Thanks for reading!</p>';
}
return $content;
}
// 2. the_title - Modify post titles
add_filter( 'the_title', 'my_title_filter', 10, 2 );
function my_title_filter( $title, $post_id ) {
if ( get_post_type( $post_id ) === 'product' ) {
$title = '🛍️ ' . $title;
}
return $title;
}
// 3. body_class - Add custom CSS classes to body tag
add_filter( 'body_class', 'my_body_classes' );
function my_body_classes( $classes ) {
if ( is_user_logged_in() ) {
$classes[] = 'logged-in-user';
}
return $classes;
}
// 4. upload_mimes - Allow additional file types for upload
add_filter( 'upload_mimes', 'my_mime_types' );
function my_mime_types( $mimes ) {
$mimes['svg'] = 'image/svg+xml';
$mimes['webp'] = 'image/webp';
return $mimes;
}
// 5. login_redirect - Change where users go after login
add_filter( 'login_redirect', 'my_login_redirect', 10, 3 );
function my_login_redirect( $redirect_to, $request, $user ) {
if ( isset( $user->roles ) && is_array( $user->roles ) ) {
if ( in_array( 'administrator', $user->roles ) ) {
return admin_url();
} else {
return home_url( '/dashboard' );
}
}
return $redirect_to;
}
?>
How Hooks Work: Execution Flow
Hook Priority and Arguments
Understanding priority and arguments is crucial for effective hook usage.
Priority
Priority determines the order in which functions are executed. Lower numbers run first (default is 10).
<?php
// These will execute in order: first_function, second_function, third_function
add_action( 'init', 'first_function', 5 ); // Priority 5 - runs first
add_action( 'init', 'second_function', 10 ); // Priority 10 - runs second (default)
add_action( 'init', 'third_function', 15 ); // Priority 15 - runs last
// Real-world example: Ensuring your styles load after theme styles
add_action( 'wp_enqueue_scripts', 'parent_theme_styles', 10 );
add_action( 'wp_enqueue_scripts', 'child_theme_styles', 20 ); // Higher priority = loads later
?>
Accepted Arguments
The fourth parameter specifies how many arguments your function accepts from the hook.
<?php
// Hook that passes 3 arguments
do_action( 'save_post', $post_ID, $post, $update );
// Function accepting all 3 arguments
add_action( 'save_post', 'my_save_function', 10, 3 );
function my_save_function( $post_id, $post, $update ) {
// Can use all three arguments
error_log( "Post $post_id saved. Is update: " . ( $update ? 'yes' : 'no' ) );
}
// Function accepting only 1 argument (default)
add_action( 'save_post', 'my_simple_save_function' );
function my_simple_save_function( $post_id ) {
// Can only use $post_id
error_log( "Post $post_id was saved" );
}
?>
Creating Custom Hooks
You can create your own hooks in themes and plugins to make your code extensible.
Creating Custom Actions
<?php
// In your theme or plugin
function my_custom_header() {
// Allow other developers to add content before header
do_action( 'before_custom_header' );
echo '<header class="custom-header">';
echo '<h1>' . get_bloginfo( 'name' ) . '</h1>';
// Allow content inside header
do_action( 'inside_custom_header', get_bloginfo( 'name' ) );
echo '</header>';
// Allow content after header
do_action( 'after_custom_header' );
}
// Other developers can now hook into your code
add_action( 'before_custom_header', 'add_announcement_bar' );
function add_announcement_bar() {
echo '<div class="announcement">Free shipping this week!</div>';
}
?>
Creating Custom Filters
<?php
// In your theme or plugin
function get_product_price( $product_id ) {
$base_price = get_post_meta( $product_id, '_price', true );
// Allow price modification through filter
$final_price = apply_filters( 'custom_product_price', $base_price, $product_id );
return $final_price;
}
// Other developers can modify the price
add_filter( 'custom_product_price', 'apply_member_discount', 10, 2 );
function apply_member_discount( $price, $product_id ) {
if ( is_user_logged_in() && current_user_can( 'member' ) ) {
$price = $price * 0.9; // 10% discount for members
}
return $price;
}
// Multiple filters can be chained
add_filter( 'custom_product_price', 'apply_bulk_discount', 20, 2 );
function apply_bulk_discount( $price, $product_id ) {
$quantity = get_cart_quantity( $product_id );
if ( $quantity >= 10 ) {
$price = $price * 0.85; // Additional 15% for bulk orders
}
return $price;
}
?>
Removing Hooks
Sometimes you need to remove hooks added by themes or plugins.
<?php
// Remove an action
remove_action( 'wp_head', 'wp_generator' );
// Remove a filter
remove_filter( 'the_content', 'wpautop' );
// Remove with specific priority (must match exactly)
remove_action( 'wp_head', 'some_function', 15 );
// Remove from within a class
// Original: add_action( 'init', array( $my_class, 'method_name' ) );
global $my_class;
remove_action( 'init', array( $my_class, 'method_name' ) );
// Remove all hooks from a tag
remove_all_actions( 'wp_footer' );
remove_all_filters( 'the_content' );
// Check if hook has functions attached
if ( has_action( 'wp_footer' ) ) {
// There are actions hooked to wp_footer
}
if ( has_filter( 'the_content' ) ) {
// There are filters hooked to the_content
}
?>
Visual Hook Execution
Click the button to see how hooks execute in order based on priority:
Best Practices for Using Hooks
-
Use descriptive function names:Prefix with your theme/plugin slug to avoid conflicts (e.g.,
mytheme_custom_header) - Always return in filters:Filters must return a value, even if unchanged
-
Check for function existence:Use
if ( ! function_exists() )for pluggable functions - Use appropriate priority:Default is 10, use lower for early execution, higher for late
- Document your hooks:If creating custom hooks, document them clearly for other developers
- Don't execute heavy operations directly:Schedule them with wp_cron if needed
- Remove your hooks when deactivating:Clean up in plugin deactivation hooks
Real World Example: WooCommerce-style Extensibility
Here's how to create a extensible plugin architecture similar to WooCommerce:
<?php
/**
* Extensible Product Display Class
*/
class Product_Display {
public function __construct() {
add_action( 'init', array( $this, 'init' ) );
}
public function init() {
add_shortcode( 'product', array( $this, 'render_product' ) );
}
public function render_product( $atts ) {
$atts = shortcode_atts( array(
'id' => 0,
), $atts );
$product_id = intval( $atts['id'] );
if ( ! $product_id ) {
return '';
}
// Start output buffering
ob_start();
// Before product hook
do_action( 'before_product_display', $product_id );
// Get product data
$product = get_post( $product_id );
$price = get_post_meta( $product_id, '_price', true );
// Apply price filters
$price = apply_filters( 'product_display_price', $price, $product_id );
// Product wrapper
echo '<div class="product-display">';
// Product image - filterable
$image_html = get_the_post_thumbnail( $product_id, 'medium' );
$image_html = apply_filters( 'product_display_image', $image_html, $product_id );
echo $image_html;
// Product title - filterable
$title = apply_filters( 'product_display_title', $product->post_title, $product_id );
echo '<h2>' . esc_html( $title ) . '</h2>';
// Product price
do_action( 'before_product_price', $product_id );
echo '<p class="price">$' . esc_html( $price ) . '</p>';
do_action( 'after_product_price', $product_id );
// Product description
$description = apply_filters( 'product_display_description', $product->post_content, $product_id );
echo '<div class="description">' . wp_kses_post( $description ) . '</div>';
// Add to cart button
do_action( 'before_add_to_cart_button', $product_id );
$button_text = apply_filters( 'add_to_cart_text', 'Add to Cart', $product_id );
echo '<button class="add-to-cart" data-product-id="' . esc_attr( $product_id ) . '">';
echo esc_html( $button_text );
echo '</button>';
do_action( 'after_add_to_cart_button', $product_id );
echo '</div>';
// After product hook
do_action( 'after_product_display', $product_id );
return ob_get_clean();
}
}
// Initialize the class
new Product_Display();
// Now other developers can extend this functionality:
// Add a sale badge
add_action( 'before_product_display', 'add_sale_badge' );
function add_sale_badge( $product_id ) {
$sale_price = get_post_meta( $product_id, '_sale_price', true );
if ( $sale_price ) {
echo '<span class="sale-badge">SALE!</span>';
}
}
// Add stock status
add_action( 'after_product_price', 'show_stock_status' );
function show_stock_status( $product_id ) {
$stock = get_post_meta( $product_id, '_stock', true );
if ( $stock < 5 ) {
echo '<p class="low-stock">Only ' . $stock . ' left in stock!</p>';
}
}
// Modify price for logged-in users
add_filter( 'product_display_price', 'member_pricing', 10, 2 );
function member_pricing( $price, $product_id ) {
if ( is_user_logged_in() ) {
$discount = 0.9; // 10% discount
$price = $price * $discount;
}
return $price;
}
// Add related products
add_action( 'after_product_display', 'show_related_products' );
function show_related_products( $product_id ) {
// Get product category
$categories = wp_get_post_terms( $product_id, 'product_category', array( 'fields' => 'ids' ) );
if ( $categories ) {
$args = array(
'post_type' => 'product',
'posts_per_page' => 3,
'post__not_in' => array( $product_id ),
'tax_query' => array(
array(
'taxonomy' => 'product_category',
'terms' => $categories
)
)
);
$related = new WP_Query( $args );
if ( $related->have_posts() ) {
echo '<div class="related-products">';
echo '<h3>Related Products</h3>';
while ( $related->have_posts() ) {
$related->the_post();
echo '<a href="' . get_permalink() . '">' . get_the_title() . '</a>';
}
echo '</div>';
wp_reset_postdata();
}
}
}
?>
Debugging Hooks
Understanding what hooks are firing and in what order is crucial for debugging.
<?php
// 1. List all functions hooked to a specific action/filter
function list_hooked_functions( $tag = false ) {
global $wp_filter;
if ( $tag ) {
$hook = $wp_filter[$tag];
ksort( $hook );
} else {
$hook = $wp_filter;
ksort( $hook );
}
echo '<pre>';
foreach( $hook as $tag => $priority ) {
echo "<br /><strong>$tag</strong><br />";
ksort( $priority );
foreach( $priority as $priority_level => $function ) {
echo $priority_level;
foreach( $function as $name => $properties ) {
echo "\t$name<br />";
}
}
}
echo '</pre>';
}
// 2. Check current action/filter
function check_current_hook() {
$current_filter = current_filter();
if ( $current_filter ) {
error_log( 'Currently executing: ' . $current_filter );
}
}
add_action( 'all', 'check_current_hook' );
// 3. Log all hooks execution (careful - this is verbose!)
function log_all_hooks() {
static $hooks_logged = array();
$current = current_filter();
if ( ! in_array( $current, $hooks_logged ) ) {
error_log( 'Hook fired: ' . $current );
$hooks_logged[] = $current;
}
}
// add_action( 'all', 'log_all_hooks' ); // Uncomment with caution
// 4. Debug specific hook
function debug_save_post( $post_id, $post, $update ) {
error_log( '=== save_post Debug ===' );
error_log( 'Post ID: ' . $post_id );
error_log( 'Post Type: ' . $post->post_type );
error_log( 'Is Update: ' . ( $update ? 'Yes' : 'No' ) );
error_log( 'Post Status: ' . $post->post_status );
error_log( '=====================' );
}
// add_action( 'save_post', 'debug_save_post', 10, 3 );
// 5. Query Monitor integration (if plugin is installed)
if ( function_exists( 'do_action_qm' ) ) {
do_action_qm( 'my_custom_action', 'Custom action for debugging' );
}
?>
Common Pitfalls and Solutions
Solutions to Common Issues
<?php
// 1. Preventing infinite loops in save_post
add_action( 'save_post', 'my_save_post_meta', 10, 3 );
function my_save_post_meta( $post_id, $post, $update ) {
// Check if this is an autosave
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
// Check user permissions
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
// Prevent infinite loop
remove_action( 'save_post', 'my_save_post_meta', 10 );
// Update the post
wp_update_post( array(
'ID' => $post_id,
'post_title' => $post->post_title . ' - Updated'
));
// Re-hook the action
add_action( 'save_post', 'my_save_post_meta', 10, 3 );
}
// 2. Proper filter usage (return, don't echo)
// WRONG
add_filter( 'the_title', 'bad_title_filter' );
function bad_title_filter( $title ) {
echo '⭐ ' . $title; // WRONG - Don't echo in filters
}
// CORRECT
add_filter( 'the_title', 'good_title_filter' );
function good_title_filter( $title ) {
return '⭐ ' . $title; // CORRECT - Return the modified value
}
// 3. Checking if it's the right context
add_filter( 'the_content', 'add_custom_content' );
function add_custom_content( $content ) {
// Only modify on single posts, not in admin or feeds
if ( is_single() && ! is_admin() && ! is_feed() ) {
$content .= '<p>Custom content here</p>';
}
return $content;
}
// 4. Safe hook removal from classes
// For static methods
remove_action( 'init', array( 'ClassName', 'method_name' ) );
// For instance methods (need the instance)
global $my_plugin_instance;
if ( isset( $my_plugin_instance ) ) {
remove_action( 'init', array( $my_plugin_instance, 'method_name' ) );
}
?>
Practice Exercise
Create a simple plugin that demonstrates both actions and filters:
Solution Template
<?php
/**
* Plugin Name: Hook Practice Plugin
* Description: Practice using WordPress hooks
* Version: 1.0
* Author: Your Name
*/
// Prevent direct access
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Hook_Practice_Plugin {
public function __construct() {
// Initialize hooks
$this->init_hooks();
}
private function init_hooks() {
// 1. Add message after content (filter)
add_filter( 'the_content', array( $this, 'add_content_message' ), 20 );
// 2. Log post views (action)
add_action( 'wp_head', array( $this, 'log_post_view' ) );
// 3. Modify excerpt length (filter)
add_filter( 'excerpt_length', array( $this, 'custom_excerpt_length' ) );
// 4. Add admin notice (action)
add_action( 'admin_notices', array( $this, 'show_admin_notice' ) );
// 5. Create custom hook in footer
add_action( 'wp_footer', array( $this, 'custom_footer_hook' ) );
}
public function add_content_message( $content ) {
if ( is_single() && in_the_loop() && is_main_query() ) {
$message = '<div class="content-footer">';
$message .= '<p>Thanks for reading! Share if you enjoyed.</p>';
$message .= '</div>';
// Allow others to modify this message
$message = apply_filters( 'hook_practice_content_message', $message );
$content .= $message;
}
return $content;
}
public function log_post_view() {
if ( is_single() ) {
$post_id = get_the_ID();
$views = get_post_meta( $post_id, '_post_views', true );
$views = $views ? $views + 1 : 1;
update_post_meta( $post_id, '_post_views', $views );
// Trigger custom action
do_action( 'hook_practice_post_viewed', $post_id, $views );
}
}
public function custom_excerpt_length( $length ) {
// Shorter excerpts on archive pages
if ( is_archive() ) {
return 20;
}
return $length;
}
public function show_admin_notice() {
if ( current_user_can( 'manage_options' ) ) {
echo '<div class="notice notice-info is-dismissible">';
echo '<p>Hook Practice Plugin is active!</p>';
echo '</div>';
}
}
public function custom_footer_hook() {
// Create extensible footer area
do_action( 'hook_practice_before_footer' );
echo '<div class="custom-footer-area">';
// Allow dynamic footer content
$footer_content = apply_filters( 'hook_practice_footer_content',
'<p>Default footer content</p>'
);
echo $footer_content;
echo '</div>';
do_action( 'hook_practice_after_footer' );
}
}
// Initialize the plugin
new Hook_Practice_Plugin();
// Example of extending the plugin from another file/plugin
add_filter( 'hook_practice_content_message', 'customize_content_message' );
function customize_content_message( $message ) {
return $message . '<p>Follow us on social media!</p>';
}
add_action( 'hook_practice_post_viewed', 'track_popular_posts', 10, 2 );
function track_popular_posts( $post_id, $views ) {
if ( $views > 100 ) {
update_post_meta( $post_id, '_is_popular', true );
}
}
?>
Practice Assignment
Build a "Content Enhancement" plugin that uses WordPress hooks extensively:
- Reading Time Calculator:Add estimated reading time to posts using the_content filter
- Social Share Buttons:Create custom hooks for before/after content that add social sharing buttons
- View Counter:Track and display post view counts using appropriate actions
- Author Bio Box:Automatically add author bio after posts using filters
- Custom Login Redirect:Redirect users based on their role after login
- Admin Dashboard Widget:Add a custom widget showing site statistics
- Comment Moderation:Filter comments for specific keywords before saving
- Make it Extensible:Add at least 5 custom hooks that other developers could use
Bonus Challenges:
- Create a settings page to enable/disable each feature
- Add priority controls for your hooks via settings
- Implement hook debugging mode that logs all hook executions
- Create documentation for your custom hooks