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.