Hooks: Add the new class WP_Hook, and modify hook handling to make use of it.

Filters and actions have been the basis of WordPress' plugin functionality since time immemorial, they've always been a reliable method for acting upon the current state of WordPress, and will continue to be so.

Over the years, however, edge cases have cropped up. Particularly when it comes to recursively executing hooks, or a hook adding and removing itself, the existing implementation struggled to keep up with more complex use cases.

And so, we introduce `WP_Hook`. By changing `$wp_filter` from an array of arrays, to an array of objects, we reduce the complexity of the hook handling code, as the processing code (see `::apply_filters()`) only needs to be aware of itself, rather than the state of all hooks. At the same time, we're able te handle more complex use cases, as the object can more easily keep track of its own state than an array ever could.

Props jbrinley for the original architecture and design of this patch.
Props SergeyBiryukov, cheeserolls, Denis-de-Bernardy, leewillis77, wonderboymusic, nacin, jorbin, DrewAPicture, ocean90, dougwollison, khag7, pento, noplanman and aaroncampbell for their testing, suggestions, contributions, patch maintenance, cajoling and patience as we got through this.
Fixes #17817.


Built from https://develop.svn.wordpress.org/trunk@38571


git-svn-id: http://core.svn.wordpress.org/trunk@38514 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Gary Pendergast
2016-09-08 03:55:31 +00:00
parent 8bec516c18
commit 5a632be944
3 changed files with 557 additions and 136 deletions

View File

