Make WordPress Core

Changeset 56681


Ignore:
Timestamp:
09/25/2023 04:23:52 PM (8 months 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.