Many people tend to take WordPress hooks for granted (or loathe them). Hooks are a powerful way to change the [ex# name=’behavior’]behavior of WordPress fairly drastically.[/ex#][ex#d name=’behavior’] Hooks are built into WordPress. Without hooks, you’d have to fork the original code and modify it to change it’s behavior. They allow you to literally hook into core functions, change and override the output of functions or disable functionality entirely.[/ex#d] But what, exactly, are hooks?
The public hook API is pretty straightforward: add_action()
, add_filter()
, remove_action()
, remove_filter()
, do_action()
, and apply_filters()
(along with some introspection like has_filter()
). These global functions work together to provide the basic building block of every single theme and plugin. Underneath the covers, they interact with a WP_Hook
class that drives everything.
What is the WP_Hook
class? At it’s core, it’s a [ex# name=’FIFO’]FIFO[/ex#][ex#d name=’FIFO’] (First In, First Out)[/ex#d] priority queue. Put callbacks in the queue, and get them out again, FIFO, by their priority. However, the implementation itself is highly idiomatic, O(n log n)
(worst case) when adding new items, and actually faster (wall clock time) than SPL priority queue, at least when I measured it a few years ago. Visiting items are O(n)
.
Adding Items
The data structures utilized are part of what makes it so fast. Let’s take a look at how items are added to the queue, which is actually a little bit more complicated than you’d think.
[ex#d name=’actions’]Actions in the WP_Hook
class are mostly special wrappers around filters.[/ex#d]
Let’s take a look at add_filter
which is also called when [ex# name=’actions’]adding actions[/ex#]:
public function add_filter( $hook_name, $callback, $priority, $accepted_args ) { $idx = _wp_filter_build_unique_id( $hook_name, $callback, $priority ); $priority_existed = isset( $this->callbacks[ $priority ] ); $this->callbacks[ $priority ][ $idx ] = array( 'function' => $callback, 'accepted_args' => $accepted_args, ); // If we're adding a new priority to the list, put them back in sorted order. if ( ! $priority_existed && count( $this->callbacks ) > 1 ) { ksort( $this->callbacks, SORT_NUMERIC ); } if ( $this->nesting_level > 0 ) { $this->resort_active_iterations( $priority, $priority_existed ); } }
The first thing we’ll notice is that it essentially does a few things:
- Creates a unique id called
idx
for the callback, - Adds the callback to the
callbacks
property, - And resorts
callbacks
property.
Let’s ignore WP_Hook::resort_active_iterations()
for now. Notice that it’s guarded by an if
statement that we’ll get to later.
So, what is this unique id?
// from plugin.php function _wp_filter_build_unique_id( $hook_name, $callback, $priority ) { if ( is_string( $callback ) ) { return $callback; } if ( is_object( $callback ) ) { // Closures are currently implemented as objects. $callback = array( $callback, '' ); } else { $callback = (array) $callback; } if ( is_object( $callback[0] ) ) { // Object class calling. return spl_object_hash( $callback[0] ) . $callback[1]; } elseif ( is_string( $callback[0] ) ) { // Static calling. return $callback[0] . '::' . $callback[1]; } }
The idx
is basically being normalized into a string that represents the callback. See, in PHP, many different things represent a callback. It can be a string like "_return_true"
, an actual Closure
, or an array representing an object and it’s method like [ $my_obj, 'my_method' ]
.
What this means, is that if we have two [ $my_obj, 'my_method' ]
arrays and both $my_obj
points to the same instance, then we’ll end up with the same string. Something like "00000000022d1bd3000000001c1f01bemy_method"
. The last case even protects against the edge case where we have a class called 00000000022d1bd3000000001c1f01be
(ignoring the fact that a class name can’t start with a number) and call a static method with it by inserting the ::
in the id.
So, now we know how the id is created, we can look at the rest of the function:
$priority_existed = isset( $this->callbacks[ $priority ] );
priority_existed
will be true
if ::add_filter
has been called with the same priority. As seen in the next block of code:
$this->callbacks[ $priority ][ $idx ] = array( 'function' => $callback, 'accepted_args' => $accepted_args, );
As we can see, we’ll create a multi-dimensional array using priority
and idx
as the keys and store the callback and the number of arguments the callback accepts.
If the priority didn’t exist, we’ll ask PHP to resort the array and put the callbacks
array back into priority order, only if the array has more than one priority.
// If we're adding a new priority to the list, put them back in sorted order. if ( ! $priority_existed && count( $this->callbacks ) > 1 ) { ksort( $this->callbacks, SORT_NUMERIC ); }
At first glance, it may seem like there’s a performance penalty to “count” the array every time we add to it, however, the performance of count()
is O(1)
. There is a small performance penalty to sort the array, though in practice, most hooks tend to use the default of 10
for priority.
So that’s how we add to the priority queue. Now, how do we take them off?
Applying A Filter
Unlike a traditional priority queue, there’s no way to actually pop items off the queue. We may need to run through the same queue multiple times without losing items on the queue, or even add/remove items from the queue as we’re processing it. These requirements alone make using a “real” priority queue prohibitive. We’re going to visit these cases later, but first, let’s take a look at the implementation:
public function apply_filters( $value, $args ) { if ( ! $this->callbacks ) { return $value; } $nesting_level = $this->nesting_level++; $this->iterations[ $nesting_level ] = array_keys( $this->callbacks ); $num_args = count( $args ); do { $this->current_priority[ $nesting_level ] = current( $this->iterations[ $nesting_level ] ); $priority = $this->current_priority[ $nesting_level ]; foreach ( $this->callbacks[ $priority ] as $the_ ) { if ( ! $this->doing_action ) { $args[0] = $value; } // Avoid the array_slice() if possible. if ( 0 == $the_['accepted_args'] ) { $value = call_user_func( $the_['function'] ); } elseif ( $the_['accepted_args'] >= $num_args ) { $value = call_user_func_array( $the_['function'], $args ); } else { $value = call_user_func_array( $the_['function'], array_slice( $args, 0, (int) $the_['accepted_args'] ) ); } } } while ( false !== next( $this->iterations[ $nesting_level ] ) ); unset( $this->iterations[ $nesting_level ] ); unset( $this->current_priority[ $nesting_level ] ); $this->nesting_level--; return $value; }
This function takes a value, some arguments, and returns a new value. You may use it like apply_filters( 'automatic_updater_disabled', AUTOMATIC_UPDATER_DISABLED ?? false );
to see if automatic updates have been disabled.
The First Iteration
Let’s take a look how this function operates on a simple filter with no nesting. For example, something like this:
$hook = new WP_Hook(); $hook->add_filter( 'test', fn( $val ) => $val + 1, 10, 1 ); var_dump( $hook->apply_filters( 0, [0] ) );
So, upon entering this function, we will note that we store and then increment the current nesting level.
$nesting_level = $this->nesting_level++; $this->iterations[ $nesting_level ] = array_keys( $this->callbacks ); $num_args = count( $args );
Then we set the iterations
array to the keys of the callbacks
. Sooo, iterations
will look something like this:
$iterations[0] = [ 10 ];
Now, we enter a huge [ex# name=’doloop’]do
loop.[/ex#] [ex#d name=’doloop’]If you haven’t seen one of these in awhile (pardon the pun), it means we’ll always “do” the items in the loop before evaluating whether to loop.[/ex#d]
do { $this->current_priority[ $nesting_level ] = current( $this->iterations[ $nesting_level ] ); $priority = $this->current_priority[ $nesting_level ];
From there, we’ll set a current_priority
array to PHP’s internal cursor (current()
) of the iterations
array. Then set a variable called priority
.
If that seems a bit strange to you, don’t worry. Let’s keep going:
foreach ( $this->callbacks[ $priority ] as $the_ ) { if ( ! $this->doing_action ) { $args[0] = $value; }
Now, we’re going to loop through each callback in the callbacks
array for our current priority. I’m not really going to talk about how we decide to call the next callback, except to say that it calls our callback with args
. What’s important is that we determine whether we’re doing_action
and then if we’re not, change the first argument to the current value
. This is the ultimate difference between an action and a filter. With actions, they always receive the original value passed to do_action()
while filters are essentially a “reduce” over a value.
} while ( false !== next( $this->iterations[ $nesting_level ] ) ); unset( $this->iterations[ $nesting_level ] ); unset( $this->current_priority[ $nesting_level ] ); $this->nesting_level--; return $value;
Now we clean up after determining whether we continue the loop by advancing the internal pointer for the iterations
array.
OK. Cool. Let’s try to understand this nesting business.
Nesting…
So, do you remember this when we were adding items?
if ( $this->nesting_level > 0 ) { $this->resort_active_iterations( $priority, $priority_existed ); }
Well, this is to handle the case where you add/remove items from the queue that you’re currently iterating. So, let’s try to understand what resort_active_iterations()
is doing:
/** * Handles resetting callback priority keys mid-iteration. * * @since 4.7.0 * * @param false|int $new_priority Optional. The priority of the new filter being added. Default false, * for no priority being added. * @param bool $priority_existed Optional. Flag for whether the priority already existed before the new * filter was added. Default false. */ private function resort_active_iterations( $new_priority = false, $priority_existed = false ) { $new_priorities = array_keys( $this->callbacks ); // If there are no remaining hooks, clear out all running iterations. if ( ! $new_priorities ) { foreach ( $this->iterations as $index => $iteration ) { $this->iterations[ $index ] = $new_priorities; } return; } $min = min( $new_priorities ); foreach ( $this->iterations as $index => &$iteration ) { $current = current( $iteration ); // If we're already at the end of this iteration, just leave the array pointer where it is. if ( false === $current ) { continue; } $iteration = $new_priorities; if ( $current < $min ) { array_unshift( $iteration, $current ); continue; } while ( current( $iteration ) < $current ) { if ( false === next( $iteration ) ) { break; } } // If we have a new priority that didn't exist, but ::apply_filters() or ::do_action() thinks it's the current priority... if ( $new_priority === $this->current_priority[ $index ] && ! $priority_existed ) { /* * ...and the new priority is the same as what $this->iterations thinks is the previous * priority, we need to move back to it. */ if ( false === current( $iteration ) ) { // If we've already moved off the end of the array, go back to the last element. $prev = end( $iteration ); } else { // Otherwise, just go back to the previous element. $prev = prev( $iteration ); } if ( false === $prev ) { // Start of the array. Reset, and go about our day. reset( $iteration ); } elseif ( $new_priority !== $prev ) { // Previous wasn't the same. Move forward again. next( $iteration ); } } } unset( $iteration ); }
Wow. That’s a lot. The first thing is for when we remove all callbacks:
// If there are no remaining hooks, clear out all running iterations. if ( ! $new_priorities ) { foreach ( $this->iterations as $index => $iteration ) { $this->iterations[ $index ] = $new_priorities; } return; }
Then, we loop through the iterations
array, which if you’ll recall is essentially a list of lists of priorities. So, if we had some priorities of 1, 2, 3, then index
will be the nesting_level
and iteration
will be a list: [ 1, 2, 3 ]
.
After that, we’ll take the internal pointer of iteration
, which is the “current priority” that is being processed in WP_Hook::apply_filters()
. If we’ve already processed everything, then we just leave it.
foreach ( $this->iterations as $index => &$iteration ) { $current = current( $iteration ); // If we're already at the end of this iteration, just leave the array pointer where it is. if ( false === $current ) { continue; }
After that, we simply replace iteration
with new_priorities
. It’s important to note that iteration
is a reference and not a copy. In PHP land, that means it was a literal replacement. What happened to the internal pointer? Why, it gets reset back to the beginning of the array. For example, the following will output 'a','a'
:
$thing = [ 0, 1, 2 ]; $what = &$thing; $new = [ 'a', 'b', 'c' ]; $what = $new; echo implode( ',', [ current( $what ), current( $new ) ] );
What this means, is that once we return back to the loop in WP_Hook::apply_filters()
, we’ll start over. But, say we were currently processing priority 10, we delete all priority 10s and then add a priority 20? The next bit of code ensures that when we return, we’ll still process the later priorities:
$iteration = $new_priorities; if ( $current < $min ) { array_unshift( $iteration, $current ); continue; }
Then, we’ll “fast forward” to the current iteration so we don’t double run things:
while ( current( $iteration ) < $current ) { if ( false === next( $iteration ) ) { break; } }
Then we’ll hit a bit of newer code that includes some helpful comments:
// If we have a new priority that didn't exist, but ::apply_filters() or ::do_action() thinks it's the current priority... if ( $new_priority === $this->current_priority[ $index ] && ! $priority_existed ) { /* * ...and the new priority is the same as what $this->iterations thinks is the previous * priority, we need to move back to it. */ if ( false === current( $iteration ) ) { // If we've already moved off the end of the array, go back to the last element. $prev = end( $iteration ); } else { // Otherwise, just go back to the previous element. $prev = prev( $iteration ); } if ( false === $prev ) { // Start of the array. Reset, and go about our day. reset( $iteration ); } elseif ( $new_priority !== $prev ) { // Previous wasn't the same. Move forward again. next( $iteration ); } }
And bam, we’ve added things to the queue without affecting our current iteration. This is some clever usage of the internal cursor and makes it really hard to reason about, though highly optimized for its use-case.
In fact, it’s the use of the internal cursor that seems to make this code hard to reason about, keeping track of when it moves, resets, or is read from is quite confusing. Though, this is also not easy to replicate in another language; it’s extremely efficient at what it does.
Behavior
Let’s try an understand the behavior of these functions given some sample code:
- Adding a list of items with various priorities
- Adding a list of items with the same priorities
- Adding/removing a new item with a lower priority than we’re currently processing
- Adding/removing a new item with a higher priority than we’re currently processing
To do that, we’re going to have a wee bit of helper code:
<?php require_once __DIR__ . '/src/wp-includes/class-wp-hook.php'; /** * Builds Unique ID for storage and retrieval. * * The old way to serialize the callback caused issues and this function is the * solution. It works by checking for objects and creating a new property in * the class to keep track of the object and new objects of the same class that * need to be added. * * It also allows for the removal of actions and filters for objects after they * change class properties. It is possible to include the property $wp_filter_id * in your class and set it to "null" or a number to bypass the workaround. * However this will prevent you from adding new classes and any new classes * will overwrite the previous hook by the same class. * * Functions and static method callbacks are just returned as strings and * shouldn't have any speed penalty. * * @link https://core.trac.wordpress.org/ticket/3875 * * @since 2.2.3 * @since 5.3.0 Removed workarounds for spl_object_hash(). * `$hook_name` and `$priority` are no longer used, * and the function always returns a string. * @access private * * @param string $hook_name Unused. The name of the filter to build ID for. * @param callable $callback The function to generate ID for. * @param int $priority Unused. The order in which the functions * associated with a particular action are executed. * @return string Unique function ID for usage as array key. */ function _wp_filter_build_unique_id( $hook_name, $callback, $priority ) { if ( is_string( $callback ) ) { return $callback; } if ( is_object( $callback ) ) { // Closures are currently implemented as objects. $callback = array( $callback, '' ); } else { $callback = (array) $callback; } if ( is_object( $callback[0] ) ) { // Object class calling. return spl_object_hash( $callback[0] ) . $callback[1]; } elseif ( is_string( $callback[0] ) ) { // Static calling. return $callback[0] . '::' . $callback[1]; } } function get_output_function(int $serial_number, callable $predicate): callable { return function($input) use ($serial_number, $predicate) { if (DO_OUTPUT) echo "Processing $serial_number\n"; return $predicate($input); }; } function get_serial_funcs(int $num_of_funcs, callable $predicate, int $starting_serial = 0): Generator { for($x = $starting_serial; $x < $num_of_funcs + $starting_serial; $x++) { yield $x => get_output_function($x, $predicate); } } function inject_serial_func(Generator $serial_funcs, callable $predicate, ?int $location = null): Generator { $counter = 0; $key = -1; foreach($serial_funcs as $key => $func) { if($counter++ === $location) { echo "Injected $predicate into $key\n"; yield $key => $predicate; } else { yield $key => $func; } } if($location === null) { yield $key + 1 => $predicate; } } $add_one = fn(int $input) => $input + 1; $hook = new WP_Hook();
List with priorities
I used the following configuration:
const NUM_FUNCS = 10000; const NUM_FUNCS_PER_PRIORITY = 10; const DO_OUTPUT = true; $funcs = get_serial_funcs(NUM_FUNCS, $add_one); foreach($funcs as $serial => $func) { $priority = (NUM_FUNCS - $serial) % NUM_FUNCS_PER_PRIORITY; echo "Inserting serial $serial in $priority\n"; $hook->add_filter('test', $func, $priority, 1); } $start_time = microtime(true); $hook->do_action([0]); $end_time = microtime(true); echo "Processed all hooks in " . ($end_time - $start_time) . "s\n";
With output off, it can process 10,000 predicates in ~3ms on my machine. Not bad… This is what the output looks like for 100 predicates:
Inserting serial 0 in 0 Inserting serial 1 in 9 Inserting serial 2 in 8 Inserting serial 3 in 7 Inserting serial 4 in 6 Inserting serial 5 in 5 Inserting serial 6 in 4 Inserting serial 7 in 3 Inserting serial 8 in 2 Inserting serial 9 in 1 Inserting serial 10 in 0 Inserting serial 11 in 9 Inserting serial 12 in 8 Inserting serial 13 in 7 Inserting serial 14 in 6 Inserting serial 15 in 5 Inserting serial 16 in 4 Inserting serial 17 in 3 Inserting serial 18 in 2 Inserting serial 19 in 1 Inserting serial 20 in 0 Inserting serial 21 in 9 Inserting serial 22 in 8 Inserting serial 23 in 7 Inserting serial 24 in 6 Inserting serial 25 in 5 Inserting serial 26 in 4 Inserting serial 27 in 3 ... Processing 0 Processing 10 Processing 20 Processing 30 Processing 40 Processing 50 Processing 60 Processing 70 Processing 80 Processing 90 Processing 9 Processing 19 Processing 29 Processing 39 Processing 49 Processing 59 Processing 69 Processing 79 Processing 89 Processing 99 Processing 8 Processing 18 Processing 28 Processing 38 Processing 48 Processing 58 Processing 68 Processing 78 Processing 88 Processing 98 Processing 7 Processing 17 Processing 27 Processing 37 Processing 47 Processing 57 Processing 67 Processing 77 Processing 87 Processing 97 Processing 6 Processing 16 Processing 26 Processing 36 Processing 46 Processing 56 Processing 66 Processing 76 Processing 86 Processing 96 Processing 5 Processing 15 Processing 25 Processing 35 Processing 45 Processing 55 Processing 65 Processing 75 Processing 85 Processing 95 Processing 4 Processing 14 Processing 24 Processing 34 Processing 44 Processing 54 Processing 64 Processing 74 Processing 84 Processing 94 Processing 3 Processing 13 Processing 23 Processing 33
List with same priorities
Using the following configuration:
const NUM_FUNCS = 10000; const NUM_FUNCS_PER_PRIORITY = 1; const DO_OUTPUT = true; $funcs = get_serial_funcs(NUM_FUNCS, $add_one); foreach($funcs as $serial => $func) { $priority = (NUM_FUNCS - $serial) % NUM_FUNCS_PER_PRIORITY; echo "Inserting serial $serial in $priority\n"; $hook->add_filter('test', $func, $priority, 1); } $start_time = microtime(true); $hook->do_action([0]); $end_time = microtime(true); echo "Processed all hooks in " . ($end_time - $start_time) . "s\n";
The time to process with output turned off is exactly the same. The output is similiar:
Inserting serial 0 in 0 Inserting serial 1 in 0 Inserting serial 2 in 0 Inserting serial 3 in 0 Inserting serial 4 in 0 Inserting serial 5 in 0 Inserting serial 6 in 0 Inserting serial 7 in 0 Inserting serial 8 in 0 Inserting serial 9 in 0 Inserting serial 10 in 0 Inserting serial 11 in 0 Inserting serial 12 in 0 Inserting serial 13 in 0 Inserting serial 14 in 0 Inserting serial 15 in 0 Inserting serial 16 in 0 Inserting serial 17 in 0 Inserting serial 18 in 0 Inserting serial 19 in 0 Inserting serial 20 in 0 Inserting serial 21 in 0 Inserting serial 22 in 0 Inserting serial 23 in 0 Inserting serial 24 in 0 Inserting serial 25 in 0 Inserting serial 26 in 0 Inserting serial 27 in 0 ... Processing 1190 Processing 1191 Processing 1192 Processing 1193 Processing 1194 Processing 1195 Processing 1196 Processing 1197 Processing 1198 Processing 1199 Processing 1200 Processing 1201 Processing 1202 Processing 1203 Processing 1204 Processing 1205 Processing 1206 Processing 1207 Processing 1208 Processing 1209 Processing 1210 Processing 1211 Processing 1212 Processing 1213 Processing 1214 Processing 1215 Processing 1216 Processing 1217 Processing 1218 Processing 1219 Processing 1220 Processing 1221 Processing 1222 Processing 1223 Processing 1224 Processing 1225 Processing 1226 Processing 1227 Processing 1228 Processing 1229 Processing 1230 Processing 1231 Processing 1232 Processing 1233
Removing and adding lower priority items
This is where it will get interesting, maybe. Let’s find out. Here’s the configuration:
const NUM_FUNCS = 20; const NUM_FUNCS_PER_PRIORITY = 10; const DO_OUTPUT = true; function remove_me(int $a): int { echo "\nI should have been removed!\n\n"; return $a; } function remove_func(int $a): int { global $hook; $hook->remove_filter('test', 'remove_me', 0); echo "removed func!\n"; return $a; } $funcs = get_serial_funcs(NUM_FUNCS, $add_one); $funcs = inject_serial_func($funcs, 'remove_me', 10); // priority 0 $funcs = inject_serial_func($funcs, 'remove_func', 18); // priority 2 foreach($funcs as $serial => $func) { $priority = (NUM_FUNCS - $serial) % NUM_FUNCS_PER_PRIORITY; echo "Inserting serial $serial in $priority\n"; $hook->add_filter('test', $func, $priority, 1); } $start_time = microtime(true); $hook->do_action([0]); $end_time = microtime(true); echo "Processed all hooks in " . ($end_time - $start_time) . "s\n";
And here’s the output:
Inserting serial 0 in 0 Inserting serial 1 in 9 Inserting serial 2 in 8 Inserting serial 3 in 7 Inserting serial 4 in 6 Inserting serial 5 in 5 Inserting serial 6 in 4 Inserting serial 7 in 3 Inserting serial 8 in 2 Inserting serial 9 in 1 Injected remove_me into 10 Inserting serial 10 in 0 Inserting serial 11 in 9 Inserting serial 12 in 8 Inserting serial 13 in 7 Inserting serial 14 in 6 Inserting serial 15 in 5 Inserting serial 16 in 4 Inserting serial 17 in 3 Injected remove_func into 18 Inserting serial 18 in 2 Inserting serial 19 in 1 Processing 0 I should have been removed! Processing 9 Processing 19 Processing 8 removed func! Processing 7 Processing 17 Processing 6 Processing 16 Processing 5 Processing 15 Processing 4 Processing 14 Processing 3 Processing 13 Processing 2 Processing 12 Processing 1 Processing 11
As should have been expected, we can’t change the past! So let’s add something to a lower priority!
Here’s the configuration:
const NUM_FUNCS = 20; const NUM_FUNCS_PER_PRIORITY = 10; const DO_OUTPUT = true; function remove_me(int $a): int { echo "\nI should have been removed!\n\n"; return $a; } function i_am_new(int $a): int { echo "\nI am new!\n\n"; return $a; } function remove_func(int $a): int { global $hook; $hook->remove_filter('test', 'remove_me', 0); echo "removed func!\n"; return $a; } function add_func(int $a): int { global $hook; $hook->add_filter('test', 'i_am_new', 0, 1); echo "added func!\n"; return $a; } $funcs = get_serial_funcs(NUM_FUNCS, $add_one); $funcs = inject_serial_func($funcs, 'add_func', 18); // priority 2 foreach($funcs as $serial => $func) { $priority = (NUM_FUNCS - $serial) % NUM_FUNCS_PER_PRIORITY; echo "Inserting serial $serial in $priority\n"; $hook->add_filter('test', $func, $priority, 1); } $start_time = microtime(true); $hook->do_action([0]); $end_time = microtime(true); echo "Processed all hooks in " . ($end_time - $start_time) . "s\n";
And the output:
Inserting serial 0 in 0 Inserting serial 1 in 9 Inserting serial 2 in 8 Inserting serial 3 in 7 Inserting serial 4 in 6 Inserting serial 5 in 5 Inserting serial 6 in 4 Inserting serial 7 in 3 Inserting serial 8 in 2 Inserting serial 9 in 1 Inserting serial 10 in 0 Inserting serial 11 in 9 Inserting serial 12 in 8 Inserting serial 13 in 7 Inserting serial 14 in 6 Inserting serial 15 in 5 Inserting serial 16 in 4 Inserting serial 17 in 3 Injected add_func into 18 Inserting serial 18 in 2 Inserting serial 19 in 1 Processing 0 Processing 10 Processing 9 Processing 19 Processing 8 added func! Processing 7 Processing 17 Processing 6 Processing 16 Processing 5 Processing 15 Processing 4 Processing 14 Processing 3 Processing 13 Processing 2 Processing 12 Processing 1 Processing 11 Processed all hooks in 0.0010130405426025s
Again, we learn we cannot change the past!
Removing and adding higher priority items
I lied, I’m not going to bore you with the exact same output. You can’t change the future either..
So, what’s up with all that complexity?
Let me show you this monster:
function add_func(int $a): int { global $hook; $hook->add_filter('test', 'i_am_new', 0, 1); $hook->remove_filter('test', 'add_func', 2); echo "added func!\nRecursively calling self\n"; $hook->do_action([100]); echo "Finished recursive call\n"; return $a; }
Now the output is:
Processing 0 Processing 10 Processing 9 Processing 19 Processing 8 added func! Recursively calling self Processing 0 Processing 10 I am new! Processing 9 Processing 19 Processing 8 Processing 7 Processing 17 Processing 6 Processing 16 Processing 5 Processing 15 Processing 4 Processing 14 Processing 3 Processing 13 Processing 2 Processing 12 Processing 1 Processing 11 Finished recursive call Processing 7 Processing 17 Processing 6 Processing 16 Processing 5 Processing 15 Processing 4 Processing 14 Processing 3 Processing 13 Processing 2 Processing 12 Processing 1 Processing 11
Pretty neat? It reran all the previous predicates the second time. However, there’s an exception here:
function add_func(int $a): int { global $hook; $hook->remove_all_filters(); $hook->add_filter('test', 'i_am_new', 0, 1); echo "added func!\nRecursively calling self\n"; $hook->do_action([100]); echo "Finished recursive call\n"; return $a; }
The output is unexpected after seeing it iterate through the queue twice:
Inserting serial 0 in 0 Inserting serial 1 in 9 Inserting serial 2 in 8 Inserting serial 3 in 7 Inserting serial 4 in 6 Inserting serial 5 in 5 Inserting serial 6 in 4 Inserting serial 7 in 3 Inserting serial 8 in 2 Inserting serial 9 in 1 Inserting serial 10 in 0 Inserting serial 11 in 9 Inserting serial 12 in 8 Inserting serial 13 in 7 Inserting serial 14 in 6 Inserting serial 15 in 5 Inserting serial 16 in 4 Inserting serial 17 in 3 Injected add_func into 18 Inserting serial 18 in 2 Inserting serial 19 in 1 Processing 0 Processing 10 Processing 9 Processing 19 Processing 8 added func! Recursively calling self I am new! Finished recursive call
Pretty. interesting.
Dissection
It seems that hooks have some very interesting behaviors and I can’t wait to see how WordPress takes advantage of it. Let’s move up the stack to kses
, after which, we’ll be able to explore how WordPress boots.