Skip to content

WordPress Hooks Pattern in Wikit

Overview

Hooks are the foundation of WordPress extensibility. Wikit uses WordPress hooks extensively and provides patterns for organizing and managing them effectively within the framework's architecture.

Hook Types

Actions

Execute code at specific points without modifying data.

Filters

Modify data before it's used or returned.

Registering Hooks

In Singleton Classes

php
namespace WDG\Theme;

use WDG\Core\SingletonTrait;

class Navigation {
	
	use SingletonTrait;
	
	/**
	 * Constructor - hooks are registered here
	 */
	protected function __construct() {
		$this->init_hooks();
	}
	
	/**
	 * Initialize all hooks for this feature
	 */
	private function init_hooks(): void {
		// Actions
		add_action( 'init', [ $this, 'register_menus' ] );
		add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] );
		add_action( 'after_setup_theme', [ $this, 'theme_support' ] );
		
		// Filters
		add_filter( 'nav_menu_css_class', [ $this, 'menu_classes' ], 10, 2 );
		add_filter( 'wp_nav_menu_args', [ $this, 'menu_args' ] );
		
		// Conditional hooks
		if ( is_admin() ) {
			add_action( 'admin_menu', [ $this, 'admin_menu' ] );
		}
	}
}

In PostType Classes

php
namespace WDG\App\PostType;

use WDG\Core\PostType;

class Products extends PostType {
	
	protected $post_type = 'product';
	
	public function init() {
		parent::init();
		
		// PostType-specific hooks
		add_action( 'save_post_' . $this->post_type, [ $this, 'save_post' ], 10, 3 );
		add_filter( 'manage_' . $this->post_type . '_posts_columns', [ $this, 'admin_columns' ] );
		add_action( 'manage_' . $this->post_type . '_posts_custom_column', [ $this, 'admin_column_content' ], 10, 2 );
	}
	
	/**
	 * Handle post save
	 */
	public function save_post( int $post_id, \WP_Post $post, bool $update ): void {
		// Verify nonce
		if ( ! isset( $_POST['product_nonce'] ) || ! wp_verify_nonce( $_POST['product_nonce'], 'save_product' ) ) {
			return;
		}
		
		// Check autosave
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return;
		}
		
		// Check permissions
		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			return;
		}
		
		// Save meta data
		if ( isset( $_POST['price'] ) ) {
			update_post_meta( $post_id, 'price', sanitize_text_field( $_POST['price'] ) );
		}
	}
}

Hook Priority and Order

Priority Conventions

php
// EARLY: Priority 5 - Setup and initialization
add_action( 'init', [ $this, 'early_init' ], 5 );

// DEFAULT: Priority 10 - Standard functionality
add_action( 'init', [ $this, 'standard_init' ] ); // 10 is implicit

// LATE: Priority 20 - Cleanup and finalization
add_action( 'init', [ $this, 'late_init' ], 20 );

// VERY LATE: Priority 999 - Last resort modifications
add_action( 'init', [ $this, 'final_init' ], 999 );

Execution Order Pattern

php
class FeatureLoader {
	
	protected function __construct() {
		// 1. Register post types and taxonomies
		add_action( 'init', [ $this, 'register_types' ], 0 );
		
		// 2. Register blocks
		add_action( 'init', [ $this, 'register_blocks' ], 5 );
		
		// 3. Standard initialization
		add_action( 'init', [ $this, 'init_features' ], 10 );
		
		// 4. Late initialization depending on other plugins
		add_action( 'init', [ $this, 'late_init' ], 20 );
	}
}

Custom Hooks

Creating Custom Actions

php
namespace WDG\App;

class CustomFeature {
	
	/**
	 * Process data with custom hook points
	 */
	public function process_data( array $data ): array {
		// Before processing hook
		do_action( 'wdg/before_process_data', $data );
		
		// Process data
		$processed = $this->transform_data( $data );
		
		// After processing hook
		do_action( 'wdg/after_process_data', $processed, $data );
		
		return $processed;
	}
}

// Using the custom action
add_action( 'wdg/before_process_data', function( $data ) {
	error_log( 'Processing: ' . wp_json_encode( $data ) );
} );

Creating Custom Filters

php
namespace WDG\Theme;

class Content {
	
	/**
	 * Get filtered content
	 */
	public function get_content( string $content ): string {
		// Apply custom filter
		$content = apply_filters( 'wdg/content/before_display', $content );
		
		// Process content
		$content = $this->process_content( $content );
		
		// Apply another filter with context
		$content = apply_filters( 'wdg/content/after_process', $content, get_the_ID() );
		
		return $content;
	}
}

// Using the custom filter
add_filter( 'wdg/content/before_display', function( $content ) {
	return wpautop( $content );
} );

add_filter( 'wdg/content/after_process', function( $content, $post_id ) {
	if ( get_post_type( $post_id ) === 'product' ) {
		$content .= '<div class="product-cta">Buy Now!</div>';
	}
	return $content;
}, 10, 2 );

