WordPress.org

Make WordPress Core

Ticket #17817: 17817.2.patch

File 17817.2.patch, 15.0 KB (added by jbrinley, 6 years ago)

Implement the WP_Hook_Iterator for action/filter loops

  • src/wp-includes/plugin.php

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
     
    2020 */
    2121
    2222// Initialize the filter globals.
    23 global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
     23global $wp_filter, $wp_actions, $wp_current_filter;
    2424
    2525if ( ! isset( $wp_filter ) )
    2626        $wp_filter = array();
     
    2828if ( ! isset( $wp_actions ) )
    2929        $wp_actions = array();
    3030
    31 if ( ! isset( $merged_filters ) )
    32         $merged_filters = array();
    33 
    3431if ( ! isset( $wp_current_filter ) )
    3532        $wp_current_filter = array();
    3633
     
    6764 * @subpackage Plugin
    6865 *
    6966 * @global array $wp_filter      A multidimensional array of all hooks and the callbacks hooked to them.
    70  * @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.
    7167 *
    7268 * @since 0.71
    7369 *
     
    8076 * @return boolean true
    8177 */
    8278function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
    83         global $wp_filter, $merged_filters;
     79        global $wp_filter;
    8480
    8581        $idx = _wp_filter_build_unique_id($tag, $function_to_add, $priority);
    8682        $wp_filter[$tag][$priority][$idx] = array('function' => $function_to_add, 'accepted_args' => $accepted_args);
    87         unset( $merged_filters[ $tag ] );
    8883        return true;
    8984}
    9085
     
    150145 * @subpackage Plugin
    151146 *
    152147 * @global array $wp_filter         Stores all of the filters
    153  * @global array $merged_filters    Merges the filter hooks using this function.
    154148 * @global array $wp_current_filter stores the list of current filters with the current one last
    155149 *
    156150 * @since 0.71
     
    161155 * @return mixed The filtered value after all hooked functions are applied to it.
    162156 */
    163157function apply_filters( $tag, $value ) {
    164         global $wp_filter, $merged_filters, $wp_current_filter;
     158        global $wp_filter, $wp_current_filter;
    165159
    166160        $args = array();
    167161
     
    181175        if ( !isset($wp_filter['all']) )
    182176                $wp_current_filter[] = $tag;
    183177
    184         // Sort
    185         if ( !isset( $merged_filters[ $tag ] ) ) {
    186                 ksort($wp_filter[$tag]);
    187                 $merged_filters[ $tag ] = true;
    188         }
    189 
    190         reset( $wp_filter[ $tag ] );
    191 
    192178        if ( empty($args) )
    193179                $args = func_get_args();
    194180
    195         do {
    196                 foreach( (array) current($wp_filter[$tag]) as $the_ )
    197                         if ( !is_null($the_['function']) ){
    198                                 $args[1] = $value;
    199                                 $value = call_user_func_array($the_['function'], array_slice($args, 1, (int) $the_['accepted_args']));
    200                         }
     181        $iterator = new WP_Hook_Iterator($tag);
     182        foreach ( $iterator as $the_ ) {
     183                if ( !is_null($the_['function']) ) {
     184                        $args[1] = $value;
     185                        $value = call_user_func_array($the_['function'], array_slice($args, 1, (int) $the_['accepted_args']));
     186                }
     187        }
    201188
    202         } while ( next($wp_filter[$tag]) !== false );
    203 
    204189        array_pop( $wp_current_filter );
    205190
    206191        return $value;
     
    216201 * @subpackage Plugin
    217202 * @since 3.0.0
    218203 * @global array $wp_filter Stores all of the filters
    219  * @global array $merged_filters Merges the filter hooks using this function.
    220204 * @global array $wp_current_filter stores the list of current filters with the current one last
    221205 *
    222206 * @param string $tag The name of the filter hook.
     
    224208 * @return mixed The filtered value after all hooked functions are applied to it.
    225209 */
    226210function apply_filters_ref_array($tag, $args) {
    227         global $wp_filter, $merged_filters, $wp_current_filter;
     211        global $wp_filter, $wp_current_filter;
    228212
    229213        // Do 'all' actions first
    230214        if ( isset($wp_filter['all']) ) {
     
    242226        if ( !isset($wp_filter['all']) )
    243227                $wp_current_filter[] = $tag;
    244228
    245         // Sort
    246         if ( !isset( $merged_filters[ $tag ] ) ) {
    247                 ksort($wp_filter[$tag]);
    248                 $merged_filters[ $tag ] = true;
    249         }
     229        $iterator = new WP_Hook_Iterator($tag);
     230        foreach ( $iterator as $the_ ) {
     231                if ( !is_null($the_['function']) ) {
     232                        $args[0] = call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
     233                }
     234        }
    250235
    251         reset( $wp_filter[ $tag ] );
    252 
    253         do {
    254                 foreach( (array) current($wp_filter[$tag]) as $the_ )
    255                         if ( !is_null($the_['function']) )
    256                                 $args[0] = call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
    257 
    258         } while ( next($wp_filter[$tag]) !== false );
    259 
    260236        array_pop( $wp_current_filter );
    261237
    262238        return $args[0];
     
    308284 * @return bool True when finished.
    309285 */
    310286function remove_all_filters($tag, $priority = false) {
    311         global $wp_filter, $merged_filters;
     287        global $wp_filter;
    312288
    313289        if( isset($wp_filter[$tag]) ) {
    314290                if( false !== $priority && isset($wp_filter[$tag][$priority]) )
     
    317293                        unset($wp_filter[$tag]);
    318294        }
    319295
    320         if( isset($merged_filters[$tag]) )
    321                 unset($merged_filters[$tag]);
    322 
    323296        return true;
    324297}
    325298
     
    384357 * @return null Will return null if $tag does not exist in $wp_filter array
    385358 */
    386359function do_action($tag, $arg = '') {
    387         global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
     360        global $wp_filter, $wp_actions, $wp_current_filter;
    388361
    389362        if ( ! isset($wp_actions[$tag]) )
    390363                $wp_actions[$tag] = 1;
     
    415388        for ( $a = 2; $a < func_num_args(); $a++ )
    416389                $args[] = func_get_arg($a);
    417390
    418         // Sort
    419         if ( !isset( $merged_filters[ $tag ] ) ) {
    420                 ksort($wp_filter[$tag]);
    421                 $merged_filters[ $tag ] = true;
    422         }
    423 
    424         reset( $wp_filter[ $tag ] );
    425 
    426         do {
    427                 foreach ( (array) current($wp_filter[$tag]) as $the_ )
    428                         if ( !is_null($the_['function']) )
    429                                 call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
    430 
    431         } while ( next($wp_filter[$tag]) !== false );
    432 
     391        $iterator = new WP_Hook_Iterator($tag);
     392        foreach ( $iterator as $the_ ) {
     393                if ( !is_null($the_['function']) ) {
     394                        call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
     395                }
     396        }
    433397        array_pop($wp_current_filter);
    434398}
    435399
     
    470434 * @return null Will return null if $tag does not exist in $wp_filter array
    471435 */
    472436function do_action_ref_array($tag, $args) {
    473         global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
     437        global $wp_filter, $wp_actions, $wp_current_filter;
    474438
    475439        if ( ! isset($wp_actions[$tag]) )
    476440                $wp_actions[$tag] = 1;
     
    493457        if ( !isset($wp_filter['all']) )
    494458                $wp_current_filter[] = $tag;
    495459
    496         // Sort
    497         if ( !isset( $merged_filters[ $tag ] ) ) {
    498                 ksort($wp_filter[$tag]);
    499                 $merged_filters[ $tag ] = true;
    500         }
     460        $iterator = new WP_Hook_Iterator($tag);
     461        foreach ( $iterator as $the_ ) {
     462                if ( !is_null($the_['function']) ) {
     463                        call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
     464                }
     465        }
    501466
    502         reset( $wp_filter[ $tag ] );
    503 
    504         do {
    505                 foreach( (array) current($wp_filter[$tag]) as $the_ )
    506                         if ( !is_null($the_['function']) )
    507                                 call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
    508 
    509         } while ( next($wp_filter[$tag]) !== false );
    510 
    511467        array_pop($wp_current_filter);
    512468}
    513469
     
    730686 * @param array $args The collected parameters from the hook that was called.
    731687 */
    732688function _wp_call_all_hook($args) {
    733         global $wp_filter;
    734 
    735         reset( $wp_filter['all'] );
    736         do {
    737                 foreach( (array) current($wp_filter['all']) as $the_ )
    738                         if ( !is_null($the_['function']) )
    739                                 call_user_func_array($the_['function'], $args);
    740 
    741         } while ( next($wp_filter['all']) !== false );
     689        $iterator = new WP_Hook_Iterator('all');
     690        foreach ( $iterator as $the_ ) {
     691                if ( !is_null($the_['function']) ) {
     692                        call_user_func_array($the_['function'], $args);
     693                }
     694        }
    742695}
    743696
    744697/**
  • src/wp-settings.php

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
     
    6565require( ABSPATH . WPINC . '/functions.php' );
    6666require( ABSPATH . WPINC . '/class-wp.php' );
    6767require( ABSPATH . WPINC . '/class-wp-error.php' );
     68require( ABSPATH . WPINC . '/class-wp-hook-iterator.php' );
    6869require( ABSPATH . WPINC . '/plugin.php' );
    6970require( ABSPATH . WPINC . '/pomo/mo.php' );
    7071
  • src/wp-includes/class-wp-hook-iterator.php

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
     
     1<?php
     2
     3/**
     4 * Class WP_Hook_Iterator
     5 */
     6class WP_Hook_Iterator implements Iterator {
     7        private $hook = '';
     8        private $current_callback = NULL;
     9        private $current_priority = NULL;
     10        private $callbacks_for_current_priority = array();
     11
     12        public function __construct( $hook ) {
     13                $this->hook = $hook;
     14                $this->rewind();
     15        }
     16
     17        /**
     18         * Return the current element
     19         *
     20         * @link http://php.net/manual/en/iterator.current.php
     21         * @return mixed Can return any type.
     22         */
     23        public function current() {
     24                return $this->current_callback;
     25        }
     26
     27        /**
     28         * Move forward to next element
     29         * @link http://php.net/manual/en/iterator.next.php
     30         * @return void Any returned value is ignored.
     31         */
     32        public function next() {
     33                $this->current_callback = NULL;
     34                $next = next( $this->callbacks_for_current_priority );
     35
     36                if ( $next === FALSE ) {
     37                        do {
     38                                $this->increment_priority();
     39                        } while ( empty($this->callbacks_for_current_priority) && isset( $this->current_priority) );
     40
     41                        $next = reset( $this->callbacks_for_current_priority );
     42                }
     43
     44                if ( !empty($next) ) {
     45                        $this->current_callback = $next;
     46                }
     47        }
     48
     49        private function increment_priority() {
     50                $this->current_priority = $this->get_next_priority();
     51                if ( isset($this->current_priority) ) {
     52                        $this->callbacks_for_current_priority = $this->get_callbacks($this->current_priority);
     53                } else {
     54                        $this->callbacks_for_current_priority = array();
     55                }
     56        }
     57
     58        private function get_next_priority() {
     59                global $wp_filter;
     60                if ( empty($wp_filter[$this->hook]) ) {
     61                        return NULL;
     62                }
     63
     64                $priorities = array_keys($wp_filter[$this->hook]);
     65
     66                if ( !isset($this->current_priority) ) {
     67                        return min($priorities); // start at the beginning
     68                }
     69
     70                $next = NULL;
     71
     72                // get the next greater priority
     73                // this runs every time so that callbacks can be added at arbitrary times
     74                foreach ( $priorities as $p ) {
     75                        if ( $p > $this->current_priority && ( !isset($next) || $p < $next ) ) {
     76                                $next = $p;
     77                        }
     78                }
     79
     80                return $next;
     81        }
     82
     83        private function get_callbacks( $priority ) {
     84                global $wp_filter;
     85                if ( isset($wp_filter[$this->hook][$priority]) && is_array($wp_filter[$this->hook][$priority]) ) {
     86                        return $wp_filter[$this->hook][$priority];
     87                }
     88                return array();
     89        }
     90
     91        /**
     92         * Return the key of the current element
     93         * @link http://php.net/manual/en/iterator.key.php
     94         * @return mixed scalar on success, or null on failure.
     95         */
     96        public function key() {
     97                if ( empty($this->current_callback) ) {
     98                        return NULL;
     99                }
     100                return _wp_filter_build_unique_id($this->hook, $this->current_callback, $this->current_priority);
     101        }
     102
     103        /**
     104         * Checks if current position is valid
     105         * @link http://php.net/manual/en/iterator.valid.php
     106         * @return boolean The return value will be casted to boolean and then evaluated.
     107         * Returns true on success or false on failure.
     108         */
     109        public function valid() {
     110                return !empty($this->current_callback);
     111        }
     112
     113        /**
     114         * Rewind the Iterator to the first element
     115         * @link http://php.net/manual/en/iterator.rewind.php
     116         * @return void Any returned value is ignored.
     117         */
     118        public function rewind() {
     119                $this->current_priority = NULL;
     120                $this->current_callback = NULL;
     121                $this->callbacks_for_current_priority = array();
     122                $this->next();
     123        }
     124}
  • tests/phpunit/tests/actions.php

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
     
    256256        function action_self_removal() {
    257257                remove_action( 'test_action_self_removal', array( $this, 'action_self_removal' ) );
    258258        }
     259
     260        /**
     261         * @ticket 17817
     262         */
     263        function test_action_recursion() {
     264                $tag = rand_str();
     265                $a = new MockAction();
     266                $b = new MockAction();
     267
     268                add_action( $tag, array($a, 'action'), 11, 1 );
     269                add_action( $tag, array($b, 'action'), 13, 1 );
     270                add_action( $tag, array($this, 'action_that_causes_recursion'), 12, 1 );
     271                do_action( $tag, $tag );
     272
     273                $this->assertEquals( 2, $a->get_call_count(), 'recursive actions should call all callbacks with earlier priority' );
     274                $this->assertEquals( 2, $b->get_call_count(), 'recursive actions should call callbacks with later priority' );
     275        }
     276
     277        function action_that_causes_recursion( $tag ) {
     278                static $recursing = FALSE;
     279                if ( !$recursing ) {
     280                        $recursing = TRUE;
     281                        do_action( $tag, $tag );
     282                }
     283                $recursing = FALSE;
     284        }
     285
     286        /**
     287         * @ticket 9968
     288         */
     289        function test_action_callback_manipulation_while_running() {
     290                $tag = rand_str();
     291                $a = new MockAction();
     292                $b = new MockAction();
     293                $c = new MockAction();
     294                $d = new MockAction();
     295                $e = new MockAction();
     296
     297                add_action( $tag, array($a, 'action'), 11, 2 );
     298                add_action( $tag, array($this, 'action_that_manipulates_a_running_hook'), 12, 2 );
     299                add_action( $tag, array($b, 'action'), 12, 2 );
     300
     301                do_action( $tag, $tag, array($a,$b,$c,$d,$e) );
     302                do_action( $tag, $tag, array($a,$b,$c,$d,$e) );
     303
     304                $this->assertEquals( 2, $a->get_call_count(), 'callbacks should run unless otherwise instructed' );
     305                $this->assertEquals( 1, $b->get_call_count(), 'callback removed by same priority callback should still get called' );
     306                $this->assertEquals( 1, $c->get_call_count(), 'callback added by same priority callback should not get called' );
     307                $this->assertEquals( 2, $d->get_call_count(), 'callback added by earlier priority callback should get called' );
     308                $this->assertEquals( 1, $e->get_call_count(), 'callback added by later priority callback should not get called' );
     309        }
     310
     311        function action_that_manipulates_a_running_hook( $tag, $mocks ) {
     312                remove_action( $tag, array($mocks[1], 'action'), 12, 2 );
     313                add_action( $tag, array($mocks[2], 'action' ), 12, 2 );
     314                add_action( $tag, array($mocks[3], 'action' ), 13, 2 );
     315                add_action( $tag, array($mocks[4], 'action' ), 10, 2 );
     316        }
    259317}
  • tests/phpunit/includes/functions.php

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
     
    22
    33// For adding hooks before loading WP
    44function tests_add_filter($tag, $function_to_add, $priority = 10, $accepted_args = 1) {
    5         global $wp_filter, $merged_filters;
     5        global $wp_filter;
    66
    77        $idx = _test_filter_build_unique_id($tag, $function_to_add, $priority);
    88        $wp_filter[$tag][$priority][$idx] = array('function' => $function_to_add, 'accepted_args' => $accepted_args);
    9         unset( $merged_filters[ $tag ] );
    109        return true;
    1110}
    1211