Skip to main content

Course Progress

Loading...

WordPress Hooks System: Actions and Filters

Duration: 45 minutes
Module 4: Session 2

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.

💡
Key Concept
Hooks are the backbone of WordPress customization. They enable a modular architecture where functionality can be added, removed, or modified without changing the original code.

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

graph TD A[WordPress Core Execution] --> B{Hook Point Reached} B -->|Action Hook| C[do_action called] B -->|Filter Hook| D[apply_filters called] C --> E[All hooked functions execute] D --> F[All hooked functions modify data] E --> G[Continue execution] F --> H[Return modified data] H --> G G --> I[Next hook point or end]

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:Useif ( ! 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

⚠️
Common Mistakes
  • Forgetting to return in filters:Always return a value, even if unchanged
  • Wrong priority when removing:Must match exactly when using remove_action/filter
  • Infinite loops:Be careful when hooking into save_post and updating post meta
  • Direct output in filters:Filters should return, not echo
  • Hook timing issues:Some hooks fire before others are available

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:

💻
Try It Now

Create a plugin that:

  1. Adds a custom message after post content (using filter)
  2. Logs when posts are viewed (using action)
  3. Creates a custom hook that others can use
  4. Modifies the excerpt length
  5. Adds a custom admin notice

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

Additional Resources