Make WordPress Core

Changeset 56681


Ignore:
Timestamp:
09/25/2023 04:23:52 PM (3 years ago)
Author:
flixos90
Message:

Options, Meta APIs: Improve logic to avoid unnecessary database writes in update_option().

Prior to this change, a strict comparison between the old and new database value could lead to a false negative, since database values are generally stored as strings. For example, passing an integer to update_option() would almost always result in an update given any existing database value for that option would be that number cast to a string.

This changeset adjusts the logic to perform an intentional "loose-y" comparison by casting the values to strings. Extensive coverage previously added in [56648] provides additional confidence that this does not introduce any backward compatibility issues.

Props mukesh27, costdev, spacedmonkey, joemcgill, flixos90, nacin, atimmer, duck_, boonebgorges.
Fixes #22192.

Location:
trunk
Files:
1 added
2 edited

Legend:

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

    r56595 r56681  
    777777        $value = apply_filters( 'pre_update_option', $value, $option, $old_value );
    778778
     779        /** This filter is documented in wp-includes/option.php */
     780        $default_value = apply_filters( "default_option_{$option}", false, $option, false );
     781
    779782        /*
    780783         * If the new and old values are the same, no need to update.
    781784         *
    782          * Unserialized values will be adequate in most cases. If the unserialized
    783          * data differs, the (maybe) serialized data is checked to avoid
    784          * unnecessary database calls for otherwise identical object instances.
    785          *
    786          * See https://core.trac.wordpress.org/ticket/38903
    787          */
    788         if ( $value === $old_value || maybe_serialize( $value ) === maybe_serialize( $old_value ) ) {
     785         * An exception applies when no value is set in the database, i.e. the old value is the default.
     786         * In that case, the new value should always be added as it may be intentional to store it rather than relying on the default.
     787         *
     788         * See https://core.trac.wordpress.org/ticket/38903 and https://core.trac.wordpress.org/ticket/22192.
     789         */
     790        if ( $old_value !== $default_value && _is_equal_database_value( $old_value, $value ) ) {
    789791                return false;
    790792        }
    791793
    792         /** This filter is documented in wp-includes/option.php */
    793         if ( apply_filters( "default_option_{$option}", false, $option, false ) === $old_value ) {
     794        if ( $old_value === $default_value ) {
     795
    794796                // Default setting for new options is 'yes'.
    795797                if ( null === $autoload ) {
     
    28882890        return $registered[ $option ]['default'];
    28892891}
     2892
     2893/**
     2894 * Determines whether two values will be equal when stored in the database.
     2895 *
     2896 * @since 6.4.0
     2897 * @access private
     2898 *
     2899 * @param mixed $old_value The old value to compare.
     2900 * @param mixed $new_value The new value to compare.
     2901 * @return bool True if the values are equal, false otherwise.
     2902 */
     2903function _is_equal_database_value( $old_value, $new_value ) {
     2904        $values = array(
     2905                'old' => $old_value,
     2906                'new' => $new_value,
     2907        );
     2908
     2909        foreach ( $values as $_key => &$_value ) {
     2910                // Cast scalars or null to a string so type discrepancies don't result in cache misses.
     2911                if ( null === $_value || is_scalar( $_value ) ) {
     2912                        $_value = (string) $_value;
     2913                }
     2914        }
     2915
     2916        if ( $values['old'] === $values['new'] ) {
     2917                return true;
     2918        }
     2919
     2920        /*
     2921         * Unserialized values will be adequate in most cases. If the unserialized
     2922         * data differs, the (maybe) serialized data is checked to avoid
     2923         * unnecessary database calls for otherwise identical object instances.
     2924         *
     2925         * See https://core.trac.wordpress.org/ticket/38903
     2926         */
     2927        return maybe_serialize( $old_value ) === maybe_serialize( $new_value );
     2928}
  • trunk/tests/phpunit/tests/option/option.php

    r56648 r56681  
    525525                );
    526526        }
     527
     528        /**
     529         * Tests that update_option() stores an option that uses
     530         * an unfiltered default value of (bool) false.
     531         *
     532         * @ticket 22192
     533         *
     534         * @covers ::update_option
     535         */
     536        public function test_update_option_should_store_option_with_default_value_false() {
     537                global $wpdb;
     538
     539                $option = 'update_option_default_false';
     540                update_option( $option, false );
     541
     542                $actual = $wpdb->query(
     543                        $wpdb->prepare(
     544                                "SELECT option_name FROM $wpdb->options WHERE option_name = %s LIMIT 1",
     545                                $option
     546                        )
     547                );
     548
     549                $this->assertSame( 1, $actual );
     550        }
     551
     552        /**
     553         * Tests that update_option() stores an option that uses
     554         * a filtered default value.
     555         *
     556         * @ticket 22192
     557         *
     558         * @covers ::update_option
     559         */
     560        public function test_update_option_should_store_option_with_filtered_default_value() {
     561                global $wpdb;
     562
     563                $option        = 'update_option_custom_default';
     564                $default_value = 'default-value';
     565
     566                add_filter(
     567                        "default_option_{$option}",
     568                        static function () use ( $default_value ) {
     569                                return $default_value;
     570                        }
     571                );
     572
     573                update_option( $option, $default_value );
     574
     575                $actual = $wpdb->query(
     576                        $wpdb->prepare(
     577                                "SELECT option_name FROM $wpdb->options WHERE option_name = %s LIMIT 1",
     578                                $option
     579                        )
     580                );
     581
     582                $this->assertSame( 1, $actual );
     583        }
    527584}
Note: See TracChangeset for help on using the changeset viewer.