Index: src/wp-includes/class-wp-hook.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- src/wp-includes/class-wp-hook.php	(revision )
+++ src/wp-includes/class-wp-hook.php	(revision )
@@ -0,0 +1,125 @@
+<?php
+
+/**
+ * Class WP_Hook
+ *
+ * TODO: This implements SplSubject. Make that explicit
+ *       when we can depend on SPL's presence.
+ */
+class WP_Hook implements IteratorAggregate, Countable {
+	// TODO: When PHP 5.3 is the minimum version, use SplObjectStorage
+	/** @var SplObserver[] */
+	private $observers = array();
+	public $callbacks = array();
+
+	public function add_filter( $function_to_add, $priority, $accepted_args, $tag ) {
+		$idx = _wp_filter_build_unique_id($tag, $function_to_add, $priority);
+		$this->callbacks[$priority][$idx] = array( 'function' => $function_to_add, 'accepted_args' => $accepted_args );
+
+		if ( $this->observers ) {
+			$this->notify();
+		}
+	}
+
+	/**
+	 * @param string $function_key
+	 * @param int $priority
+	 *
+	 * @return bool Whether the callback existed before it was removed
+	 */
+	public function remove_filter( $function_key, $priority ) {
+		$exists = isset($this->callbacks[$priority][$function_key]);
+		if ( $exists ) {
+			unset($this->callbacks[$priority][$function_key]);
+			if ( empty($this->callbacks[$priority]) ) {
+				unset($this->callbacks[$priority]);
+			}
+			if ( $this->observers ) {
+				$this->notify();
+			}
+		}
+		return $exists;
+	}
+
+	/**
+	 * Check if any action has been registered for a hook.
+	 *
+	 * @param string $function_key The hashed index of the filter
+	 * @return mixed The priority of that hook is returned, or false if the function is not attached.
+	 */
+	public function has_filter( $function_key ) {
+		foreach ( $this->callbacks as $priority => &$callbacks ) {
+			if ( isset($callbacks[$function_key]) ) {
+				return $priority;
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * Remove all of the callbacks from the filter.
+	 *
+	 * @param int|bool $priority The priority number to remove.
+	 * @return void
+	 */
+	public function remove_all_filters( $priority = false ) {
+		if ( empty($this->callbacks) ) {
+			return;
+		}
+		if( false !== $priority && isset($this->callbacks[$priority]) ) {
+			unset($this->callbacks[$priority]);
+		} else {
+			$this->callbacks = array();
+		}
+		$this->notify();
+	}
+
+	public function get_iterator() {
+		$iterator = new WP_Hook_Iterator( $this );
+		return $iterator;
+	}
+
+	public function attach( $observer ) {
+		$this->observers[] = $observer;
+	}
+
+	public function detach( $observer ) {
+		foreach ( $this->observers as $key => $o ) {
+			if ( $observer === $o ) {
+				unset($this->observers[$key]);
+			}
+		}
+	}
+
+	public function notify() {
+		foreach ( $this->observers as &$o ) {
+			$o->update($this);
+		}
+	}
+
+	/**
+	 * Retrieve an external iterator
+	 *
+	 * @link http://php.net/manual/en/iteratoraggregate.getiterator.php
+	 * @return Traversable An instance of an object implementing Iterator or
+	 * Traversable
+	 */
+	public function getIterator() {
+		return new ArrayIterator($this->callbacks);
+	}
+
+	/**
+	 * Count elements of an object
+	 *
+	 * @link http://php.net/manual/en/countable.count.php
+	 * @return int The custom count as an integer.
+	 *
+	 * The return value is cast to an integer.
+	 */
+	public function count() {
+		return count($this->callbacks);
+	}
+
+
+}
+ 
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 28372)
+++ tests/phpunit/includes/functions.php	(revision )
@@ -1,37 +1,5 @@
 <?php
 
-// For adding hooks before loading WP
-function tests_add_filter($tag, $function_to_add, $priority = 10, $accepted_args = 1) {
-	global $wp_filter, $merged_filters;
-
-	$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;
-}
-
-function _test_filter_build_unique_id($tag, $function, $priority) {
-	global $wp_filter;
-	static $filter_id_count = 0;
-
-	if ( is_string($function) )
-		return $function;
-
-	if ( is_object($function) ) {
-		// Closures are currently implemented as objects
-		$function = array( $function, '' );
-	} else {
-		$function = (array) $function;
-	}
-
-	if (is_object($function[0]) ) {
-		return spl_object_hash($function[0]) . $function[1];
-	} else if ( is_string($function[0]) ) {
-		// Static Calling
-		return $function[0].$function[1];
-	}
-}
-
 function _delete_all_posts() {
 	global $wpdb;
 
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 28372)
+++ src/wp-includes/plugin.php	(revision )
@@ -20,7 +20,10 @@
  */
 
 // Initialize the filter globals.
-global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
+require( ABSPATH . '/wp-includes/class-wp-hook.php' );
+require( ABSPATH . '/wp-includes/class-wp-hook-iterator.php' );
+/** @var WP_Hook[] $wp_filter */
+global $wp_filter, $wp_actions, $wp_current_filter;
 
 if ( ! isset( $wp_filter ) )
 	$wp_filter = array();
@@ -28,9 +31,6 @@
 if ( ! isset( $wp_actions ) )
 	$wp_actions = array();
 
-if ( ! isset( $merged_filters ) )
-	$merged_filters = array();
-
 if ( ! isset( $wp_current_filter ) )
 	$wp_current_filter = array();
 
@@ -64,7 +64,6 @@
  * so everything is as quick as possible.
  *
  * @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
  *
@@ -77,11 +76,12 @@
  * @return boolean true
  */
 function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
-	global $wp_filter, $merged_filters;
+	global $wp_filter;
+	if ( !isset($wp_filter[$tag]) ) {
+		$wp_filter[$tag] = new WP_Hook();
+	}
+	$wp_filter[$tag]->add_filter( $function_to_add, $priority, $accepted_args, $tag );
 
-	$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;
 }
 
@@ -102,21 +102,16 @@
 function has_filter($tag, $function_to_check = false) {
 	global $wp_filter;
 
-	$has = !empty($wp_filter[$tag]);
+	$has = isset($wp_filter[$tag]) && !empty($wp_filter[$tag]->callbacks);
 	if ( false === $function_to_check || false == $has )
 		return $has;
 
 	if ( !$idx = _wp_filter_build_unique_id($tag, $function_to_check, false) )
 		return false;
 
-	foreach ( (array) array_keys($wp_filter[$tag]) as $priority ) {
-		if ( isset($wp_filter[$tag][$priority][$idx]) )
-			return $priority;
+	return $wp_filter[$tag]->has_filter($idx);
-	}
+}
 
-	return false;
-}
-
 /**
  * Call the functions added to a filter hook.
  *
@@ -143,7 +138,6 @@
  * </code>
  *
  * @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
@@ -154,7 +148,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();
 
@@ -174,26 +168,16 @@
 	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']) ){
+	$iterator = $wp_filter[$tag]->get_iterator();
+	foreach ( $iterator as $the_ ) {
-				$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']));
+	}
+	$iterator->cleanup();
 
-	} while ( next($wp_filter[$tag]) !== false );
-
 	array_pop( $wp_current_filter );
 
 	return $value;
@@ -207,7 +191,6 @@
  *
  * @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.
@@ -215,7 +198,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']) ) {
@@ -233,21 +216,12 @@
 	if ( !isset($wp_filter['all']) )
 		$wp_current_filter[] = $tag;
 
-	// Sort
-	if ( !isset( $merged_filters[ $tag ] ) ) {
-		ksort($wp_filter[$tag]);
-		$merged_filters[ $tag ] = true;
+	$iterator = $wp_filter[$tag]->get_iterator();
+	foreach ( $iterator as $the_ ) {
+		$args[0] = call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
 	}
+	$iterator->cleanup();
 
-	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];
@@ -273,15 +247,12 @@
  * @return boolean 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]);
-		unset($GLOBALS['merged_filters'][$tag]);
+	$r = false;
+	if ( isset($wp_filter[$tag]) ) {
+		$function_to_remove = _wp_filter_build_unique_id($tag, $function_to_remove, $priority);
+		$r = $wp_filter[$tag]->remove_filter($function_to_remove, $priority);
 	}
 
 	return $r;
@@ -297,18 +268,12 @@
  * @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]) )
-			unset($wp_filter[$tag][$priority]);
-		else
-			unset($wp_filter[$tag]);
+		$wp_filter[$tag]->remove_all_filters($priority);
 	}
 
-	if( isset($merged_filters[$tag]) )
-		unset($merged_filters[$tag]);
-
 	return true;
 }
 
@@ -425,7 +390,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;
@@ -456,21 +421,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;
+	$iterator = $wp_filter[$tag]->get_iterator();
+	foreach ( $iterator as $the_ ) {
+		call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
 	}
+	$iterator->cleanup();
 
-	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);
 }
 
@@ -509,7 +465,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;
@@ -532,21 +488,12 @@
 	if ( !isset($wp_filter['all']) )
 		$wp_current_filter[] = $tag;
 
-	// Sort
-	if ( !isset( $merged_filters[ $tag ] ) ) {
-		ksort($wp_filter[$tag]);
-		$merged_filters[ $tag ] = true;
+	$iterator = $wp_filter[$tag]->get_iterator();
+	foreach ( $iterator as $the_ ) {
+		call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
 	}
+	$iterator->cleanup();
 
-	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);
 }
 
@@ -799,14 +746,11 @@
  */
 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 = $wp_filter['all']->get_iterator();
