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:
- WordPress Hooks — Core WP hooks like
init,admin_init,the_content - 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 prefixwpFilter()— filters WP, no prefixcontext->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
| Priority | Use Case |
|---|---|
| 1-5 | Run before almost everything |
| 10 | Default priority |
| 20-50 | Run after most plugins |
| 99-999 | Run last (careful with output) |
| PHP_INT_MAX | Absolutely 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
- Service Providers — Register hooks in providers
- Request Lifecycle — When hooks fire