Make WordPress Core


Ignore:
Timestamp:
09/08/2016 03:54:13 AM (9 years ago)
Author:
pento
Message:

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.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/tests/actions.php

    r38382 r38571  
    113113        $argsvar2 = $a2->get_args();
    114114        $this->assertEquals( array( $val1 ), array_pop( $argsvar2 ) );
     115    }
     116
     117    /**
     118     * Test that multiple callbacks receive the correct number of args even when the number
     119     * is less than, or greater than previous hooks.
     120     *
     121     * @see https://core.trac.wordpress.org/ticket/17817#comment:72
     122     * @ticket 17817
     123     */
     124    function test_action_args_3() {
     125        $a1 = new MockAction();
     126        $a2 = new MockAction();
     127        $a3 = new MockAction();
     128        $tag = rand_str();
     129        $val1 = rand_str();
     130        $val2 = rand_str();
     131
     132        // a1 accepts two arguments, a2 doesn't, a3 accepts two arguments
     133        add_action( $tag, array( &$a1, 'action' ), 10, 2 );
     134        add_action( $tag, array( &$a2, 'action' ) );
     135        add_action( $tag, array( &$a3, 'action' ), 10, 2 );
     136        // call the action with two arguments
     137        do_action( $tag, $val1, $val2 );
     138
     139        $call_count = $a1->get_call_count();
     140        // a1 should be called with both args
     141        $this->assertEquals( 1, $call_count );
     142        $argsvar1 = $a1->get_args();
     143        $this->assertEquals( array( $val1, $val2 ), array_pop( $argsvar1 ) );
     144
     145        // a2 should be called with one only
     146        $this->assertEquals( 1, $a2->get_call_count() );
     147        $argsvar2 = $a2->get_args();
     148        $this->assertEquals( array( $val1 ), array_pop( $argsvar2 ) );
     149
     150        // a3 should be called with both args
     151        $this->assertEquals( 1, $a3->get_call_count() );
     152        $argsvar3 = $a3->get_args();
     153        $this->assertEquals( array( $val1, $val2 ), array_pop( $argsvar3 ) );
    115154    }
    116155
     
    259298
    260299    /**
     300     * @ticket 17817
     301     */
     302    function test_action_recursion() {
     303        $tag = rand_str();
     304        $a = new MockAction();
     305        $b = new MockAction();
     306
     307        add_action( $tag, array( $a, 'action' ), 11, 1 );
     308        add_action( $tag, array( $b, 'action' ), 13, 1 );
     309        add_action( $tag, array( $this, 'action_that_causes_recursion' ), 12, 1 );
     310        do_action( $tag, $tag );
     311
     312        $this->assertEquals( 2, $a->get_call_count(), 'recursive actions should call all callbacks with earlier priority' );
     313        $this->assertEquals( 2, $b->get_call_count(), 'recursive actions should call callbacks with later priority' );
     314    }
     315
     316    function action_that_causes_recursion( $tag ) {
     317        static $recursing = false;
     318        if ( ! $recursing ) {
     319            $recursing = true;
     320            do_action( $tag, $tag );
     321        }
     322        $recursing = false;
     323    }
     324
     325    /**
     326     * @ticket 9968
     327     * @ticket 17817
     328     */
     329    function test_action_callback_manipulation_while_running() {
     330        $tag = rand_str();
     331        $a = new MockAction();
     332        $b = new MockAction();
     333        $c = new MockAction();
     334        $d = new MockAction();
     335        $e = new MockAction();
     336
     337        add_action( $tag, array( $a, 'action' ), 11, 2 );
     338        add_action( $tag, array( $this, 'action_that_manipulates_a_running_hook' ), 12, 2 );
     339        add_action( $tag, array( $b, 'action' ), 12, 2 );
     340
     341        do_action( $tag, $tag, array( $a, $b, $c, $d, $e ) );
     342        do_action( $tag, $tag, array( $a, $b, $c, $d, $e ) );
     343
     344        $this->assertEquals( 2, $a->get_call_count(), 'callbacks should run unless otherwise instructed' );
     345        $this->assertEquals( 1, $b->get_call_count(), 'callback removed by same priority callback should still get called' );
     346        $this->assertEquals( 1, $c->get_call_count(), 'callback added by same priority callback should not get called' );
     347        $this->assertEquals( 2, $d->get_call_count(), 'callback added by earlier priority callback should get called' );
     348        $this->assertEquals( 1, $e->get_call_count(), 'callback added by later priority callback should not get called' );
     349    }
     350
     351    function action_that_manipulates_a_running_hook( $tag, $mocks ) {
     352        remove_action( $tag, array( $mocks[ 1 ], 'action' ), 12, 2 );
     353        add_action( $tag, array( $mocks[ 2 ], 'action' ), 12, 2 );
     354        add_action( $tag, array( $mocks[ 3 ], 'action' ), 13, 2 );
     355        add_action( $tag, array( $mocks[ 4 ], 'action' ), 10, 2 );
     356    }
     357
     358    /**
     359     * @ticket 17817
     360     *
     361     * This specificaly addresses the concern raised at
     362     * https://core.trac.wordpress.org/ticket/17817#comment:52
     363     */
     364    function test_remove_anonymous_callback() {
     365        $tag = rand_str();
     366        $a = new MockAction();
     367        add_action( $tag, array( $a, 'action' ), 12, 1 );
     368        $this->assertTrue( has_action( $tag ) );
     369
     370        $hook = $GLOBALS['wp_filter'][ $tag ];
     371
     372        // From http://wordpress.stackexchange.com/a/57088/6445
     373        foreach ( $hook as $priority => $filter ) {
     374            foreach ( $filter as $identifier => $function ) {
     375                if ( is_array( $function )
     376                    && is_a( $function['function'][ 0 ], 'MockAction' )
     377                    && 'action' === $function['function'][ 1 ]
     378                ) {
     379                    remove_filter(
     380                        $tag,
     381                        array( $function['function'][ 0 ], 'action' ),
     382                        $priority
     383                    );
     384                }
     385            }
     386        }
     387
     388        $this->assertFalse( has_action( $tag ) );
     389    }
     390
     391
     392    /**
     393     * Test the ArrayAccess methods of WP_Hook
     394     *
     395     * @ticket 17817
     396     */
     397    function test_array_access_of_wp_filter_global() {
     398        global $wp_filter;
     399        $tag = rand_str();
     400
     401        add_action( $tag, '__return_null', 11, 1 );
     402
     403        $this->assertTrue( isset( $wp_filter[ $tag ][ 11 ] ) );
     404        $this->assertArrayHasKey( '__return_null', $wp_filter[ $tag ][ 11 ] );
     405
     406        unset( $wp_filter[ $tag ][ 11 ] );
     407        $this->assertFalse( has_action( $tag, '__return_null' ) );
     408
     409        $wp_filter[ $tag ][ 11 ] = array( '__return_null' => array( 'function' => '__return_null', 'accepted_args' => 1 ) );
     410        $this->assertEquals( 11, has_action( $tag, '__return_null' ) );
     411    }
     412
     413    /**
    261414     * Make sure current_action() behaves as current_filter()
    262415     *
Note: See TracChangeset for help on using the changeset viewer.