@@ -22,17 +22,20 @@
*/
// Initialize the filter globals.
global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
require( ABSPATH . WPINC . '/class-wp-hook.php' );
if ( ! isset( $wp_filter ) )
/** @var WP_Hook[] $wp_filter */
global $wp_filter, $wp_actions, $wp_current_filter;
if ( $wp_filter ) {
$wp_filter = WP_Hook::build_preinitialized_hooks( $wp_filter );
} else {
$wp_filter = array();
}
if ( ! isset( $wp_actions ) )
$wp_actions = array();
if ( ! isset( $merged_filters ) )
$merged_filters = array();
if ( ! isset( $wp_current_filter ) )
$wp_current_filter = array();
@@ -89,8 +92,6 @@ if ( ! isset( $wp_current_filter ) )
* @since 0.71
*
* @global array $wp_filter A multidimensional array of all hooks and the callbacks hooked to them.
* @global array $merged_filters Tracks the tags that need to be merged for later. If the hook is added,
* it doesn't need to run through that process.
*
* @param string $tag The name of the filter to hook the $function_to_add callback to.
* @param callable $function_to_add The callback to be run when the filter is applied.
@@ -103,11 +104,11 @@ if ( ! isset( $wp_current_filter ) )
* @return true
*/
function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
global $wp_filter, $merged_filters;
$idx = _wp_filter_build_unique_id($tag, $function_to_add, $priority);
$wp_filter[$tag][$priority][$idx] = array('function' => $function_to_add, 'accepted_args' => $accepted_args);
unset( $merged_filters[ $tag ] );
global $wp_filter;
if ( ! isset( $wp_filter[ $tag ] ) ) {
$wp_filter[ $tag ] = new WP_Hook();
}
$wp_filter[ $tag ]->add_filter( $tag, $function_to_add, $priority, $accepted_args );
return true;
}
@@ -128,38 +129,13 @@ function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1
* return value.
*/
function has_filter($tag, $function_to_check = false) {
// Don't reset the internal array pointer
$wp_filter = $GLOBALS['wp_filter'];
global $wp_filter;
$has = ! empty( $wp_filter[ $tag ] );
// Make sure at least one priority has a filter callback
if ( $has ) {
$exists = false;
foreach ( $wp_filter[ $tag ] as $callbacks ) {
if ( ! empty( $callbacks ) ) {
$exists = true;
break;
}
}
if ( ! $exists ) {
$has = false;
}
}
if ( false === $function_to_check || false === $has )
return $has;
if ( !$idx = _wp_filter_build_unique_id($tag, $function_to_check, false) )
if ( ! isset( $wp_filter[ $tag ] ) ) {
return false;
foreach ( (array) array_keys($wp_filter[$tag]) as $priority ) {
if ( isset($wp_filter[$tag][$priority][$idx]) )
return $priority;
}
return false;
return $wp_filter[ $tag ]->has_filter( $tag, $function_to_check );
}
/**
@@ -190,7 +166,6 @@ function has_filter($tag, $function_to_check = false) {
* @since 0.71
*
* @global array $wp_filter Stores all of the filters.
* @global array $merged_filters Merges the filter hooks using this function.
* @global array $wp_current_filter Stores the list of current filters with the current one last.
*
* @param string $tag The name of the filter hook.
@@ -199,7 +174,7 @@ function has_filter($tag, $function_to_check = false) {
* @return mixed The filtered value after all hooked functions are applied to it.
*/
function apply_filters( $tag, $value ) {
global $wp_filter, $merged_filters, $wp_current_filter;
global $wp_filter, $wp_current_filter;
$args = array();
@@ -219,29 +194,17 @@ function apply_filters( $tag, $value ) {
if ( !isset($wp_filter['all']) )
$wp_current_filter[] = $tag;
// Sort.
if ( !isset( $merged_filters[ $tag ] ) ) {
ksort($wp_filter[$tag]);
$merged_filters[ $tag ] = true;
}
reset( $wp_filter[ $tag ] );
if ( empty($args) )
$args = func_get_args();
do {
foreach ( (array) current($wp_filter[$tag]) as $the_ )
if ( !is_null($the_['function']) ){
$args[1] = $value;
$value = call_user_func_array($the_['function'], array_slice($args, 1, (int) $the_['accepted_args']));
}
// don't pass the tag name to WP_Hook
array_shift( $args );
} while ( next($wp_filter[$tag]) !== false );
$filtered = $wp_filter[ $tag ]->apply_filters( $value, $args );
array_pop( $wp_current_filter );
return $value;
return $filtered;
}
/**
@@ -253,7 +216,6 @@ function apply_filters( $tag, $value ) {
* functions hooked to `$tag` are supplied using an array.
*
* @global array $wp_filter Stores all of the filters
* @global array $merged_filters Merges the filter hooks using this function.
* @global array $wp_current_filter Stores the list of current filters with the current one last
*
* @param string $tag The name of the filter hook.
@@ -261,7 +223,7 @@ function apply_filters( $tag, $value ) {
* @return mixed The filtered value after all hooked functions are applied to it.
*/
function apply_filters_ref_array($tag, $args) {
global $wp_filter, $merged_filters, $wp_current_filter;
global $wp_filter, $wp_current_filter;
// Do 'all' actions first
if ( isset($wp_filter['all']) ) {
@@ -279,24 +241,11 @@ function apply_filters_ref_array($tag, $args) {
if ( !isset($wp_filter['all']) )
$wp_current_filter[] = $tag;
// Sort
if ( !isset( $merged_filters[ $tag ] ) ) {
ksort($wp_filter[$tag]);
$merged_filters[ $tag ] = true;
}
reset( $wp_filter[ $tag ] );
do {
foreach ( (array) current($wp_filter[$tag]) as $the_ )
if ( !is_null($the_['function']) )
$args[0] = call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
} while ( next($wp_filter[$tag]) !== false );
$filtered = $wp_filter[ $tag ]->apply_filters( $args[0], $args );
array_pop( $wp_current_filter );
return $args[0];
return $filtered;
}
/**
@@ -313,7 +262,6 @@ function apply_filters_ref_array($tag, $args) {
* @since 1.2.0
*
* @global array $wp_filter Stores all of the filters
* @global array $merged_filters Merges the filter hooks using this function.
*
* @param string $tag The filter hook to which the function to be removed is hooked.
* @param callable $function_to_remove The name of the function which should be removed.
@@ -321,19 +269,14 @@ function apply_filters_ref_array($tag, $args) {
* @return bool Whether the function existed before it was removed.
*/
function remove_filter( $tag, $function_to_remove, $priority = 10 ) {
$function_to_remove = _wp_filter_build_unique_id( $tag, $function_to_remove, $priority );
global $wp_filter;
$r = isset( $GLOBALS['wp_filter'][ $tag ][ $priority ][ $function_to_remove ] );
if ( true === $r ) {
unset( $GLOBALS['wp_filter'][ $tag ][ $priority ][ $function_to_remove ] );
if ( empty( $GLOBALS['wp_filter'][ $tag ][ $priority ] ) ) {
unset( $GLOBALS['wp_filter'][ $tag ][ $priority ] );
$r = false;
if ( isset( $wp_filter[ $tag ] ) ) {
$r = $wp_filter[ $tag ]->remove_filter( $tag, $function_to_remove, $priority );
if ( ! $wp_filter[ $tag ]->callbacks ) {
unset( $wp_filter[ $tag ] );
}
if ( empty( $GLOBALS['wp_filter'][ $tag ] ) ) {
$GLOBALS['wp_filter'][ $tag ] = array();
}
unset( $GLOBALS['merged_filters'][ $tag ] );
}
return $r;
@@ -344,26 +287,22 @@ function remove_filter( $tag, $function_to_remove, $priority = 10 ) {
*
* @since 2.7.0
*
* @global array $wp_filter Stores all of the filters
* @global array $merged_filters Merges the filter hooks using this function.
* @global array $wp_filter Stores all of the filters
*
* @param string $tag The filter to remove hooks from.
* @param int|bool $priority Optional. The priority number to remove. Default false.
* @return true True when finished.
*/
function remove_all_filters( $tag, $priority = false ) {
global $wp_filter, $merged_filters;
global $wp_filter;
if ( isset( $wp_filter[ $tag ]) ) {
if ( false === $priority ) {
$wp_filter[ $tag ] = array();
} elseif ( isset( $wp_filter[ $tag ][ $priority ] ) ) {
$wp_filter[ $tag ][ $priority ] = array();
$wp_filter[ $tag ]->remove_all_filters( $priority );
if ( ! $wp_filter[ $tag ]->has_filters() ) {
unset( $wp_filter[ $tag ] );
}
}
unset( $merged_filters[ $tag ] );
return true;
}
@@ -473,7 +412,6 @@ function add_action($tag, $function_to_add, $priority = 10, $accepted_args = 1)
*
* @global array $wp_filter Stores all of the filters
* @global array $wp_actions Increments the amount of times action was triggered.
* @global array $merged_filters Merges the filter hooks using this function.
* @global array $wp_current_filter Stores the list of current filters with the current one last
*
* @param string $tag The name of the action to be executed.
@@ -481,7 +419,7 @@ function add_action($tag, $function_to_add, $priority = 10, $accepted_args = 1)
* functions hooked to the action. Default empty.
*/
function do_action($tag, $arg = '') {
global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
global $wp_filter, $wp_actions, $wp_current_filter;
if ( ! isset($wp_actions[$tag]) )
$wp_actions[$tag] = 1;
@@ -512,20 +450,7 @@ function do_action($tag, $arg = '') {
for ( $a = 2, $num = func_num_args(); $a < $num; $a++ )
$args[] = func_get_arg($a);
// Sort
if ( !isset( $merged_filters[ $tag ] ) ) {
ksort($wp_filter[$tag]);
$merged_filters[ $tag ] = true;
}
reset( $wp_filter[ $tag ] );
do {
foreach ( (array) current($wp_filter[$tag]) as $the_ )
if ( !is_null($the_['function']) )
call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
} while ( next($wp_filter[$tag]) !== false );
$wp_filter[ $tag ]->do_action( $args );
array_pop($wp_current_filter);
}
@@ -558,14 +483,13 @@ function did_action($tag) {
* functions hooked to $tag< are supplied using an array.
* @global array $wp_filter Stores all of the filters
* @global array $wp_actions Increments the amount of times action was triggered.
* @global array $merged_filters Merges the filter hooks using this function.
* @global array $wp_current_filter Stores the list of current filters with the current one last
*
* @param string $tag The name of the action to be executed.
* @param array $args The arguments supplied to the functions hooked to `$tag`.
*/
function do_action_ref_array($tag, $args) {
global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
global $wp_filter, $wp_actions, $wp_current_filter;
if ( ! isset($wp_actions[$tag]) )
$wp_actions[$tag] = 1;
@@ -588,20 +512,7 @@ function do_action_ref_array($tag, $args) {
if ( !isset($wp_filter['all']) )
$wp_current_filter[] = $tag;
// Sort
if ( !isset( $merged_filters[ $tag ] ) ) {
ksort($wp_filter[$tag]);
$merged_filters[ $tag ] = true;
}
reset( $wp_filter[ $tag ] );
do {
foreach ( (array) current($wp_filter[$tag]) as $the_ )
if ( !is_null($the_['function']) )
call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
} while ( next($wp_filter[$tag]) !== false );
$wp_filter[ $tag ]->do_action( $args );
array_pop($wp_current_filter);
}
@@ -923,13 +834,7 @@ function register_uninstall_hook( $file, $callback ) {
function _wp_call_all_hook($args) {
global $wp_filter;
reset( $wp_filter['all'] );
do {
foreach ( (array) current($wp_filter['all']) as $the_ )
if ( !is_null($the_['function']) )
call_user_func_array($the_['function'], $args);
} while ( next($wp_filter['all']) !== false );
$wp_filter['all']->do_all_hook( $args );
}
/**