+	foreach ( $iterator as $the_ ) {
-				call_user_func_array($the_['function'], $args);
+		call_user_func_array($the_['function'], $args);
-
-	} while ( next($wp_filter['all']) !== false );
+	}
+	$iterator->cleanup();
 }
 
 /**
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 28372)
+++ tests/phpunit/tests/actions.php	(revision )
@@ -356,4 +356,95 @@
 		$this->assertTrue( doing_filter( 'testing_nested' ) );
 		$this->assertFalse( doing_filter( 'something_else' ) );
 	}
+
+	/**
+	 * @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 );
+	}
+
+	/**
+	 * @ticket 17817
+	 *
+	 * This specificaly addresses the concern raised at
+	 * https://core.trac.wordpress.org/ticket/17817#comment:52
+	 */
+	function test_remove_anonymous_callback() {
+		$tag = rand_str();
+		$a = new MockAction();
+		add_action( $tag, array( $a, 'action' ), 12, 1 );
+		$this->assertTrue( has_action( $tag ) );
+
+		$hook = $GLOBALS['wp_filter'][ $tag ];
+
+		// From http://wordpress.stackexchange.com/a/57088/6445
+		foreach ( $hook as $priority => $filter ) {
+			foreach ( $filter as $identifier => $function ) {
+				if ( is_array( $function)
+					&& is_a( $function['function'][0], 'MockAction' )
+					&& 'action' === $function['function'][1]
+				) {
+					remove_filter(
+						$tag,
+						array ( $function['function'][0], 'action' ),
+						$priority
+					);
+				}
+			}
+		}
+
+		$this->assertFalse( has_action( $tag ) );
+	}
 }