Common WordPress Hooks

Action Hooks by Lifecycle

php
// Theme/Plugin Initialization
add_action( 'after_setup_theme', [ $this, 'theme_setup' ] );
add_action( 'init', [ $this, 'initialize' ] );
add_action( 'widgets_init', [ $this, 'register_widgets' ] );

// Request Lifecycle
add_action( 'parse_request', [ $this, 'handle_request' ] );
add_action( 'template_redirect', [ $this, 'redirect_logic' ] );
add_action( 'wp', [ $this, 'wp_loaded' ] );

// Head and Footer
add_action( 'wp_head', [ $this, 'add_head_tags' ] );
add_action( 'wp_footer', [ $this, 'add_footer_scripts' ] );

// Admin
add_action( 'admin_init', [ $this, 'admin_initialize' ] );
add_action( 'admin_menu', [ $this, 'add_menu_pages' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'admin_assets' ] );

// User
add_action( 'wp_login', [ $this, 'on_user_login' ], 10, 2 );
add_action( 'wp_logout', [ $this, 'on_user_logout' ] );
add_action( 'user_register', [ $this, 'on_user_register' ] );

Filter Hooks by Purpose

php
// Content Filters
add_filter( 'the_content', [ $this, 'filter_content' ] );
add_filter( 'the_title', [ $this, 'filter_title' ], 10, 2 );
add_filter( 'the_excerpt', [ $this, 'filter_excerpt' ] );

// Query Filters
add_filter( 'pre_get_posts', [ $this, 'modify_query' ] );
add_filter( 'posts_where', [ $this, 'filter_where' ], 10, 2 );
add_filter( 'posts_orderby', [ $this, 'filter_orderby' ], 10, 2 );

// URL Filters
add_filter( 'post_link', [ $this, 'filter_permalink' ], 10, 2 );
add_filter( 'term_link', [ $this, 'filter_term_link' ], 10, 3 );
add_filter( 'home_url', [ $this, 'filter_home_url' ], 10, 2 );

// User Filters
add_filter( 'user_has_cap', [ $this, 'filter_capabilities' ], 10, 3 );
add_filter( 'authenticate', [ $this, 'custom_authenticate' ], 10, 3 );

AJAX Hooks

Registering AJAX Handlers

php
namespace WDG\App;

class AjaxHandler {
	
	use \WDG\Core\SingletonTrait;
	
	protected function __construct() {
		// For logged-in users
		add_action( 'wp_ajax_wdg_get_data', [ $this, 'get_data' ] );
		
		// For non-logged-in users
		add_action( 'wp_ajax_nopriv_wdg_get_data', [ $this, 'get_data' ] );
		
		// Admin only
		add_action( 'wp_ajax_wdg_admin_action', [ $this, 'admin_action' ] );
	}
	
	/**
	 * Handle AJAX request
	 */
	public function get_data(): void {
		// Verify nonce
		check_ajax_referer( 'wdg_ajax_nonce', 'nonce' );
		
		// Get and sanitize data
		$type = isset( $_POST['type'] ) ? sanitize_key( $_POST['type'] ) : '';
		
		// Process request
		$data = $this->fetch_data( $type );
		
		// Return JSON response
		if ( $data ) {
			wp_send_json_success( $data );
		} else {
			wp_send_json_error( 'No data found' );
		}
	}
}

REST API Hooks

Registering REST Routes

php
namespace WDG\App;

class RestEndpoints {
	
	use \WDG\Core\SingletonTrait;
	
	protected function __construct() {
		add_action( 'rest_api_init', [ $this, 'register_routes' ] );
	}
	
	/**
	 * Register REST API routes
	 */
	public function register_routes(): void {
		register_rest_route( 'wdg/v1', '/products', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'get_products' ],
			'permission_callback' => '__return_true',
			'args'                => [
				'per_page' => [
					'default'           => 10,
					'validate_callback' => function( $param ) {
						return is_numeric( $param );
					},
				],
			],
		] );
		
		register_rest_route( 'wdg/v1', '/product/(?P<id>\d+)', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'update_product' ],
			'permission_callback' => function() {
				return current_user_can( 'edit_posts' );
			},
			'args'                => [
				'id' => [
					'validate_callback' => function( $param ) {
						return is_numeric( $param );
					},
				],
			],
		] );
	}
}

Block Editor Hooks

Block Registration

php
namespace WDG\Theme;

class BlockEditor {
	
	use \WDG\Core\SingletonTrait;
	
	protected function __construct() {
		add_action( 'init', [ $this, 'register_blocks' ] );
		add_filter( 'block_categories_all', [ $this, 'block_categories' ] );
		add_action( 'enqueue_block_editor_assets', [ $this, 'editor_assets' ] );
	}
	
	/**
	 * Register custom blocks
	 */
	public function register_blocks(): void {
		register_block_type( 
			get_template_directory() . '/blocks/custom-block',
			[
				'render_callback' => [ $this, 'render_custom_block' ],
			]
		);
	}
	
