LESSON 3 ⏱️ 14 min read

Hooks Deep Dive: Actions, Filters & Custom Hooks

Understanding the Hooks System

WordPress hooks are the foundation of plugin development. They let you "hook into" WordPress at specific points to add or modify functionality.

There are two types:

  • Actions – Do something at a specific point
  • Filters – Modify data before it's used

Actions: Executing Code at Specific Points

Actions let you run code when WordPress reaches certain points:

// Adding an action
add_action( 'init', [ $this, 'register_post_type' ] );
add_action( 'admin_menu', [ $this, 'add_menu_page' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'load_assets' ] );

// Executing (doing) an action
do_action( 'my_custom_action', $arg1, $arg2 );

Common Actions for Plugins

ActionWhen It FiresCommon Use
plugins_loadedAfter all plugins loadInitialize plugin
initWordPress initializedRegister CPTs, taxonomies
admin_initAdmin area initializingRegister settings
admin_menuBuilding admin menuAdd menu pages
wp_enqueue_scriptsFrontend scriptsEnqueue CSS/JS
admin_enqueue_scriptsAdmin scriptsEnqueue admin assets
rest_api_initREST API initializingRegister endpoints
save_postPost savedCustom save logic

Filters: Modifying Data

Filters let you modify data before WordPress uses it:

// Adding a filter
add_filter( 'the_content', [ $this, 'append_task_list' ] );
add_filter( 'plugin_action_links_' . TM_PLUGIN_BASENAME, [ $this, 'add_settings_link' ] );

// Applying a filter
$content = apply_filters( 'task_manager_task_title', $task->title, $task );

Example: Adding Settings Link

<?php
// In Plugin class

public function __construct() {
    add_filter(
        'plugin_action_links_' . plugin_basename( TM_PLUGIN_FILE ),
        [ $this, 'add_action_links' ]
    );
}

public function add_action_links( array $links ): array {
    $settings_link = sprintf(
        '<a href="%s">%s</a>',
        admin_url( 'admin.php?page=task-manager-settings' ),
        __( 'Settings', 'task-manager' )
    );
    
    array_unshift( $links, $settings_link );
    return $links;
}

Hook Priority

The third parameter controls execution order (default: 10):

// Lower numbers run first
add_action( 'init', [ $this, 'early_init' ], 5 );   // Runs first
add_action( 'init', [ $this, 'normal_init' ] );      // Priority 10
add_action( 'init', [ $this, 'late_init' ], 20 );   // Runs last

// For filters, priority affects when your modification happens
add_filter( 'the_content', [ $this, 'first_pass' ], 5 );
add_filter( 'the_content', [ $this, 'final_pass' ], 999 );
Priority Tips:
  • Use 5 or lower to run before most plugins
  • Use 10 (default) for normal operations
  • Use 20+ to run after standard hooks
  • Use 999 or PHP_INT_MAX for absolute last

Accepted Arguments

The fourth parameter specifies how many arguments your callback receives:

// Default: 1 argument
add_action( 'save_post', [ $this, 'on_save' ] );

public function on_save( $post_id ) {
    // Only receives post ID
}

// Request all 3 arguments
add_action( 'save_post', [ $this, 'on_save' ], 10, 3 );

public function on_save( int $post_id, WP_Post $post, bool $update ): void {
    if ( $update && $post->post_type === 'task' ) {
        $this->sync_task( $post_id );
    }
}

Removing Hooks

Sometimes you need to remove hooks added by WordPress or other plugins:

// Remove a function
remove_action( 'wp_head', 'wp_generator' );

// Remove a class method - requires exact same instance
remove_action( 'admin_menu', [ $other_plugin, 'add_menu' ] );

// Remove with priority (must match original)
remove_filter( 'the_content', 'wpautop', 10 );

Removing Class Methods

This is tricky when the original instance isn't accessible:

// Find and remove a class method
global $wp_filter;

if ( isset( $wp_filter['admin_menu'] ) ) {
    foreach ( $wp_filter['admin_menu']->callbacks as $priority => $callbacks ) {
        foreach ( $callbacks as $id => $callback ) {
            if ( is_array( $callback['function'] ) 
                && is_object( $callback['function'][0] )
                && get_class( $callback['function'][0] ) === 'OtherPluginAdminMenu'
            ) {
                remove_action( 'admin_menu', $callback['function'], $priority );
            }
        }
    }
}

Creating Custom Hooks

Make your plugin extensible by adding your own hooks:

<?php
// In TasksTable class

public function create( array $data ): int {
    // Allow modification before insert
    $data = apply_filters( 'task_manager_before_create', $data );
    
    // Validate
    if ( empty( $data['title'] ) ) {
        return 0;
    }
    
    global $wpdb;
    $wpdb->insert( $this->table_name, $data );
    $task_id = $wpdb->insert_id;
    
    // Notify after creation
    do_action( 'task_manager_task_created', $task_id, $data );
    
    return $task_id;
}

public function delete( int $id ): bool {
    // Allow cancellation
    if ( ! apply_filters( 'task_manager_can_delete', true, $id ) ) {
        return false;
    }
    
    global $wpdb;
    $result = $wpdb->delete( $this->table_name, [ 'id' => $id ] );
    
    if ( $result ) {
        do_action( 'task_manager_task_deleted', $id );
    }
    
    return (bool) $result;
}

Documenting Your Hooks

Use DocBlocks to document hooks:

/**
 * Fires after a task is created.
 *
 * @since 1.0.0
 *
 * @param int   $task_id The new task ID.
 * @param array $data    The task data that was inserted.
 */
do_action( 'task_manager_task_created', $task_id, $data );

/**
 * Filters task data before database insertion.
 *
 * @since 1.0.0
 *
 * @param array $data Task data array.
 * @return array Modified task data.
 */
$data = apply_filters( 'task_manager_before_create', $data );

Hooks in OOP Architecture

Organize hooks cleanly in your classes:

<?php
// src/Admin/AdminMenu.php

namespace TaskManagerAdmin;

class AdminMenu {
    
    public function __construct() {
        $this->init_hooks();
    }
    
    private function init_hooks(): void {
        add_action( 'admin_menu', [ $this, 'register_menus' ] );
        add_action( 'admin_init', [ $this, 'register_settings' ] );
    }
    
    public function register_menus(): void {
        add_menu_page(
            __( 'Task Manager', 'task-manager' ),
            __( 'Tasks', 'task-manager' ),
            'manage_options',
            'task-manager',
            [ $this, 'render_main_page' ],
            'dashicons-yes-alt',
            30
        );
        
        // Fire hook for extensions to add submenus
        do_action( 'task_manager_admin_menu_registered' );
    }
}

Hook Best Practices

1. Use Early Returns

public function on_save_post( int $post_id ): void {
    // Skip autosave
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }
    
    // Skip revisions
    if ( wp_is_post_revision( $post_id ) ) {
        return;
    }
    
    // Skip wrong post type
    if ( get_post_type( $post_id ) !== 'task' ) {
        return;
    }
    
    // Now do the actual work
    $this->process_task( $post_id );
}

2. Always Return Filter Values

// ✅ Correct - always return
public function filter_title( string $title ): string {
    if ( $this->should_modify( $title ) ) {
        return $this->modify( $title );
    }
    return $title; // Always return original if not modifying
}

// ❌ Wrong - forgets return
public function filter_title( string $title ): string {
    if ( $this->should_modify( $title ) ) {
        return $this->modify( $title );
    }
    // Oops - returns null!
}

3. Namespace Your Hooks

// ❌ Generic names - might conflict
do_action( 'task_created', $id );
do_action( 'before_save', $data );

// ✅ Namespaced - unique to your plugin
do_action( 'task_manager_task_created', $id );
do_action( 'task_manager_before_save', $data );

Next Steps

In Lesson 4, we'll build the admin interface using add_menu_page() and the Settings API to create a professional settings page.

🎯 Lesson Complete! You now understand WordPress hooks and can create extensible plugins.