Index: tests/phpunit/includes/bootstrap.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- tests/phpunit/includes/bootstrap.php	(revision 28372)
+++ tests/phpunit/includes/bootstrap.php	(revision )
@@ -67,6 +67,10 @@
 
 require_once dirname( __FILE__ ) . '/functions.php';
 
+// load plugin.php out of order so that we can set up
+// some filters in advance
+require_once ABSPATH . '/wp-includes/plugin.php';
+
 // Preset WordPress options defined in bootstrap file.
 // Used to activate themes, plugins, as well as  other settings.
 if(isset($GLOBALS['wp_tests_options'])) {
@@ -76,7 +80,7 @@
 	}
 
 	foreach ( array_keys( $GLOBALS['wp_tests_options'] ) as $key ) {
-		tests_add_filter( 'pre_option_'.$key, 'wp_tests_options' );
+		add_filter( 'pre_option_'.$key, 'wp_tests_options' );
 	}
 }
 
Index: src/wp-settings.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- src/wp-settings.php	(revision 28372)
+++ src/wp-settings.php	(revision )
@@ -72,7 +72,7 @@
 require( ABSPATH . WPINC . '/functions.php' );
 require( ABSPATH . WPINC . '/class-wp.php' );
 require( ABSPATH . WPINC . '/class-wp-error.php' );