	/**
	 * Add custom block category
	 */
	public function block_categories( array $categories ): array {
		return array_merge(
			[
				[
					'slug'  => 'wikit',
					'title' => __( 'Wikit Blocks', 'wikit' ),
					'icon'  => 'admin-site',
				],
			],
			$categories
		);
	}
}

Dynamic Hooks

Using Variable Hook Names

php
class DynamicHooks {
	
	private string $prefix = 'wdg';
	private string $feature = 'products';
	
	protected function __construct() {
		// Dynamic action name
		$action_name = "{$this->prefix}/{$this->feature}/init";
		add_action( $action_name, [ $this, 'feature_init' ] );
		
		// Dynamic filter name
		$filter_name = "{$this->prefix}/{$this->feature}/data";
		add_filter( $filter_name, [ $this, 'filter_data' ] );
		
		// Post type specific
		$post_type = 'product';
		add_action( "save_post_{$post_type}", [ $this, 'save_post' ] );
		add_filter( "manage_{$post_type}_posts_columns", [ $this, 'columns' ] );
	}
}

Removing Hooks

Unhooking Actions and Filters

php
class HookRemoval {
	
	protected function __construct() {
		// Remove with specific priority
		remove_action( 'wp_head', 'wp_generator' );
		
		// Remove class method with priority
		remove_filter( 'the_content', [ SomeClass::instance(), 'filter_method' ], 10 );
		
		// Remove all hooks for an action
		remove_all_actions( 'wp_footer' );
		
		// Conditional removal
		if ( ! is_admin() ) {
			remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
		}
	}
	
	/**
	 * Remove hooks added by other plugins/themes
	 */
	public function late_removal(): void {
		// Must run after the hook was added
		add_action( 'init', function() {
			remove_action( 'init', 'unwanted_function', 15 );
		}, 14 ); // Run at priority 14 to remove priority 15
	}
}

Hook Documentation

Documenting Custom Hooks

php
/**
 * Fires before product data is saved
 *
 * @since 1.0.0
 *
 * @param array $data    Product data array
 * @param int   $post_id Post ID
 */
do_action( 'wdg/product/before_save', $data, $post_id );

/**
 * Filters the product price display
 *
 * @since 1.0.0
 *
 * @param string $price_html Formatted price HTML
 * @param float  $price      Raw price value
 * @param int    $product_id Product post ID
 * @return string Modified price HTML
 */
$price_html = apply_filters( 'wdg/product/price_html', $price_html, $price, $product_id );

Best Practices

Do's

  • ✅ Use descriptive hook names
  • ✅ Document custom hooks thoroughly
  • ✅ Use namespaced hook names (wdg/feature/hook)
  • ✅ Check nonces in form/AJAX handlers
  • ✅ Verify capabilities in admin hooks
  • ✅ Use appropriate priorities
  • ✅ Return filtered values
  • ✅ Remove hooks when necessary

Don'ts

  • ❌ Don't use generic hook names
  • ❌ Don't forget the number of parameters
  • ❌ Don't modify global variables in hooks
  • ❌ Don't echo in filter callbacks
  • ❌ Don't use anonymous functions for removable hooks
  • ❌ Don't assume hook execution order

Hook Debugging

Viewing Attached Hooks

php
// Debug: See all hooks attached to an action
global $wp_filter;
if ( isset( $wp_filter['init'] ) ) {
	error_log( print_r( $wp_filter['init'], true ) );
}

// Check if action has been fired
if ( did_action( 'init' ) ) {
	// Init has already run
}

// Check current filter
$current = current_filter();
if ( $current === 'the_content' ) {
	// Currently filtering content
}

Performance Considerations

  1. Minimize Hook Usage: Don't add unnecessary hooks
  2. Use Appropriate Priorities: Let WordPress core run first
  3. Conditional Loading: Only add hooks when needed
  4. Remove Unused Hooks: Clean up third-party hooks
  5. Cache Hook Results: Don't recalculate on every call

Common Patterns

Conditional Hook Registration

php
protected function __construct() {
	// Only on frontend
	if ( ! is_admin() ) {
		add_action( 'wp_enqueue_scripts', [ $this, 'frontend_assets' ] );
	}
	
	// Only in admin
	if ( is_admin() ) {
		add_action( 'admin_init', [ $this, 'admin_init' ] );
	}
	
	// Only for specific post type
	add_action( 'wp', function() {
		if ( is_singular( 'product' ) ) {
			add_filter( 'the_content', [ $this, 'product_content' ] );
		}
	} );
	
	// Only for logged-in users
	if ( is_user_logged_in() ) {
		add_action( 'init', [ $this, 'user_features' ] );
	}
}

One-time Hooks

php
// Self-removing hook
add_action( 'init', function() {
	// Do something once
	update_option( 'wdg_initialized', true );
	
	// Remove this hook
	remove_action( 'init', __FUNCTION__ );
} );

Hooks are the backbone of WordPress extensibility. Master them to create flexible, maintainable Wikit applications.

Released under the MIT License.