Hook System

The WPZylos Hook system provides a clean API for WordPress hooks while ensuring proper prefixing for custom hooks.

The Two Types of Hooks

WPZylos distinguishes between:

  1. WordPress Hooks — Core WP hooks like init, admin_init, the_content
  2. Custom Hooks — Plugin-specific hooks that must be prefixed
graph LR
    subgraph WordPress Core Hooks
        init
        admin_init
        the_content
        save_post
    end

    subgraph Custom Hooks via Context
        mp_settings_saved["mp_settings_saved"]
        mp_user_registered["mp_user_registered"]
        mp_cache_cleared["mp_cache_cleared"]
    end

    HM[HookManager] --> |wpAction/wpFilter| init
    HM --> |context->hook| mp_settings_saved

HookManager API

Hooking into WordPress Actions

use WPZylos\Framework\Hooks\HookManager;

$hooks = new HookManager($context);

// Hook into WordPress core action (no prefix)
$hooks->wpAction('init', function () {
    // Runs on WordPress 'init'
});

// With priority
$hooks->wpAction('admin_init', [$this, 'setupAdmin'], 20);

// With accepted args
$hooks->wpAction('save_post', function ($postId, $post) {
    // Handle post save
}, 10, 2);

Hooking into WordPress Filters

// Filter (no prefix)
$hooks->wpFilter('the_title', function (string $title): string {
    return $title . ' | My Plugin';
});

// With priority and args
$hooks->wpFilter('the_content', [$this, 'appendCta'], 99, 1);

Custom Plugin Hooks

For plugin-specific hooks, use context to get prefixed names:

// Create a custom hook
$hookName = $context->hook('settings_saved');
// Returns: mp_settings_saved

// Fire the custom hook
do_action($hookName, $settings);

// Listen to the custom hook
add_action($hookName, function ($settings) {
    // Handle settings saved
});

Dispatching Custom Hooks

class SettingsController
{
    public function save(): void
    {
        // Save settings...

        // Fire custom hook (prefixed)
        do_action($this->context->hook('settings_saved'), $settings);
    }
}

Removing Hooks

// Remove your own hook
remove_action('init', [$this, 'myCallback']);

// Remove WordPress default
remove_action('wp_head', 'wp_generator');

Why We Did It This Way

Unprefixed WordPress Hooks

Problem: If wpAction('init', ...) prefixed to mp_init, it would never fire because WordPress dispatches init, not mp_init.

Solution: wpAction() and wpFilter() hook directly into the exact name provided.

// This hooks into WordPress 'init', not 'mp_init'
$hooks->wpAction('init', fn() => ...);

// Internally: add_action('init', fn() => ...)

Prefixed Custom Hooks

Problem: Two plugins might both use do_action('settings_saved'), causing cross-triggering.

Solution: Always use context to prefix custom hook names.

// Plugin A (prefix: pa_)
do_action($contextA->hook('settings_saved'));  // pa_settings_saved

// Plugin B (prefix: pb_)
do_action($contextB->hook('settings_saved'));  // pb_settings_saved

// No collision!

Explicit wp* Methods

Problem: A generic action() method is ambiguous — does it prefix or not?

Solution: Explicit method names make intent clear:

  • wpAction() — hooks into WP, no prefix
  • wpFilter() — filters WP, no prefix
  • context->hook() — creates prefixed custom hook name
// Intent is clear
$hooks->wpAction('init', ...);           // WordPress hook
$hookName = $context->hook('my_event');  // Custom hook (prefixed)

Common Patterns

Admin-Only Actions

$hooks->wpAction('admin_init', function () {
    // Only runs in admin
});

// Or check manually
$hooks->wpAction('init', function () {
    if (!is_admin()) {
        return;
    }
    // Admin-only code
});

Frontend-Only Filters

$hooks->wpAction('wp', function () {
    if (is_admin()) {
        return;
    }

    $this->hooks->wpFilter('the_content', [$this, 'addWidget']);
});

Conditional Hook Registration

$hooks->wpAction('init', function () {
    if (!current_user_can('manage_options')) {
        return;
    }

    // Register admin-only hooks
    add_action('admin_menu', [$this, 'addMenus']);
});

AJAX Hooks

// Logged-in users
$hooks->wpAction('wp_ajax_mp_save_settings', [$this, 'handleSave']);

// Non-logged-in users
$hooks->wpAction('wp_ajax_nopriv_mp_public_action', [$this, 'handlePublic']);

Hook Priority Guide

PriorityUse Case
1-5Run before almost everything
10Default priority
20-50Run after most plugins
99-999Run last (careful with output)
PHP_INT_MAXAbsolutely last
// Run early
$hooks->wpAction('init', $callback, 1);

// Run late
$hooks->wpFilter('the_content', $callback, 99);

Testing Hooks

public function testSettingsSavedHookFires(): void
{
    $fired = false;

    add_action($this->context->hook('settings_saved'), function () use (&$fired) {
        $fired = true;
    });

    $this->controller->save();

    $this->assertTrue($fired);
}

Anti-Patterns

Bad: Hardcoded Custom Hook Names

// Bad: No prefix, will conflict
do_action('my_plugin_settings_saved');

Bad: Prefixing WordPress Hooks

// Bad: 'mp_init' doesn't exist in WordPress
$hooks->wpAction($context->hook('init'), fn() => ...);

Good: Correct Usage

// WordPress hooks: direct name
$hooks->wpAction('init', fn() => ...);

// Custom hooks: via context
do_action($context->hook('settings_saved'));

Next Steps