This is the last part. Every failure in this series — a missing return, running too early, a priority race, the wrong side of a filter chain, a removal that never removed — looks much the same on screen. “It does not work.” So you need an order for checking rather than guessing.
Four questions, in order
The order matters because each answer is the premise of the next. Tuning priority on a callback that was never registered is wasted effort, and so is reading a callback body on a page where the hook never fires at all.
Read the registry directly
WordPress keeps every registration in the global $wp_filter. Pull out a hook by name and you have the callbacks, grouped by priority.
add_action( 'wp_footer', function () {
$hook = $GLOBALS['wp_filter']['the_content'] ?? null;
if ( ! $hook ) {
return;
}
foreach ( $hook->callbacks as $priority => $group ) {
foreach ( $group as $cb ) {
$fn = $cb['function'];
if ( is_string( $fn ) ) {
$name = $fn;
} elseif ( is_array( $fn ) ) {
$owner = is_object( $fn[0] ) ? get_class( $fn[0] ) : $fn[0];
$name = $owner . '::' . $fn[1];
} else {
$name = 'Closure';
}
error_log( sprintf( '%4d %s (args %d)', $priority, $name, $cb['accepted_args'] ) );
}
}
}, 9999 );
That one block answers four things at once: is my callback in the list at all, at what number, who sits behind me and could overwrite my value, and how many arguments it was registered to accept.
Entries printed as Closure rather than a name are exactly the callbacks you cannot remove by name from part six. You can see them; there is simply no handle to grab.
Lighter checks first
has_filter() is especially useful. Given a callback it returns the priority it is attached at, which turns part six’s “the removal failed because the priority differed” into something you can assert in code. If it is not attached, you get false.
did_action() settles the second question — does this hook actually fire on this screen — in one line. If it is zero, no amount of staring at the callback will help.
Use sparingly
Attaching to the special all hook hands you every hook that fires. It is genuinely useful for answering “what runs at this point”, but it is called thousands of times per request, so switch it on briefly and switch it off again.
There are development tools that surface queries and hooks on the page as well. Most of them install a drop-in that adds overhead to every request, which makes them development and staging only. Left running in production they cost you performance and leak internals at the same time.
Closing the series
Eight parts, condensed: a filter must always return; register on the world that init has built; choose your position rather than trying to win a priority race; start every pre_get_posts callback with two guard lines; decide whether you want raw or finished markup before attaching to the_content; check what remove_action returned; and leave places in your own code for other people to attach.
They share one idea. Hooks exist so that nobody has to edit core, and keeping that benefit means using them in a way that does not require editing anyone else’s code either.
How these principles play out on live sites is published step by step on our process page, and if you would like version upgrades, performance work and a security review handled as one job, that is our optimization program.