WordPress.org

Make WordPress Core

Ticket #32103: 32103.wip2.diff

File 32103.wip2.diff, 30.7 KB (added by westonruter, 5 years ago)
  • 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 98f37f9..68e2aa5 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                                'value_inspected_instances' => array(), // Keep track of which settings have had value() called.
     190
     191                                'root_value'                => $this->get_root_value( array() ), // Root value for initial state, manipulated by preview and update calls.
     192                                'previewed_value'           => null,    // Root value which has multidimensional replacements applied for each previewed instance.
     193                                'updated_value'             => null,    // Each call to update() will do the multidimensional replacements on $this->get_root_value().
     194                        );
     195                }
     196                $this->is_multidimensional_aggregated = true;
    120197        }
    121198
    122199        /**
    class WP_Customize_Setting { 
    153230        protected $_original_value;
    154231
    155232        /**
    156          * Set up filters for the setting so that the preview request
    157          * will render the drafted changes.
     233         * Add filters to supply the setting's value when accessed.
     234         *
     235         * If the setting already has a pre-existing value and there is no incoming
     236         * post value for the setting, then this method will short-circuit since
     237         * there is no change to preview.
    158238         *
    159239         * @since 3.4.0
     240         * @since 4.4.0 Added boolean return value.
     241         * @access public
     242         *
     243         * @return bool False when preview short-circuits due no change needing to be previewed.
    160244         */
    161245        public function preview() {
    162                 if ( ! isset( $this->_original_value ) ) {
    163                         $this->_original_value = $this->value();
    164                 }
    165246                if ( ! isset( $this->_previewed_blog_id ) ) {
    166247                        $this->_previewed_blog_id = get_current_blog_id();
    167248                }
     249                $id_base = $this->id_data['base'];
     250                $is_multidimensional = ! empty( $this->id_data['keys'] );
     251                $multidimensional_filter = array( $this, '_multidimensional_preview_filter' );
     252
     253                /*
     254                 * Check if the setting has a pre-existing value (an isset check),
     255                 * and if doesn't have any incoming post value. If both checks are true,
     256                 * then the preview short-circuits because there is nothing that needs
     257                 * to be previewed.
     258                 */
     259                $undefined = new stdClass();
     260                $needs_preview = ( $undefined !== $this->post_value( $undefined ) );
     261                $value = null;
     262
     263                // Since no post value was defined, check if we have an initial value set.
     264                if ( ! $needs_preview ) {
     265                        if ( $this->is_multidimensional_aggregated ) {
     266                                $root = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
     267                                $value = $this->multidimensional_get( $root, $this->id_data['keys'], $undefined );
     268                        } else {
     269                                $default = $this->default;
     270                                $this->default = $undefined; // Temporarily set default to undefined so we can detect if existing value is set.
     271                                $value = $this->value();
     272                                $this->default = $default;
     273                        }
     274                        $needs_preview = ( $undefined === $value ); // Because the default needs to be supplied.
     275                }
     276
     277                if ( ! $needs_preview ) {
     278                        return false;
     279                }
    168280
    169                 switch( $this->type ) {
     281//              $this->_original_value = ( $value ?: $default );
     282                switch ( $this->type ) {
    170283                        case 'theme_mod' :
    171                                 add_filter( 'theme_mod_' . $this->id_data[ 'base' ], array( $this, '_preview_filter' ) );
     284                                if ( ! $is_multidimensional ) {
     285                                        add_filter( "theme_mod_{$id_base}", array( $this, '_preview_filter' ) );
     286                                } else {
     287                                        if ( empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] ) ) {
     288                                                // Only add this filter once for this ID base.
     289                                                add_filter( "theme_mod_{$id_base}", $multidimensional_filter );
     290                                        }
     291                                        self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'][ $this->id ] = $this;
     292                                }
    172293                                break;
    173294                        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' ) );
     295                                if ( ! $is_multidimensional ) {
     296                                        add_filter( "pre_option_{$id_base}", array( $this, '_preview_filter' ) );
     297                                } else {
     298                                        if ( empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] ) ) {
     299                                                // Only add these filters once for this ID base.
     300                                                add_filter( "option_{$id_base}", $multidimensional_filter );
     301                                                add_filter( "default_option_{$id_base}", $multidimensional_filter );
     302                                        }
     303                                        self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'][ $this->id ] = $this;
    179304                                }
    180305                                break;
    181306                        default :
    class WP_Customize_Setting { 
    204329                                 */
    205330                                do_action( "customize_preview_{$this->type}", $this );
    206331                }
     332                return true;
    207333        }
    208334
    209335        /**
    210          * Callback function to filter the theme mods and options.
     336         * Callback function to filter non-multidimensional theme mods and options.
    211337         *
    212338         * If switch_to_blog() was called after the preview() method, and the current
    213339         * blog is now not the same blog, then this method does a no-op and returns
    214340         * the original value.
    215341         *
    216342         * @since 3.4.0
    217          * @uses WP_Customize_Setting::multidimensional_replace()
    218343         *
    219344         * @param mixed $original Old value.
    220345         * @return mixed New or old value.
    class WP_Customize_Setting { 
    224349                        return $original;
    225350                }
    226351
    227                 $undefined = new stdClass(); // symbol hack
     352                $undefined = new stdClass(); // Symbol hack.
    228353                $post_value = $this->post_value( $undefined );
    229                 if ( $undefined === $post_value ) {
    230                         $value = $this->_original_value;
    231                 } else {
     354                if ( $undefined !== $post_value ) {
    232355                        $value = $post_value;
     356                } else {
     357                        $value = $original;
     358                }
     359                return $value;
     360        }
     361
     362        /**
     363         * Callback function to filter multidimensional theme mods and options.
     364         *
     365         * For all multidimensional settings of a given type, the preview filter for
     366         * the first setting previewed will be used to apply the values for the others.
     367         *
     368         * @since 4.4.0
     369         * @access public
     370         *
     371         * @see WP_Customize_Setting::$aggregated_multidimensionals
     372         * @param mixed $original Original root value.
     373         * @return mixed New or old value.
     374         */
     375        public function _multidimensional_preview_filter( $original ) {
     376                if ( ! $this->is_current_blog_previewed() ) {
     377                        return $original;
     378                }
     379
     380                $id_base = $this->id_data['base'];
     381
     382                // If no settings have been previewed yet (which should not be the case, since $this is), just pass through the original value.
     383                if ( empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] ) ) {
     384                        return $original;
    233385                }
    234386
    235                 return $this->multidimensional_replace( $original, $this->id_data['keys'], $value );
     387                foreach ( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] as $previewed_setting ) {
     388                        // Skip applying previewed value for any settings that have already been applied.
     389                        if ( ! empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['preview_applied_instances'][ $previewed_setting->id ] ) ) {
     390                                continue;
     391                        }
     392
     393                        // Do the replacements of the posted/default sub value into the root value.
     394                        $value = $previewed_setting->post_value( $previewed_setting->default );
     395                        $root = self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['root_value'];
     396                        $root = $previewed_setting->multidimensional_replace( $root, $previewed_setting->id_data['keys'], $value );
     397                        self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['root_value'] = $root;
     398
     399                        // Mark this setting having been applied so that it will be skipped when the filter is called again.
     400                        self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['preview_applied_instances'][ $previewed_setting->id ] = true;
     401                }
     402
     403                return self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
    236404        }
    237405
    238406        /**
    class WP_Customize_Setting { 
    299467        }
    300468
    301469        /**
     470         * Get the root value for a setting, especially for multidimensional ones.
     471         *
     472         * @since 4.4.0
     473         * @access protected
     474         *
     475         * @param mixed $default Value to return if root does not exist.
     476         * @return mixed
     477         */
     478        protected function get_root_value( $default = null ) {
     479                $id_base = $this->id_data['base'];
     480                if ( 'option' === $this->type ) {
     481                        return get_option( $id_base, $default );
     482                } else if ( 'theme_mod' ) {
     483                        return get_theme_mod( $id_base, $default );
     484                } else {
     485                        /*
     486                         * Any WP_Customize_Setting subclass implementing aggregate multidimensional
     487                         * will need to override this method to obtain the data from the appropriate
     488                         * location.
     489                         */
     490                        return $default;
     491                }
     492        }
     493
     494        /**
     495         * Set the root value for a setting, especially for multidimensional ones.
     496         *
     497         * @since 4.4.0
     498         * @access protected
     499         *
     500         * @param mixed $value Value to set as root of multidimensional setting.
     501         * @return bool Whether the multidimensional root was updated successfully.
     502         */
     503        protected function set_root_value( $value ) {
     504                $id_base = $this->id_data['base'];
     505                if ( 'option' === $this->type ) {
     506                        return update_option( $id_base, $value );
     507                } else if ( 'theme_mod' ) {
     508                        set_theme_mod( $id_base, $value );
     509                        return true;
     510                } else {
     511                        /*
     512                         * Any WP_Customize_Setting subclass implementing aggregate multidimensional
     513                         * will need to override this method to obtain the data from the appropriate
     514                         * location.
     515                         */
     516                        return false;
     517                }
     518        }
     519
     520        /**
    302521         * Save the value of the setting, using the related API.
    303522         *
    304523         * @since 3.4.0
    class WP_Customize_Setting { 
    307526         * @return mixed The result of saving the value.
    308527         */
    309528        protected function update( $value ) {
    310                 switch( $this->type ) {
    311                         case 'theme_mod' :
    312                                 return $this->_update_theme_mod( $value );
    313 
    314                         case 'option' :
    315                                 return $this->_update_option( $value );
    316 
    317                         default :
    318 
    319                                 /**
    320                                  * Fires when the {@see WP_Customize_Setting::update()} method is called for settings
    321                                  * not handled as theme_mods or options.
    322                                  *
    323                                  * The dynamic portion of the hook name, `$this->type`, refers to the type of setting.
    324                                  *
    325                                  * @since 3.4.0
    326                                  *
    327                                  * @param mixed                $value Value of the setting.
    328                                  * @param WP_Customize_Setting $this  WP_Customize_Setting instance.
    329                                  */
    330                                 return do_action( 'customize_update_' . $this->type, $value, $this );
     529                $id_base = $this->id_data['base'];
     530                if ( 'option' === $this->type || 'theme_mod' === $this->type ) {
     531                        if ( ! $this->is_multidimensional_aggregated ) {
     532                                return $this->set_root_value( $value );
     533                        } else {
     534                                if ( isset( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['updated_value'] ) ) {
     535                                        $root = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['updated_value'];
     536                                } else {
     537                                        $root = $this->get_root_value( $this->default );
     538                                }
     539                                $root = $this->multidimensional_replace( $root, $this->id_data['keys'], $value );
     540                                self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['updated_value'] = $root;
     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 );
    331555                }
    332556        }
    333557
    334558        /**
    335          * Update the theme mod from the value of the parameter.
     559         * Deprecated method.
    336560         *
    337561         * @since 3.4.0
    338          *
    339          * @param mixed $value The value to update.
     562         * @deprecated 4.4.0 Deprecated in favor of update() method.
    340563         */
    341         protected function _update_theme_mod( $value ) {
    342                 // Handle non-array theme mod.
    343                 if ( empty( $this->id_data[ 'keys' ] ) ) {
    344                         set_theme_mod( $this->id_data[ 'base' ], $value );
    345                         return;
    346                 }
    347                 // Handle array-based theme mod.
    348                 $mods = get_theme_mod( $this->id_data[ 'base' ] );
    349                 $mods = $this->multidimensional_replace( $mods, $this->id_data[ 'keys' ], $value );
    350                 if ( isset( $mods ) ) {
    351                         set_theme_mod( $this->id_data[ 'base' ], $mods );
    352                 }
     564        protected function _update_theme_mod() {
     565                _deprecated_function( __METHOD__, '4.4.0' );
    353566        }
    354567
    355568        /**
    356          * Update the option from the value of the setting.
     569         * Deprecated method.
    357570         *
    358571         * @since 3.4.0
     572         * @deprecated 4.4.0 Deprecated in favor of update() method.
     573         */
     574        protected function _update_option() {
     575                _deprecated_function( __METHOD__, '4.4.0' );
     576        }
     577
     578        /**
     579         * Finalize/commit the updates for multidimensional settings.
    359580         *
    360          * @param mixed $value The value to update.
    361          * @return bool The result of saving the value.
    362          */
    363         protected function _update_option( $value ) {
    364                 // Handle non-array option.
    365                 if ( empty( $this->id_data[ 'keys' ] ) )
    366                         return update_option( $this->id_data[ 'base' ], $value );
    367 
    368                 // Handle array-based options.
    369                 $options = get_option( $this->id_data[ 'base' ] );
    370                 $options = $this->multidimensional_replace( $options, $this->id_data[ 'keys' ], $value );
    371                 if ( isset( $options ) )
    372                         return update_option( $this->id_data[ 'base' ], $options );
     581         * @see WP_Customize_Manager::save()
     582         * @since 4.4.0
     583         * @access public
     584         */
     585        final public function finalize_multidimensional_update() {
     586                if ( ! $this->is_multidimensional_aggregated ) {
     587                        return;
     588                }
     589                $id_base = $this->id_data['base'];
     590                if ( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['update_finalized'] ) {
     591                        return;
     592                }
     593                if ( ! isset( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['updated_value'] ) ) {
     594                        return;
     595                }
     596                $root = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['updated_value'];
     597                if ( isset( $root ) ) {
     598                        $this->set_root_value( $root );
     599                }
     600                self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['update_finalized'] = true;
    373601        }
    374602
    375603        /**
    class WP_Customize_Setting { 
    380608         * @return mixed The value.
    381609         */
    382610        public function value() {
    383                 // Get the callback that corresponds to the setting type.
    384                 switch( $this->type ) {
    385                         case 'theme_mod' :
    386                                 $function = 'get_theme_mod';
    387                                 break;
    388                         case 'option' :
    389                                 $function = 'get_option';
    390                                 break;
    391                         default :
    392 
    393                                 /**
    394                                  * Filter a Customize setting value not handled as a theme_mod or option.
    395                                  *
    396                                  * The dynamic portion of the hook name, `$this->id_date['base']`, refers to
    397                                  * the base slug of the setting name.
    398                                  *
    399                                  * For settings handled as theme_mods or options, see those corresponding
    400                                  * functions for available hooks.
    401                                  *
    402                                  * @since 3.4.0
    403                                  *
    404                                  * @param mixed $default The setting default value. Default empty.
    405                                  */
    406                                 return apply_filters( 'customize_value_' . $this->id_data[ 'base' ], $this->default );
     611                $id_base = $this->id_data['base'];
     612                $is_core_type = ( 'option' === $this->type || 'theme_mod' === $this->type );
     613
     614                if ( ! $is_core_type && ! $this->is_multidimensional_aggregated ) {
     615                        $value = $this->get_root_value( $this->default ); // @todo Should $this->default be used here?
     616
     617                        /**
     618                         * Filter a Customize setting value not handled as a theme_mod or option.
     619                         *
     620                         * The dynamic portion of the hook name, `$this->id_date['base']`, refers to
     621                         * the base slug of the setting name.
     622                         *
     623                         * For settings handled as theme_mods or options, see those corresponding
     624                         * functions for available hooks.
     625                         *
     626                         * @since 3.4.0
     627                         *
     628                         * @param mixed $default The setting default value. Default empty.
     629                         */
     630                        $value = apply_filters( 'customize_value_' . $this->id_data['base'], $value );
     631                } else if ( $this->is_multidimensional_aggregated ) {
     632                        $value = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
     633
     634                        if ( ! isset( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['value_inspected_instances'][ $this->id ] ) ) {
     635                                $value = $this->multidimensional_get( $value, $this->id_data['keys'], $this->default );
     636                                self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'] = $value;
     637
     638                                // Mark this multidimensional setting as having had its value inspected.
     639                                self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['value_inspected_instances'][ $this->id ] = $this;
     640                        }
     641                } else {
     642                        $value = $this->get_root_value( $this->default );
    407643                }
    408 
    409                 // Handle non-array value
    410                 if ( empty( $this->id_data[ 'keys' ] ) )
    411                         return $function( $this->id_data[ 'base' ], $this->default );
    412 
    413                 // Handle array-based value
    414                 $values = $function( $this->id_data[ 'base' ] );
    415                 return $this->multidimensional_get( $values, $this->id_data[ 'keys' ], $this->default );
     644                return $value;
    416645        }
    417646
    418647        /**
    class WP_Customize_Setting { 
    517746         * @param $root
    518747         * @param $keys
    519748         * @param mixed $value The value to update.
    520          * @return
     749         * @return mixed
    521750         */
    522751        final protected function multidimensional_replace( $root, $keys, $value ) {
    523752                if ( ! isset( $value ) )
    class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { 
    8501079        }
    8511080
    8521081        /**
    853          * Get the instance data for a given widget setting.
     1082         * Get the instance data for a given nav_menu_item setting.
    8541083         *
    8551084         * @since 4.3.0
    8561085         * @access public
    class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { 
    9841213         * Handle previewing the setting.
    9851214         *
    9861215         * @since 4.3.0
     1216         * @since 4.4.0 Added boolean return value.
    9871217         * @access public
    9881218         *
    9891219         * @see WP_Customize_Manager::post_value()
     1220         *
     1221         * @return bool False if method short-circuited due to no-op.
    9901222         */
    9911223        public function preview() {
    9921224                if ( $this->is_previewed ) {
    993                         return;
     1225                        return false;
     1226                }
     1227
     1228                $undefined = new stdClass();
     1229                $is_placeholder = ( $this->post_id < 0 );
     1230                $is_dirty = ( $undefined !== $this->post_value( $undefined ) );
     1231                if ( ! $is_placeholder && ! $is_dirty ) {
     1232                        return false;
    9941233                }
    9951234
    9961235                $this->is_previewed              = true;
    class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { 
    10061245                }
    10071246
    10081247                // @todo Add get_post_metadata filters for plugins to add their data.
     1248
     1249                return true;
    10091250        }
    10101251
    10111252        /**
    class WP_Customize_Nav_Menu_Setting extends WP_Customize_Setting { 
    16181859         * Handle previewing the setting.
    16191860         *
    16201861         * @since 4.3.0
     1862         * @since 4.4.0 Added boolean return value
    16211863         * @access public
    16221864         *
    16231865         * @see WP_Customize_Manager::post_value()
     1866         *
     1867         * @return bool False if method short-circuited due to no-op.
    16241868         */
    16251869        public function preview() {
    16261870                if ( $this->is_previewed ) {
    1627                         return;
     1871                        return false;
     1872                }
     1873
     1874                $undefined = new stdClass();
     1875                $is_placeholder = ( $this->term_id < 0 );
     1876                $is_dirty = ( $undefined !== $this->post_value( $undefined ) );
     1877                if ( ! $is_placeholder && ! $is_dirty ) {
     1878                        return false;
    16281879                }
    16291880
    16301881                $this->is_previewed       = true;
    class WP_Customize_Nav_Menu_Setting extends WP_Customize_Setting { 
    16351886                add_filter( 'wp_get_nav_menu_object', array( $this, 'filter_wp_get_nav_menu_object' ), 10, 2 );
    16361887                add_filter( 'default_option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
    16371888                add_filter( 'option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
     1889
     1890                return true;
    16381891        }
    16391892
    16401893        /**
  • tests/phpunit/tests/customize/setting.php

    diff --git tests/phpunit/tests/customize/setting.php tests/phpunit/tests/customize/setting.php
    index 08bbc65..f3accbb 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