WordPress.org

Make WordPress Core

Ticket #32103: 32103.wip3.diff

File 32103.wip3.diff, 30.5 KB (added by westonruter, 5 years ago)

Fixed unit tests

  • src/wp-includes/class-wp-customize-manager.php

    diff --git src/wp-includes/class-wp-customize-manager.php src/wp-includes/class-wp-customize-manager.php
    index 1505b60..e07bb8a 100644
    final class WP_Customize_Manager { 
    899899                do_action( 'customize_save', $this );
    900900
    901901                foreach ( $this->settings as $setting ) {
     902                        /*
     903                         * Note that aggregated multidimensional settings will only be
     904                         * prepared for saving with this call. They will actually be saved
     905                         * in the finalize_multidimensional_update() call below.
     906                         */
    902907                        $setting->save();
    903908                }
     909                foreach ( $this->settings as $setting ) {
     910                        $setting->finalize_multidimensional_update();
     911                }
    904912
    905913                /**
    906914                 * Fires after Customize settings have been saved.
  • src/wp-includes/class-wp-customize-setting.php

    diff --git src/wp-includes/class-wp-customize-setting.php src/wp-includes/class-wp-customize-setting.php
    index 6630157..9469f28 100644
    class WP_Customize_Setting { 
    8282        protected $id_data = array();
    8383
    8484        /**
     85         * Cache of multidimensional values to improve performance.
     86         *
     87         * @since 4.4.0
     88         * @access protected
     89         * @var array
     90         * @static
     91         */
     92        protected static $aggregated_multidimensionals = array();
     93
     94        /**
     95         * Whether the multidimensional setting is aggregated.
     96         *
     97         * @todo Do we need this? We can look at $aggregated_multidimensionals anyway
     98         * @since 4.4.0
     99         * @access protected
     100         * @var bool
     101         */
     102        protected $is_multidimensional_aggregated = false;
     103
     104        /**
    85105         * Constructor.
    86106         *
    87107         * Any supplied $args override class property defaults.
    class WP_Customize_Setting { 
    96116        public function __construct( $manager, $id, $args = array() ) {
    97117                $keys = array_keys( get_object_vars( $this ) );
    98118                foreach ( $keys as $key ) {
    99                         if ( isset( $args[ $key ] ) )
     119                        if ( isset( $args[ $key ] ) ) {
    100120                                $this->$key = $args[ $key ];
     121                        }
    101122                }
    102123
    103124                $this->manager = $manager;
    104125                $this->id = $id;
    105126
    106127                // Parse the ID for array keys.
    107                 $this->id_data[ 'keys' ] = preg_split( '/\[/', str_replace( ']', '', $this->id ) );
    108                 $this->id_data[ 'base' ] = array_shift( $this->id_data[ 'keys' ] );
     128                $this->id_data['keys'] = preg_split( '/\[/', str_replace( ']', '', $this->id ) );
     129                $this->id_data['base'] = array_shift( $this->id_data['keys'] );
    109130
    110131                // Rebuild the ID.
    111132                $this->id = $this->id_data[ 'base' ];
    112                 if ( ! empty( $this->id_data[ 'keys' ] ) )
    113                         $this->id .= '[' . implode( '][', $this->id_data[ 'keys' ] ) . ']';
     133                if ( ! empty( $this->id_data[ 'keys' ] ) ) {
     134                        $this->id .= '[' . implode( '][', $this->id_data['keys'] ) . ']';
     135                }
    114136
    115                 if ( $this->sanitize_callback )
     137                if ( $this->sanitize_callback ) {
    116138                        add_filter( "customize_sanitize_{$this->id}", $this->sanitize_callback, 10, 2 );
    117 
    118                 if ( $this->sanitize_js_callback )
     139                }
     140                if ( $this->sanitize_js_callback ) {
    119141                        add_filter( "customize_sanitize_js_{$this->id}", $this->sanitize_js_callback, 10, 2 );
     142                }
     143
     144                if ( 'option' === $this->type || 'theme_mod' === $this->type ) {
     145                        // Other setting types can opt-in to aggregate multidimensional explicitly.
     146                        $this->aggregate_multidimensional();
     147                }
     148        }
     149
     150        /**
     151         * Get parsed ID data for multidimensional setting.
     152         *
     153         * @since 4.4.0
     154         * @access public
     155         *
     156         * @return array {
     157         *     ID data for multidimensional setting.
     158         *
     159         *     @type string $base ID base
     160         *     @type array  $keys Keys for multidimensional array.
     161         * }
     162         */
     163        final public function id_data() {
     164                return $this->id_data;
     165        }
     166
     167        /**
     168         * Set up the setting for aggregated multidimensional values.
     169         *
     170         * When a multidimensional setting gets aggregated, all of its preview and update
     171         * calls get combined into one call, greatly improving performance.
     172         *
     173         * @since 4.4.0
     174         * @access protected
     175         */
     176        protected function aggregate_multidimensional() {
     177                if ( empty( $this->id_data['keys'] ) ) {
     178                        return;
     179                }
     180
     181                $id_base = $this->id_data['base'];
     182                if ( ! isset( self::$aggregated_multidimensionals[ $this->type ] ) ) {
     183                        self::$aggregated_multidimensionals[ $this->type ] = array();
     184                }
     185                if ( ! isset( self::$aggregated_multidimensionals[ $this->type ][ $id_base ] ) ) {
     186                        self::$aggregated_multidimensionals[ $this->type ][ $id_base ] = array(
     187                                'previewed_instances'       => array(), // Calling preview() will add the $setting to the array.
     188                                'preview_applied_instances' => array(), // Flags for which settings have had their values applied.
     189                                'updated_instances'         => array(), // Keep track of which settings have had update() called; will determine whether
     190                                'root_value'                => $this->get_root_value( array() ), // Root value for initial state, manipulated by preview and update calls.
     191                        );
     192                }
     193                $this->is_multidimensional_aggregated = true;
    120194        }
    121195
    122196        /**
    class WP_Customize_Setting { 
    153227        protected $_original_value;
    154228
    155229        /**
    156          * Set up filters for the setting so that the preview request
    157          * will render the drafted changes.
     230         * Add filters to supply the setting's value when accessed.
     231         *
     232         * If the setting already has a pre-existing value and there is no incoming
     233         * post value for the setting, then this method will short-circuit since
     234         * there is no change to preview.
    158235         *
    159236         * @since 3.4.0
     237         * @since 4.4.0 Added boolean return value.
     238         * @access public
     239         *
     240         * @return bool False when preview short-circuits due no change needing to be previewed.
    160241         */
    161242        public function preview() {
    162                 if ( ! isset( $this->_original_value ) ) {
    163                         $this->_original_value = $this->value();
    164                 }
    165243                if ( ! isset( $this->_previewed_blog_id ) ) {
    166244                        $this->_previewed_blog_id = get_current_blog_id();
    167245                }
     246                $id_base = $this->id_data['base'];
     247                $is_multidimensional = ! empty( $this->id_data['keys'] );
     248                $multidimensional_filter = array( $this, '_multidimensional_preview_filter' );
    168249
    169                 switch( $this->type ) {
     250                /*
     251                 * Check if the setting has a pre-existing value (an isset check),
     252                 * and if doesn't have any incoming post value. If both checks are true,
     253                 * then the preview short-circuits because there is nothing that needs
     254                 * to be previewed.
     255                 */
     256                $undefined = new stdClass();
     257                $needs_preview = ( $undefined !== $this->post_value( $undefined ) );
     258                $value = null;
     259
     260                // Since no post value was defined, check if we have an initial value set.
     261                if ( ! $needs_preview ) {
     262                        if ( $this->is_multidimensional_aggregated ) {
     263                                $root = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
     264                                $value = $this->multidimensional_get( $root, $this->id_data['keys'], $undefined );
     265                        } else {
     266                                $default = $this->default;
     267                                $this->default = $undefined; // Temporarily set default to undefined so we can detect if existing value is set.
     268                                $value = $this->value();
     269                                $this->default = $default;
     270                        }
     271                        $needs_preview = ( $undefined === $value ); // Because the default needs to be supplied.
     272                }
     273
     274                if ( ! $needs_preview ) {
     275                        return false;
     276                }
     277
     278                switch ( $this->type ) {
    170279                        case 'theme_mod' :
    171                                 add_filter( 'theme_mod_' . $this->id_data[ 'base' ], array( $this, '_preview_filter' ) );
     280                                if ( ! $is_multidimensional ) {
     281                                        add_filter( "theme_mod_{$id_base}", array( $this, '_preview_filter' ) );
     282                                } else {
     283                                        if ( empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] ) ) {
     284                                                // Only add this filter once for this ID base.
     285                                                add_filter( "theme_mod_{$id_base}", $multidimensional_filter );
     286                                        }
     287                                        self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'][ $this->id ] = $this;
     288                                }
    172289                                break;
    173290                        case 'option' :
    174                                 if ( empty( $this->id_data[ 'keys' ] ) )
    175                                         add_filter( 'pre_option_' . $this->id_data[ 'base' ], array( $this, '_preview_filter' ) );
    176                                 else {
    177                                         add_filter( 'option_' . $this->id_data[ 'base' ], array( $this, '_preview_filter' ) );
    178                                         add_filter( 'default_option_' . $this->id_data[ 'base' ], array( $this, '_preview_filter' ) );
     291                                if ( ! $is_multidimensional ) {
     292                                        add_filter( "pre_option_{$id_base}", array( $this, '_preview_filter' ) );
     293                                } else {
     294                                        if ( empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] ) ) {
     295                                                // Only add these filters once for this ID base.
     296                                                add_filter( "option_{$id_base}", $multidimensional_filter );
     297                                                add_filter( "default_option_{$id_base}", $multidimensional_filter );
     298                                        }
     299                                        self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'][ $this->id ] = $this;
    179300                                }
    180301                                break;
    181302                        default :
    class WP_Customize_Setting { 
    204325                                 */
    205326                                do_action( "customize_preview_{$this->type}", $this );
    206327                }
     328                return true;
    207329        }
    208330
    209331        /**
    210          * Callback function to filter the theme mods and options.
     332         * Callback function to filter non-multidimensional theme mods and options.
    211333         *
    212334         * If switch_to_blog() was called after the preview() method, and the current
    213335         * blog is now not the same blog, then this method does a no-op and returns
    214336         * the original value.
    215337         *
    216338         * @since 3.4.0
    217          * @uses WP_Customize_Setting::multidimensional_replace()
    218339         *
    219340         * @param mixed $original Old value.
    220341         * @return mixed New or old value.
    class WP_Customize_Setting { 
    224345                        return $original;
    225346                }
    226347
    227                 $undefined = new stdClass(); // symbol hack
     348                $undefined = new stdClass(); // Symbol hack.
    228349                $post_value = $this->post_value( $undefined );
    229                 if ( $undefined === $post_value ) {
    230                         $value = $this->_original_value;
    231                 } else {
     350                if ( $undefined !== $post_value ) {
    232351                        $value = $post_value;
     352                } else {
     353                        /*
     354                         * Note that we don't use $original here because preview() will
     355                         * not add the filter in the first place if it has an initial value
     356                         * and there is no post value.
     357                         */
     358                        $value = $this->default;
     359                }
     360                return $value;
     361        }
     362
     363        /**
     364         * Callback function to filter multidimensional theme mods and options.
     365         *
     366         * For all multidimensional settings of a given type, the preview filter for
     367         * the first setting previewed will be used to apply the values for the others.
     368         *
     369         * @since 4.4.0
     370         * @access public
     371         *
     372         * @see WP_Customize_Setting::$aggregated_multidimensionals
     373         * @param mixed $original Original root value.
     374         * @return mixed New or old value.
     375         */
     376        public function _multidimensional_preview_filter( $original ) {
     377                if ( ! $this->is_current_blog_previewed() ) {
     378                        return $original;
     379                }
     380
     381                $id_base = $this->id_data['base'];
     382
     383                // If no settings have been previewed yet (which should not be the case, since $this is), just pass through the original value.
     384                if ( empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] ) ) {
     385                        return $original;
    233386                }
    234387
    235                 return $this->multidimensional_replace( $original, $this->id_data['keys'], $value );
     388                foreach ( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] as $previewed_setting ) {
     389                        // Skip applying previewed value for any settings that have already been applied.
     390                        if ( ! empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['preview_applied_instances'][ $previewed_setting->id ] ) ) {
     391                                continue;
     392                        }
     393
     394                        // Do the replacements of the posted/default sub value into the root value.
     395                        $value = $previewed_setting->post_value( $previewed_setting->default );
     396                        $root = self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['root_value'];
     397                        $root = $previewed_setting->multidimensional_replace( $root, $previewed_setting->id_data['keys'], $value );
     398                        self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['root_value'] = $root;
     399
     400                        // Mark this setting having been applied so that it will be skipped when the filter is called again.
     401                        self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['preview_applied_instances'][ $previewed_setting->id ] = true;
     402                }
     403
     404                return self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
    236405        }
    237406
    238407        /**
    class WP_Customize_Setting { 
    299468        }
    300469
    301470        /**
     471         * Get the root value for a setting, especially for multidimensional ones.
     472         *
     473         * @since 4.4.0
     474         * @access protected
     475         *
     476         * @param mixed $default Value to return if root does not exist.
     477         * @return mixed
     478         */
     479        protected function get_root_value( $default = null ) {
     480                $id_base = $this->id_data['base'];
     481                if ( 'option' === $this->type ) {
     482                        return get_option( $id_base, $default );
     483                } else if ( 'theme_mod' ) {
     484                        return get_theme_mod( $id_base, $default );
     485                } else {
     486                        /*
     487                         * Any WP_Customize_Setting subclass implementing aggregate multidimensional
     488                         * will need to override this method to obtain the data from the appropriate
     489                         * location.
     490                         */
     491                        return $default;
     492                }
     493        }
     494
     495        /**
     496         * Set the root value for a setting, especially for multidimensional ones.
     497         *
     498         * @since 4.4.0
     499         * @access protected
     500         *
     501         * @param mixed $value Value to set as root of multidimensional setting.
     502         * @return bool Whether the multidimensional root was updated successfully.
     503         */
     504        protected function set_root_value( $value ) {
     505                $id_base = $this->id_data['base'];
     506                if ( 'option' === $this->type ) {
     507                        return update_option( $id_base, $value );
     508                } else if ( 'theme_mod' ) {
     509                        set_theme_mod( $id_base, $value );
     510                        return true;
     511                } else {
     512                        /*
     513                         * Any WP_Customize_Setting subclass implementing aggregate multidimensional
     514                         * will need to override this method to obtain the data from the appropriate
     515                         * location.
     516                         */
     517                        return false;
     518                }
     519        }
     520
     521        /**
    302522         * Save the value of the setting, using the related API.
    303523         *
    304524         * @since 3.4.0
    class WP_Customize_Setting { 
    307527         * @return bool The result of saving the value.
    308528         */
    309529        protected function update( $value ) {
    310                 switch ( $this->type ) {
    311                         case 'theme_mod' :
    312                                 $this->_update_theme_mod( $value );
     530                $id_base = $this->id_data['base'];
     531                if ( 'option' === $this->type || 'theme_mod' === $this->type ) {
     532                        if ( ! $this->is_multidimensional_aggregated ) {
     533                                return $this->set_root_value( $value );
     534                        } else {
     535                                $root = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
     536                                $root = $this->multidimensional_replace( $root, $this->id_data['keys'], $value );
     537                                self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'] = $root;
     538                                self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['updated_instances'] = $this; // Mark this setting as updated.
     539                                // @todo Should actually just go ahead and call set set_root_value() and skip the whole finalize logic?
    313540                                return true;
    314 
    315                         case 'option' :
    316                                 return $this->_update_option( $value );
    317 
    318                         default :
    319 
    320                                 /**
    321                                  * Fires when the {@see WP_Customize_Setting::update()} method is called for settings
    322                                  * not handled as theme_mods or options.
    323                                  *
    324                                  * The dynamic portion of the hook name, `$this->type`, refers to the type of setting.
    325                                  *
    326                                  * @since 3.4.0
    327                                  *
    328                                  * @param mixed                $value Value of the setting.
    329                                  * @param WP_Customize_Setting $this  WP_Customize_Setting instance.
    330                                  */
    331                                 do_action( "customize_update_{$this->type}", $value, $this );
    332 
    333                                 return has_action( "customize_update_{$this->type}" );
     541                        }
     542                } else {
     543                        /**
     544                         * Fires when the {@see WP_Customize_Setting::update()} method is called for settings
     545                         * not handled as theme_mods or options.
     546                         *
     547                         * The dynamic portion of the hook name, `$this->type`, refers to the type of setting.
     548                         *
     549                         * @since 3.4.0
     550                         *
     551                         * @param mixed                $value Value of the setting.
     552                         * @param WP_Customize_Setting $this  WP_Customize_Setting instance.
     553                         */
     554                        do_action( "customize_update_{$this->type}", $value, $this );
     555
     556                        return has_action( "customize_update_{$this->type}" );
    334557                }
    335558        }
    336559
    337560        /**
    338          * Update the theme mod from the value of the parameter.
     561         * Deprecated method.
    339562         *
    340563         * @since 3.4.0
    341          *
    342          * @param mixed $value The value to update.
     564         * @deprecated 4.4.0 Deprecated in favor of update() method.
    343565         */
    344         protected function _update_theme_mod( $value ) {
    345                 // Handle non-array theme mod.
    346                 if ( empty( $this->id_data[ 'keys' ] ) ) {
    347                         set_theme_mod( $this->id_data[ 'base' ], $value );
    348                         return;
    349                 }
    350                 // Handle array-based theme mod.
    351                 $mods = get_theme_mod( $this->id_data[ 'base' ] );
    352                 $mods = $this->multidimensional_replace( $mods, $this->id_data[ 'keys' ], $value );
    353                 if ( isset( $mods ) ) {
    354                         set_theme_mod( $this->id_data[ 'base' ], $mods );
    355                 }
     566        protected function _update_theme_mod() {
     567                _deprecated_function( __METHOD__, '4.4.0' );
    356568        }
    357569
    358570        /**
    359          * Update the option from the value of the setting.
     571         * Deprecated method.
    360572         *
    361573         * @since 3.4.0
     574         * @deprecated 4.4.0 Deprecated in favor of update() method.
     575         */
     576        protected function _update_option() {
     577                _deprecated_function( __METHOD__, '4.4.0' );
     578        }
     579
     580        /**
     581         * Finalize/commit the updates for multidimensional settings.
    362582         *
    363          * @param mixed $value The value to update.
    364          * @return bool The result of saving the value.
     583         * @see WP_Customize_Manager::save()
     584         * @since 4.4.0
     585         * @access public
    365586         */
    366         protected function _update_option( $value ) {
    367                 // Handle non-array option.
    368                 if ( empty( $this->id_data[ 'keys' ] ) )
    369                         return update_option( $this->id_data[ 'base' ], $value );
     587        final public function finalize_multidimensional_update() {
     588                if ( ! $this->is_multidimensional_aggregated ) {
     589                        return;
     590                }
     591                $id_base = $this->id_data['base'];
    370592
    371                 // Handle array-based options.
    372                 $options = get_option( $this->id_data[ 'base' ] );
    373                 $options = $this->multidimensional_replace( $options, $this->id_data[ 'keys' ], $value );
    374                 if ( isset( $options ) )
    375                         return update_option( $this->id_data[ 'base' ], $options );
     593                // Abort finalizing a multidimensional if none have been updated.
     594                if ( empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['updated_instances'] ) ) {
     595                        return;
     596                }
     597
     598                if ( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['update_finalized'] ) {
     599                        return;
     600                }
     601                // @todo We should make sure we don't inject the default values into the array. Should be OK since preview() would not have applied.
     602                $this->set_root_value( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'] );
     603                self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['update_finalized'] = true;
    376604        }
    377605
    378606        /**
    class WP_Customize_Setting { 
    383611         * @return mixed The value.
    384612         */
    385613        public function value() {
    386                 // Get the callback that corresponds to the setting type.
    387                 switch( $this->type ) {
    388                         case 'theme_mod' :
    389                                 $function = 'get_theme_mod';
    390                                 break;
    391                         case 'option' :
    392                                 $function = 'get_option';
    393                                 break;
    394                         default :
    395 
    396                                 /**
    397                                  * Filter a Customize setting value not handled as a theme_mod or option.
    398                                  *
    399                                  * The dynamic portion of the hook name, `$this->id_date['base']`, refers to
    400                                  * the base slug of the setting name.
    401                                  *
    402                                  * For settings handled as theme_mods or options, see those corresponding
    403                                  * functions for available hooks.
    404                                  *
    405                                  * @since 3.4.0
    406                                  *
    407                                  * @param mixed $default The setting default value. Default empty.
    408                                  */
    409                                 return apply_filters( 'customize_value_' . $this->id_data[ 'base' ], $this->default );
     614                $id_base = $this->id_data['base'];
     615                $is_core_type = ( 'option' === $this->type || 'theme_mod' === $this->type );
     616
     617                if ( ! $is_core_type && ! $this->is_multidimensional_aggregated ) {
     618                        $value = $this->get_root_value( $this->default ); // @todo Should $this->default be used here?
     619
     620                        /**
     621                         * Filter a Customize setting value not handled as a theme_mod or option.
     622                         *
     623                         * The dynamic portion of the hook name, `$this->id_date['base']`, refers to
     624                         * the base slug of the setting name.
     625                         *
     626                         * For settings handled as theme_mods or options, see those corresponding
     627                         * functions for available hooks.
     628                         *
     629                         * @since 3.4.0
     630                         *
     631                         * @param mixed $default The setting default value. Default empty.
     632                         */
     633                        $value = apply_filters( "customize_value_{$id_base}", $value );
     634                } else if ( $this->is_multidimensional_aggregated ) {
     635                        $root_value = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
     636                        $value = $this->multidimensional_get( $root_value, $this->id_data['keys'], $this->default );
     637                } else {
     638                        $value = $this->get_root_value( $this->default );
    410639                }
    411 
    412                 // Handle non-array value
    413                 if ( empty( $this->id_data[ 'keys' ] ) )
    414                         return $function( $this->id_data[ 'base' ], $this->default );
    415 
    416                 // Handle array-based value
    417                 $values = $function( $this->id_data[ 'base' ] );
    418                 return $this->multidimensional_get( $values, $this->id_data[ 'keys' ], $this->default );
     640                return $value;
    419641        }
    420642
    421643        /**
    class WP_Customize_Setting { 
    520742         * @param $root
    521743         * @param $keys
    522744         * @param mixed $value The value to update.
    523          * @return
     745         * @return mixed
    524746         */
    525747        final protected function multidimensional_replace( $root, $keys, $value ) {
    526748                if ( ! isset( $value ) )
    class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { 
    8531075        }
    8541076
    8551077        /**
    856          * Get the instance data for a given widget setting.
     1078         * Get the instance data for a given nav_menu_item setting.
    8571079         *
    8581080         * @since 4.3.0
    8591081         * @access public
    class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { 
    9871209         * Handle previewing the setting.
    9881210         *
    9891211         * @since 4.3.0
     1212         * @since 4.4.0 Added boolean return value.
    9901213         * @access public
    9911214         *
    9921215         * @see WP_Customize_Manager::post_value()
     1216         *
     1217         * @return bool False if method short-circuited due to no-op.
    9931218         */
    9941219        public function preview() {
    9951220                if ( $this->is_previewed ) {
    996                         return;
     1221                        return false;
     1222                }
     1223
     1224                $undefined = new stdClass();
     1225                $is_placeholder = ( $this->post_id < 0 );
     1226                $is_dirty = ( $undefined !== $this->post_value( $undefined ) );
     1227                if ( ! $is_placeholder && ! $is_dirty ) {
     1228                        return false;
    9971229                }
    9981230
    9991231                $this->is_previewed              = true;
    class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { 
    10091241                }
    10101242
    10111243                // @todo Add get_post_metadata filters for plugins to add their data.
     1244
     1245                return true;
    10121246        }
    10131247
    10141248        /**
    class WP_Customize_Nav_Menu_Setting extends WP_Customize_Setting { 
    16211855         * Handle previewing the setting.
    16221856         *
    16231857         * @since 4.3.0
     1858         * @since 4.4.0 Added boolean return value
    16241859         * @access public
    16251860         *
    16261861         * @see WP_Customize_Manager::post_value()
     1862         *
     1863         * @return bool False if method short-circuited due to no-op.
    16271864         */
    16281865        public function preview() {
    16291866                if ( $this->is_previewed ) {
    1630                         return;
     1867                        return false;
     1868                }
     1869
     1870                $undefined = new stdClass();
     1871                $is_placeholder = ( $this->term_id < 0 );
     1872                $is_dirty = ( $undefined !== $this->post_value( $undefined ) );
     1873                if ( ! $is_placeholder && ! $is_dirty ) {
     1874                        return false;
    16311875                }
    16321876
    16331877                $this->is_previewed       = true;
    class WP_Customize_Nav_Menu_Setting extends WP_Customize_Setting { 
    16381882                add_filter( 'wp_get_nav_menu_object', array( $this, 'filter_wp_get_nav_menu_object' ), 10, 2 );
    16391883                add_filter( 'default_option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
    16401884                add_filter( 'option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
     1885
     1886                return true;
    16411887        }
    16421888
    16431889        /**
  • tests/phpunit/tests/customize/setting.php

    diff --git tests/phpunit/tests/customize/setting.php tests/phpunit/tests/customize/setting.php
    index 069a8ae..600fb3f 100644
    class Tests_WP_Customize_Setting extends WP_UnitTestCase { 
    102102                        $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
    103103                        $this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $name, $this->undefined ) );
    104104                        $this->assertEquals( $default, $setting->value() );
    105                         $setting->preview();
     105                        $this->assertTrue( $setting->preview(), 'Preview should not no-op since setting has no existing value.' );
    106106                        $this->assertEquals( $default, call_user_func( $type_options['getter'], $name, $this->undefined ), sprintf( 'Expected %s(%s) to return setting default: %s.', $type_options['getter'], $name, $default ) );
    107107                        $this->assertEquals( $default, $setting->value() );
    108108
    class Tests_WP_Customize_Setting extends WP_UnitTestCase { 
    114114                        $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
    115115                        $this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name ) );
    116116                        $this->assertEquals( $initial_value, $setting->value() );
    117                         $setting->preview();
     117                        $this->assertFalse( $setting->preview(), 'Preview should no-op since setting value was extant and no post value was present.' );
    118118                        $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods)
    119119                        $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods)
    120120                        $this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name ) );
    121121                        $this->assertEquals( $initial_value, $setting->value() );
    122122
    123                         // @todo What if we call the setter after preview() is called? If no post_value, should the new set value be stored? If that happens, then the following 3 assertions should be inverted
    124123                        $overridden_value = "overridden_value_$name";
    125124                        call_user_func( $type_options['setter'], $name, $overridden_value );
    126                         $this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name ) );
    127                         $this->assertEquals( $initial_value, $setting->value() );
    128                         $this->assertNotEquals( $overridden_value, $setting->value() );
     125                        $message = 'Initial value should be overridden because initial preview() was no-op due to setting having existing value and/or post value was absent.';
     126                        $this->assertEquals( $overridden_value, call_user_func( $type_options['getter'], $name ), $message );
     127                        $this->assertEquals( $overridden_value, $setting->value(), $message );
     128                        $this->assertNotEquals( $initial_value, $setting->value(), $message );
    129129
    130130                        // Non-multidimensional: Test unset setting being overridden by a post value
    131131                        $name = "unset_{$type}_overridden";
    class Tests_WP_Customize_Setting extends WP_UnitTestCase { 
    133133                        $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
    134134                        $this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $name, $this->undefined ) );
    135135                        $this->assertEquals( $default, $setting->value() );
    136                         $setting->preview(); // activate post_data
     136                        $this->assertTrue( $setting->preview(), 'Preview applies because setting has post_data_overrides.' ); // activate post_data
    137137                        $this->assertEquals( $this->post_data_overrides[ $name ], call_user_func( $type_options['getter'], $name, $this->undefined ) );
    138138                        $this->assertEquals( $this->post_data_overrides[ $name ], $setting->value() );
    139139
    class Tests_WP_Customize_Setting extends WP_UnitTestCase { 
    145145                        $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
    146146                        $this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name, $this->undefined ) );
    147147                        $this->assertEquals( $initial_value, $setting->value() );
    148                         $setting->preview(); // activate post_data
     148                        $this->assertTrue( $setting->preview(), 'Preview applies because setting has post_data_overrides.' ); // activate post_data
    149149                        $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods)
    150150                        $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods)
    151151                        $this->assertEquals( $this->post_data_overrides[ $name ], call_user_func( $type_options['getter'], $name, $this->undefined ) );
    class Tests_WP_Customize_Setting extends WP_UnitTestCase { 
    167167                        $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
    168168                        $this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $base_name, $this->undefined ) );
    169169                        $this->assertEquals( $default, $setting->value() );
    170                         $setting->preview();
     170                        $this->assertTrue( $setting->preview() );
    171171                        $base_value = call_user_func( $type_options['getter'], $base_name, $this->undefined );
    172172                        $this->assertArrayHasKey( 'foo', $base_value );
    173173                        $this->assertEquals( $default, $base_value['foo'] );
    class Tests_WP_Customize_Setting extends WP_UnitTestCase { 
    311311                $this->assertEquals( $initial_value, $this->custom_type_getter( $name, $this->undefined ) );
    312312                $this->assertEquals( $initial_value, $setting->value() );
    313313                $setting->preview();
    314                 $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) );
    315                 $this->assertEquals( 2, did_action( "customize_preview_{$setting->type}" ) );
     314                $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ), 'Zero preview actions because initial value is set with no incoming post value, so there is no preview to apply.' );
     315                $this->assertEquals( 1, did_action( "customize_preview_{$setting->type}" ) );
    316316                $this->assertEquals( $initial_value, $this->custom_type_getter( $name, $this->undefined ) ); // should be same as above
    317317                $this->assertEquals( $initial_value, $setting->value() ); // should be same as above
    318318
    class Tests_WP_Customize_Setting extends WP_UnitTestCase { 
    325325                $this->assertEquals( $this->undefined, $this->custom_type_getter( $name, $this->undefined ) );
    326326                $this->assertEquals( $default, $setting->value() );
    327327                $setting->preview();
    328                 $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) );
    329                 $this->assertEquals( 3, did_action( "customize_preview_{$setting->type}" ) );
     328                $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ), 'One preview action now because initial value was not set and/or there is no incoming post value, so there is is a preview to apply.' );
     329                $this->assertEquals( 2, did_action( "customize_preview_{$setting->type}" ) );
    330330                $this->assertEquals( $post_data_overrides[ $name ], $this->custom_type_getter( $name, $this->undefined ) );
    331331                $this->assertEquals( $post_data_overrides[ $name ], $setting->value() );
    332332
    class Tests_WP_Customize_Setting extends WP_UnitTestCase { 
    342342                $this->assertEquals( $initial_value, $setting->value() );
    343343                $setting->preview();
    344344                $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) );
    345                 $this->assertEquals( 4, did_action( "customize_preview_{$setting->type}" ) );
     345                $this->assertEquals( 3, did_action( "customize_preview_{$setting->type}" ) );
    346346                $this->assertEquals( $post_data_overrides[ $name ], $this->custom_type_getter( $name, $this->undefined ) );
    347347                $this->assertEquals( $post_data_overrides[ $name ], $setting->value() );
    348348