-require( ABSPATH . WPINC . '/plugin.php' );
+require_once( ABSPATH . WPINC . '/plugin.php' );
 require( ABSPATH . WPINC . '/pomo/mo.php' );
 
 // Include the wpdb class and, if present, a db.php database drop-in.
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,173 @@
+<?php
+
+/**
+ * Class WP_Hook_Iterator
+ *
+ * TODO: This implements SplObserver. Make that explicit
+ *       when we can depend on SPL's presence.
+ */
+class WP_Hook_Iterator implements Iterator {
+	/** @var WP_Hook  */
+	private $hook = NULL;
+	private $current_callback = NULL;
+	private $current_callback_index = NULL;
+	private $current_priority = NULL;
+	private $current_priority_index = NULL;
+	private $all_priorities = NULL;
+	private $callbacks_for_current_priority = array();
+
+	public function __construct( WP_Hook $hook ) {
+		$this->hook = $hook;
+		$this->hook->attach($this);
+	}
+
+	public function cleanup() {
+		$this->hook->detach($this);
+	}
+
+	/**
+	 * 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() {
+		$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) );
+
+			if ( !$this->callbacks_for_current_priority ) {
+				$this->current_callback = NULL;
+				$this->current_callback_index = NULL;
+				return;
+			}
+
+			$next = reset( $this->callbacks_for_current_priority );
+		}
+
+		$this->current_callback = $next;
+		$this->current_callback_index = key($this->callbacks_for_current_priority);
+	}
+
+	/**
+	 * Setup the callbacks array for the next available priority
+	 * @return void
+	 */
+	private function increment_priority() {
+		$this->set_current_priority();
+		if ( isset($this->current_priority) ) {
+			$this->callbacks_for_current_priority = $this->get_callbacks($this->current_priority);
+		} else {
+			$this->callbacks_for_current_priority = array();
+		}
+	}
+
+	/**
+	 * Move to the next available priority
+	 * @return void
+	 */
+	private function set_current_priority() {
+		if ( empty($this->hook->callbacks) ) { // no callbacks on this hook
+			$this->all_priorities = array();
+			$this->current_priority = NULL;
+			return;
+		}
+
+		if ( !isset($this->all_priorities) ) {
+			$this->all_priorities = array_keys($this->hook->callbacks);
+			sort($this->all_priorities);
+			if ( isset($this->current_priority) ) { // callbacks were manipulated while we were running
+				foreach ( $this->all_priorities as $index => &$p ) { // go to the next larger priority
+					if ( $p > $this->current_priority ) {
+						$this->current_priority_index = $index;
+						$this->current_priority = $p;
+						break;
+					}
+				}
+			} else { // starting fresh
+				$this->current_priority_index = 0;
+				$this->current_priority = $this->all_priorities[0];
+			}
+			return;
+		}
+
+		if ( !isset($this->current_priority_index) ) {
+			$this->current_priority_index = 0;
+		} else {
+			$this->current_priority_index++;
+		}
+		if ( isset($this->all_priorities[$this->current_priority_index]) ) {
+			$this->current_priority = $this->all_priorities[$this->current_priority_index];
+			return;
+		}
+
+		$this->current_priority = NULL;
+		return;
+	}
+
+	private function get_callbacks( $priority ) {
+		if ( isset($this->hook->callbacks[$priority]) ) {
+			return $this->hook->callbacks[$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() {
+		return $this->current_callback_index;
+	}
+
+	/**
+	 * 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 isset($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_priority_index = NULL;
+		$this->current_callback = NULL;
+		$this->current_callback_index = NULL;
+		$this->callbacks_for_current_priority = array();
+		$this->next();
+	}
+
+	/**
+	 * Receive update from subject
+	 *
+	 * @link http://php.net/manual/en/splobserver.update.php
+	 *
+	 * @param SplSubject $subject The SplSubject notifying the observer of an update.
+	 *
+	 * @return void
+	 */
+	public function update( $subject ) {
+		unset($this->all_priorities);
+		unset($this->current_priority_index);
+	}
+}
