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
| Action | When It Fires | Common Use |
|---|---|---|
plugins_loaded | After all plugins load | Initialize plugin |
init | WordPress initialized | Register CPTs, taxonomies |
admin_init | Admin area initializing | Register settings |
admin_menu | Building admin menu | Add menu pages |
wp_enqueue_scripts | Frontend scripts | Enqueue CSS/JS |
admin_enqueue_scripts | Admin scripts | Enqueue admin assets |
rest_api_init | REST API initializing | Register endpoints |
save_post | Post saved | Custom 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 );- 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.