Skip to content

Instantly share code, notes, and snippets.

@jbrinley
Created October 17, 2013 12:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jbrinley/7024059 to your computer and use it in GitHub Desktop.
Save jbrinley/7024059 to your computer and use it in GitHub Desktop.
Refactor WordPress hook iteration
Index: src/wp-includes/plugin.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- src/wp-includes/plugin.php (revision 25823)
+++ src/wp-includes/plugin.php (revision )
@@ -20,7 +20,7 @@
*/
// Initialize the filter globals.
-global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
+global $wp_filter, $wp_actions, $wp_current_filter;
if ( ! isset( $wp_filter ) )
$wp_filter = array();
@@ -28,9 +28,6 @@
if ( ! isset( $wp_actions ) )
$wp_actions = array();
-if ( ! isset( $merged_filters ) )
- $merged_filters = array();
-
if ( ! isset( $wp_current_filter ) )
$wp_current_filter = array();
@@ -67,7 +64,6 @@
* @subpackage Plugin
*
* @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.
*
* @since 0.71
*
@@ -80,11 +76,10 @@
* @return boolean true
*/
function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
- global $wp_filter, $merged_filters;
+ global $wp_filter;
$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 ] );
return true;
}
@@ -150,7 +145,6 @@
* @subpackage Plugin
*
* @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
*
* @since 0.71
@@ -161,7 +155,7 @@
* @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();
@@ -181,26 +175,17 @@
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_ )
+ $iterator = new WP_Hook_Iterator($tag);
+ foreach ( $iterator as $the_ ) {
- if ( !is_null($the_['function']) ){
+ if ( !is_null($the_['function']) ) {
- $args[1] = $value;
- $value = call_user_func_array($the_['function'], array_slice($args, 1, (int) $the_['accepted_args']));
- }
+ $args[1] = $value;
+ $value = call_user_func_array($the_['function'], array_slice($args, 1, (int) $the_['accepted_args']));
+ }
+ }
- } while ( next($wp_filter[$tag]) !== false );
-
array_pop( $wp_current_filter );
return $value;
@@ -216,7 +201,6 @@
* @subpackage Plugin
* @since 3.0.0
* @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.
@@ -224,7 +208,7 @@
* @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']) ) {
@@ -242,21 +226,13 @@
if ( !isset($wp_filter['all']) )
$wp_current_filter[] = $tag;
- // Sort
- if ( !isset( $merged_filters[ $tag ] ) ) {
- ksort($wp_filter[$tag]);
- $merged_filters[ $tag ] = true;
+ $iterator = new WP_Hook_Iterator($tag);
+ foreach ( $iterator as $the_ ) {
+ if ( !is_null($the_['function']) ) {
+ $args[0] = call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
- }
+ }
+ }
- 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 );
-
array_pop( $wp_current_filter );
return $args[0];
@@ -308,7 +284,7 @@
* @return bool 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 && isset($wp_filter[$tag][$priority]) )
@@ -317,9 +293,6 @@
unset($wp_filter[$tag]);
}
- if( isset($merged_filters[$tag]) )
- unset($merged_filters[$tag]);
-
return true;
}
@@ -384,7 +357,7 @@
* @return null Will return null if $tag does not exist in $wp_filter array
*/
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;
@@ -415,21 +388,12 @@
for ( $a = 2; $a < func_num_args(); $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']) )
+ $iterator = new WP_Hook_Iterator($tag);
+ foreach ( $iterator as $the_ ) {
+ if ( !is_null($the_['function']) ) {
- call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
+ call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
-
- } while ( next($wp_filter[$tag]) !== false );
-
+ }
+ }
array_pop($wp_current_filter);
}
@@ -470,7 +434,7 @@
* @return null Will return null if $tag does not exist in $wp_filter array
*/
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;
@@ -493,21 +457,13 @@
if ( !isset($wp_filter['all']) )
$wp_current_filter[] = $tag;
- // Sort
- if ( !isset( $merged_filters[ $tag ] ) ) {
- ksort($wp_filter[$tag]);
- $merged_filters[ $tag ] = true;
+ $iterator = new WP_Hook_Iterator($tag);
+ foreach ( $iterator as $the_ ) {
+ if ( !is_null($the_['function']) ) {
+ call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
- }
+ }
+ }
- 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 );
-
array_pop($wp_current_filter);
}
@@ -730,15 +686,12 @@
* @param array $args The collected parameters from the hook that was called.
*/
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']) )
+ $iterator = new WP_Hook_Iterator('all');
+ foreach ( $iterator as $the_ ) {
+ if ( !is_null($the_['function']) ) {
- call_user_func_array($the_['function'], $args);
+ call_user_func_array($the_['function'], $args);
-
- } while ( next($wp_filter['all']) !== false );
+ }
+ }
}
/**
Index: src/wp-settings.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- src/wp-settings.php (revision 25823)
+++ src/wp-settings.php (revision )
@@ -65,6 +65,7 @@
require( ABSPATH . WPINC . '/functions.php' );
require( ABSPATH . WPINC . '/class-wp.php' );
require( ABSPATH . WPINC . '/class-wp-error.php' );
+require( ABSPATH . WPINC . '/class-wp-hook-iterator.php' );
require( ABSPATH . WPINC . '/plugin.php' );
require( ABSPATH . WPINC . '/pomo/mo.php' );
Index: src/wp-includes/class-wp-hook-iterator.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- src/wp-includes/class-wp-hook-iterator.php (revision )
+++ src/wp-includes/class-wp-hook-iterator.php (revision )
@@ -0,0 +1,124 @@
+<?php
+
+/**
+ * Class WP_Hook_Iterator
+ */
+class WP_Hook_Iterator implements Iterator {
+ private $hook = '';
+ private $current_callback = NULL;
+ private $current_priority = NULL;
+ private $callbacks_for_current_priority = array();
+
+ public function __construct( $hook ) {
+ $this->hook = $hook;
+ $this->rewind();
+ }
+
+ /**
+ * Return the current element
+ *
+ * @link http://php.net/manual/en/iterator.current.php
+ * @return mixed Can return any type.
+ */
+ public function current() {
+ return $this->current_callback;
+ }
+
+ /**
+ * Move forward to next element
+ * @link http://php.net/manual/en/iterator.next.php
+ * @return void Any returned value is ignored.
+ */
+ public function next() {
+ $this->current_callback = NULL;
+ $next = next( $this->callbacks_for_current_priority );
+
+ if ( $next === FALSE ) {
+ do {
+ $this->increment_priority();
+ } while ( empty($this->callbacks_for_current_priority) && isset( $this->current_priority) );
+
+ $next = reset( $this->callbacks_for_current_priority );
+ }
+
+ if ( !empty($next) ) {
+ $this->current_callback = $next;
+ }
+ }
+
+ private function increment_priority() {
+ $this->current_priority = $this->get_next_priority();
+ if ( isset($this->current_priority) ) {
+ $this->callbacks_for_current_priority = $this->get_callbacks($this->current_priority);
+ } else {
+ $this->callbacks_for_current_priority = array();
+ }
+ }
+
+ private function get_next_priority() {
+ global $wp_filter;
+ if ( empty($wp_filter[$this->hook]) ) {
+ return NULL;
+ }
+
+ $priorities = array_keys($wp_filter[$this->hook]);
+
+ if ( !isset($this->current_priority) ) {
+ return min($priorities); // start at the beginning
+ }
+
+ $next = NULL;
+
+ // get the next greater priority
+ // this runs every time so that callbacks can be added at arbitrary times
+ foreach ( $priorities as $p ) {
+ if ( $p > $this->current_priority && ( !isset($next) || $p < $next ) ) {
+ $next = $p;
+ }
+ }
+
+ return $next;
+ }
+
+ private function get_callbacks( $priority ) {
+ global $wp_filter;
+ if ( isset($wp_filter[$this->hook][$priority]) && is_array($wp_filter[$this->hook][$priority]) ) {
+ return $wp_filter[$this->hook][$priority];
+ }
+ return array();
+ }
+
+ /**
+ * Return the key of the current element
+ * @link http://php.net/manual/en/iterator.key.php
+ * @return mixed scalar on success, or null on failure.
+ */
+ public function key() {
+ if ( empty($this->current_callback) ) {
+ return NULL;
+ }
+ return _wp_filter_build_unique_id($this->hook, $this->current_callback, $this->current_priority);
+ }
+
+ /**
+ * Checks if current position is valid
+ * @link http://php.net/manual/en/iterator.valid.php
+ * @return boolean The return value will be casted to boolean and then evaluated.
+ * Returns true on success or false on failure.
+ */
+ public function valid() {
+ return !empty($this->current_callback);
+ }
+
+ /**
+ * Rewind the Iterator to the first element
+ * @link http://php.net/manual/en/iterator.rewind.php
+ * @return void Any returned value is ignored.
+ */
+ public function rewind() {
+ $this->current_priority = NULL;
+ $this->current_callback = NULL;
+ $this->callbacks_for_current_priority = array();
+ $this->next();
+ }
+}
Index: tests/phpunit/tests/actions.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- tests/phpunit/tests/actions.php (revision 25823)
+++ tests/phpunit/tests/actions.php (revision )
@@ -256,4 +256,62 @@
function action_self_removal() {
remove_action( 'test_action_self_removal', array( $this, 'action_self_removal' ) );
}
+
+ /**
+ * @ticket 17817
+ */
+ function test_action_recursion() {
+ $tag = rand_str();
+ $a = new MockAction();
+ $b = new MockAction();
+
+ add_action( $tag, array($a, 'action'), 11, 1 );
+ add_action( $tag, array($b, 'action'), 13, 1 );
+ add_action( $tag, array($this, 'action_that_causes_recursion'), 12, 1 );
+ do_action( $tag, $tag );
+
+ $this->assertEquals( 2, $a->get_call_count(), 'recursive actions should call all callbacks with earlier priority' );
+ $this->assertEquals( 2, $b->get_call_count(), 'recursive actions should call callbacks with later priority' );
+ }
+
+ function action_that_causes_recursion( $tag ) {
+ static $recursing = FALSE;
+ if ( !$recursing ) {
+ $recursing = TRUE;
+ do_action( $tag, $tag );
+ }
+ $recursing = FALSE;
+ }
+
+ /**
+ * @ticket 9968
+ */
+ function test_action_callback_manipulation_while_running() {
+ $tag = rand_str();
+ $a = new MockAction();
+ $b = new MockAction();
+ $c = new MockAction();
+ $d = new MockAction();
+ $e = new MockAction();
+
+ add_action( $tag, array($a, 'action'), 11, 2 );
+ add_action( $tag, array($this, 'action_that_manipulates_a_running_hook'), 12, 2 );
+ add_action( $tag, array($b, 'action'), 12, 2 );
+
+ do_action( $tag, $tag, array($a,$b,$c,$d,$e) );
+ do_action( $tag, $tag, array($a,$b,$c,$d,$e) );
+
+ $this->assertEquals( 2, $a->get_call_count(), 'callbacks should run unless otherwise instructed' );
+ $this->assertEquals( 1, $b->get_call_count(), 'callback removed by same priority callback should still get called' );
+ $this->assertEquals( 1, $c->get_call_count(), 'callback added by same priority callback should not get called' );
+ $this->assertEquals( 2, $d->get_call_count(), 'callback added by earlier priority callback should get called' );
+ $this->assertEquals( 1, $e->get_call_count(), 'callback added by later priority callback should not get called' );
+ }
+
+ function action_that_manipulates_a_running_hook( $tag, $mocks ) {
+ remove_action( $tag, array($mocks[1], 'action'), 12, 2 );
+ add_action( $tag, array($mocks[2], 'action' ), 12, 2 );
+ add_action( $tag, array($mocks[3], 'action' ), 13, 2 );
+ add_action( $tag, array($mocks[4], 'action' ), 10, 2 );
+ }
}
Index: tests/phpunit/includes/functions.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- tests/phpunit/includes/functions.php (revision 25823)
+++ tests/phpunit/includes/functions.php (revision )
@@ -2,11 +2,10 @@
// For adding hooks before loading WP
function tests_add_filter($tag, $function_to_add, $priority = 10, $accepted_args = 1) {
- global $wp_filter, $merged_filters;
+ global $wp_filter;
$idx = _test_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 ] );
return true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment