Make WordPress Core

Changeset 57029 for trunk


Ignore:
Timestamp:
10/30/2023 10:56:25 PM (14 months ago)
Author:
peterwilsoncc
Message:

Options, Meta APIs: Fast follow fixes for option cache priming functions.

A collection of fixes for wp_prime_option_caches():

  • cache arrays and objects in their serialized form for consistency with get_option() and wp_load_alloptions()
  • prevent repeat database queries for falsey and known non-existent options (notoptions)

Additional tests for wp_prime_option_caches() to ensure:

  • additional database queries are not made repriming options (known, known-unknown and alloptions)
  • cache is primed consistently
  • get_option() returns a consistent value regardless of how it is primed
  • database queries do not contain earlier primed options
  • get_option does not prime the cache when testing the cache has been successfully primed

Fixes a test for wp_prime_option_caches_by_group() to ensure get_option does not prime the cache when testing the cache has been successfully primed.

Follow up to [56445],[56990],[57013].

Props peterwilsoncc, costdev, flixos90, hellofromTonya, mikeschroder, joemcgill.
Fixes #59738. See #58962.

Location:
trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/option.php

    r57013 r57029  
    262262    $alloptions     = wp_load_alloptions();
    263263    $cached_options = wp_cache_get_multiple( $options, 'options' );
     264    $notoptions     = wp_cache_get( 'notoptions', 'options' );
     265    if ( ! is_array( $notoptions ) ) {
     266        $notoptions = array();
     267    }
    264268
    265269    // Filter options that are not in the cache.
    266270    $options_to_prime = array();
    267271    foreach ( $options as $option ) {
    268         if ( ( ! isset( $cached_options[ $option ] ) || ! $cached_options[ $option ] ) && ! isset( $alloptions[ $option ] ) ) {
     272        if (
     273            ( ! isset( $cached_options[ $option ] ) || false === $cached_options[ $option ] )
     274            && ! isset( $alloptions[ $option ] )
     275            && ! isset( $notoptions[ $option ] )
     276        ) {
    269277            $options_to_prime[] = $option;
    270278        }
     
    289297    $options_found = array();
    290298    foreach ( $results as $result ) {
    291         $options_found[ $result->option_name ] = maybe_unserialize( $result->option_value );
     299        /*
     300         * The cache is primed with the raw value (i.e. not maybe_unserialized).
     301         *
     302         * `get_option()` will handle unserializing the value as needed.
     303         */
     304        $options_found[ $result->option_name ] = $result->option_value;
    292305    }
    293306    wp_cache_set_multiple( $options_found, 'options' );
     
    299312
    300313    $options_not_found = array_diff( $options_to_prime, array_keys( $options_found ) );
    301 
    302     $notoptions = wp_cache_get( 'notoptions', 'options' );
    303 
    304     if ( ! is_array( $notoptions ) ) {
    305         $notoptions = array();
    306     }
    307314
    308315    // Add the options that were not found to the cache.
  • trunk/tests/phpunit/tests/option/wpPrimeOptionCaches.php

    r57013 r57029  
    4242        foreach ( $options_to_prime as $option ) {
    4343            $this->assertSame(
     44                "value_$option",
    4445                wp_cache_get( $option, 'options' ),
    45                 get_option( $option ),
    4646                "$option was not primed in the 'options' cache group."
    4747            );
    4848
    49             $this->assertFalse(
    50                 wp_cache_get( $option, 'notoptions' ),
    51                 get_option( $option ),
    52                 "$option was primed in the 'notoptions' cache group."
     49            $new_notoptions = wp_cache_get( $option, 'notoptions' );
     50            if ( ! is_array( $new_notoptions ) ) {
     51                $new_notoptions = array();
     52            }
     53            $this->assertArrayNotHasKey(
     54                $option,
     55                $new_notoptions,
     56                "$option was primed in the 'notoptions' cache."
    5357            );
    5458        }
     
    6367
    6468    /**
     69     * Tests that wp_prime_option_caches() handles a mix of primed and unprimed options.
     70     *
     71     * @ticket 58962
     72     */
     73    public function test_wp_prime_option_caches_handles_a_mix_of_primed_and_unprimed_options() {
     74        global $wpdb;
     75        // Create some options to prime.
     76        $options_to_prime = array(
     77            'option1',
     78            'option2',
     79            'option3',
     80        );
     81
     82        /*
     83         * Set values for the options,
     84         * clear the cache for the options,
     85         * check options are not in cache initially.
     86         */
     87        foreach ( $options_to_prime as $option ) {
     88            update_option( $option, "value_$option", false );
     89            wp_cache_delete( $option, 'options' );
     90            $this->assertFalse( wp_cache_get( $option, 'options' ), "$option was not deleted from the cache." );
     91        }
     92
     93        // Add non-existent option to the options to prime.
     94        $options_to_prime[] = 'option404notfound';
     95
     96        // Prime the first option with a non-existent option.
     97        wp_prime_option_caches( array( 'option1', 'option404notfound' ) );
     98
     99        // Store the initial database query count.
     100        $initial_query_count = get_num_queries();
     101
     102        // Prime all the options, including the pre-primed option.
     103        wp_prime_option_caches( $options_to_prime );
     104
     105        // Ensure an additional database query was made.
     106        $this->assertSame(
     107            1,
     108            get_num_queries() - $initial_query_count,
     109            'Additional database queries were not made.'
     110        );
     111
     112        // Ensure the last query does not contain the pre-primed option.
     113        $this->assertStringNotContainsString(
     114            "\'option1\'",
     115            $wpdb->last_query,
     116            'The last query should not contain the pre-primed option.'
     117        );
     118
     119        // Ensure the last query does not contain the pre-primed notoption.
     120        $this->assertStringNotContainsString(
     121            "\'option404notfound\'",
     122            $wpdb->last_query,
     123            'The last query should not contain the pre-primed non-existent option.'
     124        );
     125    }
     126
     127    /**
    65128     * Tests wp_prime_option_caches() with options that do not exist in the database.
    66129     *
    67130     * @ticket 58962
     131     * @ticket 59738
    68132     */
    69133    public function test_wp_prime_option_caches_with_nonexistent_options() {
     
    97161            $this->assertArrayHasKey( $option, $new_notoptions, "$option was not added to the notoptions cache." );
    98162        }
     163
     164        // Check getting and re-priming the options does not result in additional database queries.
     165        $initial_query_count = get_num_queries();
     166        foreach ( $options_to_prime as $option ) {
     167            get_option( $option );
     168            $this->assertSame(
     169                0,
     170                get_num_queries() - $initial_query_count,
     171                "Additional database queries were made getting option $option."
     172            );
     173        }
     174
     175        wp_prime_option_caches( $options_to_prime );
     176        $this->assertSame(
     177            0,
     178            get_num_queries() - $initial_query_count,
     179            'Additional database queries were made re-priming the options.'
     180        );
    99181    }
    100182
     
    103185     *
    104186     * @ticket 58962
     187     * @ticket 59738
    105188     */
    106189    public function test_wp_prime_option_caches_with_empty_array() {
     
    108191        $notoptions = wp_cache_get( 'notoptions', 'options' );
    109192
     193        $initial_query_count = get_num_queries();
    110194        wp_prime_option_caches( array() );
    111195
    112196        $this->assertSame( $alloptions, wp_cache_get( 'alloptions', 'options' ), 'The alloptions cache was modified.' );
    113197        $this->assertSame( $notoptions, wp_cache_get( 'notoptions', 'options' ), 'The notoptions cache was modified.' );
     198
     199        // Check priming an empty array does not result in additional database queries.
     200        $this->assertSame(
     201            0,
     202            get_num_queries() - $initial_query_count,
     203            'Additional database queries were made.'
     204        );
    114205    }
    115206
     
    118209     *
    119210     * @ticket 58962
     211     * @ticket 59738
    120212     */
    121213    public function test_wp_prime_option_caches_handles_empty_notoptions_cache() {
     
    127219        $this->assertIsArray( $notoptions, 'The notoptions cache should be an array.' );
    128220        $this->assertArrayHasKey( 'nonexistent_option', $notoptions, 'nonexistent_option was not added to notoptions.' );
     221
     222        // Check getting and re-priming the options does not result in additional database queries.
     223        $initial_query_count = get_num_queries();
     224
     225        get_option( 'nonexistent_option' );
     226        $this->assertSame(
     227            0,
     228            get_num_queries() - $initial_query_count,
     229            'Additional database queries were made getting nonexistent_option.'
     230        );
     231
     232        wp_prime_option_caches( array( 'nonexistent_option' ) );
     233        $this->assertSame(
     234            0,
     235            get_num_queries() - $initial_query_count,
     236            'Additional database queries were made.'
     237        );
     238    }
     239
     240    /**
     241     * Test options primed by the wp_prime_option_caches() function are identical to those primed by get_option().
     242     *
     243     * @ticket 59738
     244     *
     245     * @dataProvider data_option_types
     246     *
     247     * @param mixed $option_value An option value.
     248     */
     249    public function test_get_option_should_return_identical_value_when_pre_primed_by_wp_prime_option_caches( $option_value ) {
     250        // As this includes a test setting the value to `(bool) false`, update_option() can not be used so add_option() is used instead.
     251        add_option( 'type_of_option', $option_value, '', false );
     252        wp_cache_delete( 'type_of_option', 'options' );
     253
     254        $this->assertFalse( wp_cache_get( 'type_of_option', 'options' ), 'type_of_option was not deleted from the cache for priming.' );
     255
     256        // Call the wp_prime_option_caches function to prime the options.
     257        wp_prime_option_caches( array( 'type_of_option' ) );
     258        $value_after_pre_priming = get_option( 'type_of_option' );
     259
     260        // Clear the cache and call get_option directly.
     261        wp_cache_delete( 'type_of_option', 'options' );
     262        $this->assertFalse( wp_cache_get( 'type_of_option', 'options' ), 'type_of_option was not deleted from the cache for get_option.' );
     263        $value_after_get_option = get_option( 'type_of_option' );
     264
     265        /*
     266         * If the option value is an object, use assertEquals() to compare the values.
     267         *
     268         * This is to compare the shape of the object rather than the identity of the object.
     269         */
     270        if ( is_object( $option_value ) ) {
     271            $this->assertEquals( $value_after_get_option, $value_after_pre_priming, 'The values should be equal.' );
     272        } else {
     273            $this->assertSame( $value_after_get_option, $value_after_pre_priming, 'The values should be identical.' );
     274        }
     275    }
     276
     277    /**
     278     * Tests that wp_prime_option_caches() shapes the cache in the same fashion as get_option()
     279     *
     280     * @ticket 59738
     281     *
     282     * @dataProvider data_option_types
     283     *
     284     * @param mixed $option_value An option value.
     285     */
     286    public function test_wp_prime_option_caches_cache_should_be_identical_to_get_option_cache( $option_value ) {
     287        // As this includes a test setting the value to `(bool) false`, update_option() can not be used so add_option() is used instead.
     288        add_option( 'type_of_option', $option_value, '', false );
     289        wp_cache_delete( 'type_of_option', 'options' );
     290
     291        $this->assertFalse( wp_cache_get( 'type_of_option', 'options' ), 'type_of_option was not deleted from the cache for wp_prime_option_caches().' );
     292
     293        // Call the wp_prime_option_caches function to prime the options.
     294        wp_prime_option_caches( array( 'type_of_option' ) );
     295        $value_from_priming = wp_cache_get( 'type_of_option', 'options' );
     296
     297        wp_cache_delete( 'type_of_option', 'options' );
     298        $this->assertFalse( wp_cache_get( 'type_of_option', 'options' ), 'type_of_option was not deleted from the cache for get_option().' );
     299
     300        // Call get_option() to prime the options.
     301        get_option( 'type_of_option' );
     302        $value_from_get_option = wp_cache_get( 'type_of_option', 'options' );
     303
     304        $this->assertIsString( $value_from_get_option, 'Cache from get_option() should always be a string' );
     305        $this->assertIsString( $value_from_priming, 'Cache from wp_prime_option_caches() should always be a string' );
     306        $this->assertSame( $value_from_get_option, $value_from_priming, 'The values should be identical.' );
     307    }
     308
     309    /**
     310     * Tests that wp_prime_option_caches() doesn't trigger DB queries on already primed options.
     311     *
     312     * @ticket 59738
     313     *
     314     * @dataProvider data_option_types
     315     *
     316     * @param mixed $option_value An option value.
     317     */
     318    public function test_wp_prime_option_caches_does_not_trigger_db_queries_repriming_options( $option_value ) {
     319        // As this includes a test setting the value to `(bool) false`, update_option() can not be used so add_option() is used instead.
     320        add_option( 'double_primed_option', $option_value, '', false );
     321        wp_cache_delete( 'double_primed_option', 'options' );
     322        $options_to_prime = array( 'double_primed_option' );
     323
     324        $this->assertFalse( wp_cache_get( 'double_primed_option', 'options' ), 'double_primed_option was not deleted from the cache.' );
     325
     326        // Call the wp_prime_option_caches function to prime the options.
     327        wp_prime_option_caches( $options_to_prime );
     328
     329        // Store the initial database query count.
     330        $initial_query_count = get_num_queries();
     331
     332        // Check that options are only in the 'options' cache group.
     333        foreach ( $options_to_prime as $option ) {
     334            $this->assertNotFalse(
     335                wp_cache_get( $option, 'options' ),
     336                "$option was not primed in the 'options' cache group."
     337            );
     338
     339            $new_notoptions = wp_cache_get( $option, 'notoptions' );
     340            if ( ! is_array( $new_notoptions ) ) {
     341                $new_notoptions = array();
     342            }
     343            $this->assertArrayNotHasKey(
     344                $option,
     345                $new_notoptions,
     346                "$option was primed in the 'notoptions' cache."
     347            );
     348        }
     349
     350        // Call the wp_prime_option_caches function to prime the options.
     351        wp_prime_option_caches( $options_to_prime );
     352
     353        // Ensure no additional database queries were made.
     354        $this->assertSame(
     355            $initial_query_count,
     356            get_num_queries(),
     357            'Additional database queries were made.'
     358        );
     359    }
     360
     361    /**
     362     * Tests that wp_prime_option_caches() doesn't trigger DB queries for items primed in alloptions.
     363     *
     364     * @ticket 59738
     365     *
     366     * @dataProvider data_option_types
     367     *
     368     * @param mixed $option_value An option value.
     369     */
     370    public function test_wp_prime_option_caches_does_not_trigger_db_queries_for_alloptions( $option_value ) {
     371        // As this includes a test setting the value to `(bool) false`, update_option() can not be used so add_option() is used instead.
     372        add_option( 'option_in_alloptions', $option_value, '', true );
     373        wp_cache_delete( 'alloptions', 'options' );
     374        wp_cache_delete( 'option_in_alloptions', 'options' );
     375        $options_to_prime = array( 'option_in_alloptions' );
     376
     377        $this->assertFalse( wp_cache_get( 'option_in_alloptions', 'options' ), 'option_in_alloptions was not deleted from the cache.' );
     378        $this->assertFalse( wp_cache_get( 'alloptions', 'options' ), 'alloptions was not deleted from the cache.' );
     379
     380        // Prime the alloptions cache.
     381        wp_load_alloptions();
     382
     383        // Store the initial database query count.
     384        $initial_query_count = get_num_queries();
     385
     386        // Call the wp_prime_option_caches function to reprime the option.
     387        wp_prime_option_caches( $options_to_prime );
     388
     389        // Check that options are in the 'alloptions' cache only.
     390        foreach ( $options_to_prime as $option ) {
     391            $this->assertFalse(
     392                wp_cache_get( $option, 'options' ),
     393                "$option was primed in the 'options' cache group."
     394            );
     395
     396            $new_notoptions = wp_cache_get( $option, 'notoptions' );
     397            if ( ! is_array( $new_notoptions ) ) {
     398                $new_notoptions = array();
     399            }
     400            $this->assertArrayNotHasKey(
     401                $option,
     402                $new_notoptions,
     403                "$option was primed in the 'notoptions' cache."
     404            );
     405
     406            $new_alloptions = wp_cache_get( 'alloptions', 'options' );
     407            if ( ! is_array( $new_alloptions ) ) {
     408                $new_alloptions = array();
     409            }
     410            $this->assertArrayHasKey(
     411                $option,
     412                $new_alloptions,
     413                "$option was not primed in the 'alloptions' cache."
     414            );
     415        }
     416
     417        // Ensure no additional database queries were made.
     418        $this->assertSame(
     419            0,
     420            get_num_queries() - $initial_query_count,
     421            'Additional database queries were made.'
     422        );
     423    }
     424
     425    /**
     426     * Data provider.
     427     *
     428     * @return array[]
     429     */
     430    public function data_option_types() {
     431        return array(
     432            'null'                              => array( null ),
     433            '(bool) false'                      => array( false ),
     434            '(bool) true'                       => array( true ),
     435            '(int) 0'                           => array( 0 ),
     436            '(int) -0'                          => array( -0 ),
     437            '(int) 1'                           => array( 1 ),
     438            '(int) -1'                          => array( -1 ),
     439            '(float) 0.0'                       => array( 0.0 ),
     440            '(float) -0.0'                      => array( -0.0 ),
     441            '(float) 1.0'                       => array( 1.0 ),
     442            'empty string'                      => array( '' ),
     443            'string with only tabs'             => array( "\t\t" ),
     444            'string with only newlines'         => array( "\n\n" ),
     445            'string with only carriage returns' => array( "\r\r" ),
     446            'string with only spaces'           => array( '   ' ),
     447            'populated string'                  => array( 'string' ),
     448            'string (1)'                        => array( '1' ),
     449            'string (0)'                        => array( '0' ),
     450            'string (0.0)'                      => array( '0.0' ),
     451            'string (-0)'                       => array( '-0' ),
     452            'string (-0.0)'                     => array( '-0.0' ),
     453            'empty array'                       => array( array() ),
     454            'populated array'                   => array( array( 'string' ) ),
     455            'empty object'                      => array( new stdClass() ),
     456            'populated object'                  => array( (object) array( 'string' ) ),
     457            'INF'                               => array( INF ),
     458            'NAN'                               => array( NAN ),
     459        );
    129460    }
    130461}
  • trunk/tests/phpunit/tests/option/wpPrimeOptionCachesByGroup.php

    r57013 r57029  
    4848        wp_prime_option_caches_by_group( 'group1' );
    4949
    50         // Check that options are now in the cache.
    51         $this->assertSame( get_option( 'option1' ), wp_cache_get( 'option1', 'options' ), 'option1\'s cache was not primed.' );
    52         $this->assertSame( get_option( 'option2' ), wp_cache_get( 'option2', 'options' ), 'option2\'s cache was not primed.' );
     50        /*
     51         * Check that options are now in the cache.
     52         *
     53         * Repeat the string here rather than using get_option as get_option
     54         * will prime the cache before the call to wp_cache_get if the option
     55         * is not in the cache. Thus causing the tests to pass when they should
     56         * fail.
     57         */
     58        $this->assertSame( 'value_option1', wp_cache_get( 'option1', 'options' ), 'option1\'s cache was not primed.' );
     59        $this->assertSame( 'value_option2', wp_cache_get( 'option2', 'options' ), 'option2\'s cache was not primed.' );
    5360
    5461        // Make sure option3 is still not in cache.
Note: See TracChangeset for help on using the changeset viewer.