<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[35007] trunk: Customizer: Fix scalability performance problem for previewing multidimensional settings.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/35007">35007</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/35007","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>westonruter</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2015-10-10 09:05:04 +0000 (Sat, 10 Oct 2015)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Customizer: Fix scalability performance problem for previewing multidimensional settings.

As the number of multidimensional settings (serialized options and theme mods) increase for a given ID base (e.g. a widget of a certain type), the number of calls to the `multidimensional` methods on `WP_Customize_Setting` increase exponentially, and the time for the preview to refresh grows in time exponentially as well.

To improve performance, this change reduces the number of filters needed to preview the settings off of a multidimensional root from N to 1. This improves performance from `O(n^2)` to `O(n)`, but the linear increase is so low that the performance is essentially `O(1)` in comparison. This is achieved by introducing the concept of an "aggregated multidimensional" setting, where the root value of the multidimensional serialized setting value gets cached in a static array variable shared across all settings.

Also improves performance by only adding preview filters if there is actually a need to do so: there is no need to add a filter if there is an initial value and if there is no posted value for a given setting (if it is not dirty).

Fixes <a href="https://core.trac.wordpress.org/ticket/32103">#32103</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesclasswpcustomizesettingphp">trunk/src/wp-includes/class-wp-customize-setting.php</a></li>
<li><a href="#trunktestsphpunittestscustomizesettingphp">trunk/tests/phpunit/tests/customize/setting.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesclasswpcustomizesettingphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-customize-setting.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-customize-setting.php      2015-10-10 06:50:35 UTC (rev 35006)
+++ trunk/src/wp-includes/class-wp-customize-setting.php        2015-10-10 09:05:04 UTC (rev 35007)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -82,6 +82,25 @@
</span><span class="cx" style="display: block; padding: 0 10px">        protected $id_data = array();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Cache of multidimensional values to improve performance.
+        *
+        * @since 4.4.0
+        * @access protected
+        * @var array
+        * @static
+        */
+       protected static $aggregated_multidimensionals = array();
+
+       /**
+        * Whether the multidimensional setting is aggregated.
+        *
+        * @since 4.4.0
+        * @access protected
+        * @var bool
+        */
+       protected $is_multidimensional_aggregated = false;
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Constructor.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * Any supplied $args override class property defaults.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -96,30 +115,83 @@
</span><span class="cx" style="display: block; padding: 0 10px">        public function __construct( $manager, $id, $args = array() ) {
</span><span class="cx" style="display: block; padding: 0 10px">                $keys = array_keys( get_object_vars( $this ) );
</span><span class="cx" style="display: block; padding: 0 10px">                foreach ( $keys as $key ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( isset( $args[ $key ] ) )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( isset( $args[ $key ] ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 $this->$key = $args[ $key ];
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        }
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->manager = $manager;
</span><span class="cx" style="display: block; padding: 0 10px">                $this->id = $id;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Parse the ID for array keys.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->id_data[ 'keys' ] = preg_split( '/\[/', str_replace( ']', '', $this->id ) );
-               $this->id_data[ 'base' ] = array_shift( $this->id_data[ 'keys' ] );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->id_data['keys'] = preg_split( '/\[/', str_replace( ']', '', $this->id ) );
+               $this->id_data['base'] = array_shift( $this->id_data['keys'] );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Rebuild the ID.
</span><span class="cx" style="display: block; padding: 0 10px">                $this->id = $this->id_data[ 'base' ];
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( ! empty( $this->id_data[ 'keys' ] ) )
-                       $this->id .= '[' . implode( '][', $this->id_data[ 'keys' ] ) . ']';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( ! empty( $this->id_data[ 'keys' ] ) ) {
+                       $this->id .= '[' . implode( '][', $this->id_data['keys'] ) . ']';
+               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( $this->sanitize_callback )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( $this->sanitize_callback ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         add_filter( "customize_sanitize_{$this->id}", $this->sanitize_callback, 10, 2 );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                }
+               if ( $this->sanitize_js_callback ) {
+                       add_filter( "customize_sanitize_js_{$this->id}", $this->sanitize_js_callback, 10, 2 );
+               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( $this->sanitize_js_callback )
-                       add_filter( "customize_sanitize_js_{$this->id}", $this->sanitize_js_callback, 10, 2 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( 'option' === $this->type || 'theme_mod' === $this->type ) {
+                       // Other setting types can opt-in to aggregate multidimensional explicitly.
+                       $this->aggregate_multidimensional();
+               }
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Get parsed ID data for multidimensional setting.
+        *
+        * @since 4.4.0
+        * @access public
+        *
+        * @return array {
+        *     ID data for multidimensional setting.
+        *
+        *     @type string $base ID base
+        *     @type array  $keys Keys for multidimensional array.
+        * }
+        */
+       final public function id_data() {
+               return $this->id_data;
+       }
+
+       /**
+        * Set up the setting for aggregated multidimensional values.
+        *
+        * When a multidimensional setting gets aggregated, all of its preview and update
+        * calls get combined into one call, greatly improving performance.
+        *
+        * @since 4.4.0
+        * @access protected
+        */
+       protected function aggregate_multidimensional() {
+               if ( empty( $this->id_data['keys'] ) ) {
+                       return;
+               }
+
+               $id_base = $this->id_data['base'];
+               if ( ! isset( self::$aggregated_multidimensionals[ $this->type ] ) ) {
+                       self::$aggregated_multidimensionals[ $this->type ] = array();
+               }
+               if ( ! isset( self::$aggregated_multidimensionals[ $this->type ][ $id_base ] ) ) {
+                       self::$aggregated_multidimensionals[ $this->type ][ $id_base ] = array(
+                               'previewed_instances'       => array(), // Calling preview() will add the $setting to the array.
+                               'preview_applied_instances' => array(), // Flags for which settings have had their values applied.
+                               'root_value'                => $this->get_root_value( array() ), // Root value for initial state, manipulated by preview and update calls.
+                       );
+               }
+               $this->is_multidimensional_aggregated = true;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * The ID for the current blog when the preview() method was called.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.2.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -153,29 +225,76 @@
</span><span class="cx" style="display: block; padding: 0 10px">        protected $_original_value;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * Set up filters for the setting so that the preview request
-        * will render the drafted changes.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Add filters to supply the setting's value when accessed.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * If the setting already has a pre-existing value and there is no incoming
+        * post value for the setting, then this method will short-circuit since
+        * there is no change to preview.
+        *
</ins><span class="cx" style="display: block; padding: 0 10px">          * @since 3.4.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 4.4.0 Added boolean return value.
+        * @access public
+        *
+        * @return bool False when preview short-circuits due no change needing to be previewed.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        public function preview() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( ! isset( $this->_original_value ) ) {
-                       $this->_original_value = $this->value();
-               }
</del><span class="cx" style="display: block; padding: 0 10px">                 if ( ! isset( $this->_previewed_blog_id ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->_previewed_blog_id = get_current_blog_id();
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $id_base = $this->id_data['base'];
+               $is_multidimensional = ! empty( $this->id_data['keys'] );
+               $multidimensional_filter = array( $this, '_multidimensional_preview_filter' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                switch( $this->type ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         /*
+                * Check if the setting has a pre-existing value (an isset check),
+                * and if doesn't have any incoming post value. If both checks are true,
+                * then the preview short-circuits because there is nothing that needs
+                * to be previewed.
+                */
+               $undefined = new stdClass();
+               $needs_preview = ( $undefined !== $this->post_value( $undefined ) );
+               $value = null;
+
+               // Since no post value was defined, check if we have an initial value set.
+               if ( ! $needs_preview ) {
+                       if ( $this->is_multidimensional_aggregated ) {
+                               $root = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
+                               $value = $this->multidimensional_get( $root, $this->id_data['keys'], $undefined );
+                       } else {
+                               $default = $this->default;
+                               $this->default = $undefined; // Temporarily set default to undefined so we can detect if existing value is set.
+                               $value = $this->value();
+                               $this->default = $default;
+                       }
+                       $needs_preview = ( $undefined === $value ); // Because the default needs to be supplied.
+               }
+
+               if ( ! $needs_preview ) {
+                       return false;
+               }
+
+               switch ( $this->type ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         case 'theme_mod' :
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                add_filter( 'theme_mod_' . $this->id_data[ 'base' ], array( $this, '_preview_filter' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         if ( ! $is_multidimensional ) {
+                                       add_filter( "theme_mod_{$id_base}", array( $this, '_preview_filter' ) );
+                               } else {
+                                       if ( empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] ) ) {
+                                               // Only add this filter once for this ID base.
+                                               add_filter( "theme_mod_{$id_base}", $multidimensional_filter );
+                                       }
+                                       self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'][ $this->id ] = $this;
+                               }
</ins><span class="cx" style="display: block; padding: 0 10px">                                 break;
</span><span class="cx" style="display: block; padding: 0 10px">                        case 'option' :
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                if ( empty( $this->id_data[ 'keys' ] ) )
-                                       add_filter( 'pre_option_' . $this->id_data[ 'base' ], array( $this, '_preview_filter' ) );
-                               else {
-                                       add_filter( 'option_' . $this->id_data[ 'base' ], array( $this, '_preview_filter' ) );
-                                       add_filter( 'default_option_' . $this->id_data[ 'base' ], array( $this, '_preview_filter' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         if ( ! $is_multidimensional ) {
+                                       add_filter( "pre_option_{$id_base}", array( $this, '_preview_filter' ) );
+                               } else {
+                                       if ( empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] ) ) {
+                                               // Only add these filters once for this ID base.
+                                               add_filter( "option_{$id_base}", $multidimensional_filter );
+                                               add_filter( "default_option_{$id_base}", $multidimensional_filter );
+                                       }
+                                       self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'][ $this->id ] = $this;
</ins><span class="cx" style="display: block; padding: 0 10px">                                 }
</span><span class="cx" style="display: block; padding: 0 10px">                                break;
</span><span class="cx" style="display: block; padding: 0 10px">                        default :
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -204,17 +323,17 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                 */
</span><span class="cx" style="display: block; padding: 0 10px">                                do_action( "customize_preview_{$this->type}", $this );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                return true;
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * Callback function to filter the theme mods and options.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Callback function to filter non-multidimensional theme mods and options.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * If switch_to_blog() was called after the preview() method, and the current
</span><span class="cx" style="display: block; padding: 0 10px">         * blog is now not the same blog, then this method does a no-op and returns
</span><span class="cx" style="display: block; padding: 0 10px">         * the original value.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.4.0
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @uses WP_Customize_Setting::multidimensional_replace()
</del><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param mixed $original Old value.
</span><span class="cx" style="display: block; padding: 0 10px">         * @return mixed New or old value.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -224,15 +343,63 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        return $original;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $undefined = new stdClass(); // symbol hack
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $undefined = new stdClass(); // Symbol hack.
</ins><span class="cx" style="display: block; padding: 0 10px">                 $post_value = $this->post_value( $undefined );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( $undefined === $post_value ) {
-                       $value = $this->_original_value;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( $undefined !== $post_value ) {
+                       $value = $post_value;
</ins><span class="cx" style="display: block; padding: 0 10px">                 } else {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $value = $post_value;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 /*
+                        * Note that we don't use $original here because preview() will
+                        * not add the filter in the first place if it has an initial value
+                        * and there is no post value.
+                        */
+                       $value = $this->default;
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                return $value;
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                return $this->multidimensional_replace( $original, $this->id_data['keys'], $value );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ /**
+        * Callback function to filter multidimensional theme mods and options.
+        *
+        * For all multidimensional settings of a given type, the preview filter for
+        * the first setting previewed will be used to apply the values for the others.
+        *
+        * @since 4.4.0
+        * @access public
+        *
+        * @see WP_Customize_Setting::$aggregated_multidimensionals
+        * @param mixed $original Original root value.
+        * @return mixed New or old value.
+        */
+       public function _multidimensional_preview_filter( $original ) {
+               if ( ! $this->is_current_blog_previewed() ) {
+                       return $original;
+               }
+
+               $id_base = $this->id_data['base'];
+
+               // If no settings have been previewed yet (which should not be the case, since $this is), just pass through the original value.
+               if ( empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] ) ) {
+                       return $original;
+               }
+
+               foreach ( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] as $previewed_setting ) {
+                       // Skip applying previewed value for any settings that have already been applied.
+                       if ( ! empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['preview_applied_instances'][ $previewed_setting->id ] ) ) {
+                               continue;
+                       }
+
+                       // Do the replacements of the posted/default sub value into the root value.
+                       $value = $previewed_setting->post_value( $previewed_setting->default );
+                       $root = self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['root_value'];
+                       $root = $previewed_setting->multidimensional_replace( $root, $previewed_setting->id_data['keys'], $value );
+                       self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['root_value'] = $root;
+
+                       // Mark this setting having been applied so that it will be skipped when the filter is called again.
+                       self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['preview_applied_instances'][ $previewed_setting->id ] = true;
+               }
+
+               return self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -299,6 +466,57 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Get the root value for a setting, especially for multidimensional ones.
+        *
+        * @since 4.4.0
+        * @access protected
+        *
+        * @param mixed $default Value to return if root does not exist.
+        * @return mixed
+        */
+       protected function get_root_value( $default = null ) {
+               $id_base = $this->id_data['base'];
+               if ( 'option' === $this->type ) {
+                       return get_option( $id_base, $default );
+               } else if ( 'theme_mod' ) {
+                       return get_theme_mod( $id_base, $default );
+               } else {
+                       /*
+                        * Any WP_Customize_Setting subclass implementing aggregate multidimensional
+                        * will need to override this method to obtain the data from the appropriate
+                        * location.
+                        */
+                       return $default;
+               }
+       }
+
+       /**
+        * Set the root value for a setting, especially for multidimensional ones.
+        *
+        * @since 4.4.0
+        * @access protected
+        *
+        * @param mixed $value Value to set as root of multidimensional setting.
+        * @return bool Whether the multidimensional root was updated successfully.
+        */
+       protected function set_root_value( $value ) {
+               $id_base = $this->id_data['base'];
+               if ( 'option' === $this->type ) {
+                       return update_option( $id_base, $value );
+               } else if ( 'theme_mod' ) {
+                       set_theme_mod( $id_base, $value );
+                       return true;
+               } else {
+                       /*
+                        * Any WP_Customize_Setting subclass implementing aggregate multidimensional
+                        * will need to override this method to obtain the data from the appropriate
+                        * location.
+                        */
+                       return false;
+               }
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Save the value of the setting, using the related API.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.4.0
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -307,72 +525,52 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @return bool The result of saving the value.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        protected function update( $value ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                switch ( $this->type ) {
-                       case 'theme_mod' :
-                               $this->_update_theme_mod( $value );
-                               return true;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $id_base = $this->id_data['base'];
+               if ( 'option' === $this->type || 'theme_mod' === $this->type ) {
+                       if ( ! $this->is_multidimensional_aggregated ) {
+                               return $this->set_root_value( $value );
+                       } else {
+                               $root = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
+                               $root = $this->multidimensional_replace( $root, $this->id_data['keys'], $value );
+                               self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'] = $root;
+                               return $this->set_root_value( $root );
+                       }
+               } else {
+                       /**
+                        * Fires when the {@see WP_Customize_Setting::update()} method is called for settings
+                        * not handled as theme_mods or options.
+                        *
+                        * The dynamic portion of the hook name, `$this->type`, refers to the type of setting.
+                        *
+                        * @since 3.4.0
+                        *
+                        * @param mixed                $value Value of the setting.
+                        * @param WP_Customize_Setting $this  WP_Customize_Setting instance.
+                        */
+                       do_action( "customize_update_{$this->type}", $value, $this );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        case 'option' :
-                               return $this->_update_option( $value );
-
-                       default :
-
-                               /**
-                                * Fires when the {@see WP_Customize_Setting::update()} method is called for settings
-                                * not handled as theme_mods or options.
-                                *
-                                * The dynamic portion of the hook name, `$this->type`, refers to the type of setting.
-                                *
-                                * @since 3.4.0
-                                *
-                                * @param mixed                $value Value of the setting.
-                                * @param WP_Customize_Setting $this  WP_Customize_Setting instance.
-                                */
-                               do_action( "customize_update_{$this->type}", $value, $this );
-
-                               return has_action( "customize_update_{$this->type}" );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 return has_action( "customize_update_{$this->type}" );
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * Update the theme mod from the value of the parameter.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Deprecated method.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.4.0
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         *
-        * @param mixed $value The value to update.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @deprecated 4.4.0 Deprecated in favor of update() method.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        protected function _update_theme_mod( $value ) {
-               // Handle non-array theme mod.
-               if ( empty( $this->id_data[ 'keys' ] ) ) {
-                       set_theme_mod( $this->id_data[ 'base' ], $value );
-                       return;
-               }
-               // Handle array-based theme mod.
-               $mods = get_theme_mod( $this->id_data[ 'base' ] );
-               $mods = $this->multidimensional_replace( $mods, $this->id_data[ 'keys' ], $value );
-               if ( isset( $mods ) ) {
-                       set_theme_mod( $this->id_data[ 'base' ], $mods );
-               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ protected function _update_theme_mod() {
+               _deprecated_function( __METHOD__, '4.4.0', __CLASS__ . '::update()' );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * Update the option from the value of the setting.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Deprecated method.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 3.4.0
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         *
-        * @param mixed $value The value to update.
-        * @return bool The result of saving the value.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @deprecated 4.4.0 Deprecated in favor of update() method.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        protected function _update_option( $value ) {
-               // Handle non-array option.
-               if ( empty( $this->id_data[ 'keys' ] ) )
-                       return update_option( $this->id_data[ 'base' ], $value );
-
-               // Handle array-based options.
-               $options = get_option( $this->id_data[ 'base' ] );
-               $options = $this->multidimensional_replace( $options, $this->id_data[ 'keys' ], $value );
-               if ( isset( $options ) )
-                       return update_option( $this->id_data[ 'base' ], $options );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ protected function _update_option() {
+               _deprecated_function( __METHOD__, '4.4.0', __CLASS__ . '::update()' );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -383,39 +581,33 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @return mixed The value.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function value() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Get the callback that corresponds to the setting type.
-               switch( $this->type ) {
-                       case 'theme_mod' :
-                               $function = 'get_theme_mod';
-                               break;
-                       case 'option' :
-                               $function = 'get_option';
-                               break;
-                       default :
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $id_base = $this->id_data['base'];
+               $is_core_type = ( 'option' === $this->type || 'theme_mod' === $this->type );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                /**
-                                * Filter a Customize setting value not handled as a theme_mod or option.
-                                *
-                                * The dynamic portion of the hook name, `$this->id_date['base']`, refers to
-                                * the base slug of the setting name.
-                                *
-                                * For settings handled as theme_mods or options, see those corresponding
-                                * functions for available hooks.
-                                *
-                                * @since 3.4.0
-                                *
-                                * @param mixed $default The setting default value. Default empty.
-                                */
-                               return apply_filters( 'customize_value_' . $this->id_data[ 'base' ], $this->default );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( ! $is_core_type && ! $this->is_multidimensional_aggregated ) {
+                       $value = $this->get_root_value( $this->default );
+
+                       /**
+                        * Filter a Customize setting value not handled as a theme_mod or option.
+                        *
+                        * The dynamic portion of the hook name, `$this->id_date['base']`, refers to
+                        * the base slug of the setting name.
+                        *
+                        * For settings handled as theme_mods or options, see those corresponding
+                        * functions for available hooks.
+                        *
+                        * @since 3.4.0
+                        *
+                        * @param mixed $default The setting default value. Default empty.
+                        */
+                       $value = apply_filters( "customize_value_{$id_base}", $value );
+               } else if ( $this->is_multidimensional_aggregated ) {
+                       $root_value = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
+                       $value = $this->multidimensional_get( $root_value, $this->id_data['keys'], $this->default );
+               } else {
+                       $value = $this->get_root_value( $this->default );
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
-               // Handle non-array value
-               if ( empty( $this->id_data[ 'keys' ] ) )
-                       return $function( $this->id_data[ 'base' ], $this->default );
-
-               // Handle array-based value
-               $values = $function( $this->id_data[ 'base' ] );
-               return $this->multidimensional_get( $values, $this->id_data[ 'keys' ], $this->default );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         return $value;
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -520,7 +712,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @param $root
</span><span class="cx" style="display: block; padding: 0 10px">         * @param $keys
</span><span class="cx" style="display: block; padding: 0 10px">         * @param mixed $value The value to update.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @return
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @return mixed
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        final protected function multidimensional_replace( $root, $keys, $value ) {
</span><span class="cx" style="display: block; padding: 0 10px">                if ( ! isset( $value ) )
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -853,7 +1045,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * Get the instance data for a given widget setting.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Get the instance data for a given nav_menu_item setting.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.3.0
</span><span class="cx" style="display: block; padding: 0 10px">         * @access public
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -987,15 +1179,25 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Handle previewing the setting.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.3.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 4.4.0 Added boolean return value.
</ins><span class="cx" style="display: block; padding: 0 10px">          * @access public
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @see WP_Customize_Manager::post_value()
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         *
+        * @return bool False if method short-circuited due to no-op.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        public function preview() {
</span><span class="cx" style="display: block; padding: 0 10px">                if ( $this->is_previewed ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        return;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 return false;
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $undefined = new stdClass();
+               $is_placeholder = ( $this->post_id < 0 );
+               $is_dirty = ( $undefined !== $this->post_value( $undefined ) );
+               if ( ! $is_placeholder && ! $is_dirty ) {
+                       return false;
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->is_previewed              = true;
</span><span class="cx" style="display: block; padding: 0 10px">                $this->_original_value           = $this->value();
</span><span class="cx" style="display: block; padding: 0 10px">                $this->original_nav_menu_term_id = $this->_original_value['nav_menu_term_id'];
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1009,6 +1211,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // @todo Add get_post_metadata filters for plugins to add their data.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               return true;
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1621,15 +1825,25 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Handle previewing the setting.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.3.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 4.4.0 Added boolean return value
</ins><span class="cx" style="display: block; padding: 0 10px">          * @access public
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @see WP_Customize_Manager::post_value()
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         *
+        * @return bool False if method short-circuited due to no-op.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        public function preview() {
</span><span class="cx" style="display: block; padding: 0 10px">                if ( $this->is_previewed ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        return;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 return false;
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $undefined = new stdClass();
+               $is_placeholder = ( $this->term_id < 0 );
+               $is_dirty = ( $undefined !== $this->post_value( $undefined ) );
+               if ( ! $is_placeholder && ! $is_dirty ) {
+                       return false;
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->is_previewed       = true;
</span><span class="cx" style="display: block; padding: 0 10px">                $this->_original_value    = $this->value();
</span><span class="cx" style="display: block; padding: 0 10px">                $this->_previewed_blog_id = get_current_blog_id();
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1638,6 +1852,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                add_filter( 'wp_get_nav_menu_object', array( $this, 'filter_wp_get_nav_menu_object' ), 10, 2 );
</span><span class="cx" style="display: block; padding: 0 10px">                add_filter( 'default_option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_filter( 'option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               return true;
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span></span></pre></div>
<a id="trunktestsphpunittestscustomizesettingphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/customize/setting.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/customize/setting.php   2015-10-10 06:50:35 UTC (rev 35006)
+++ trunk/tests/phpunit/tests/customize/setting.php     2015-10-10 09:05:04 UTC (rev 35007)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -102,7 +102,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $name, $this->undefined ) );
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertEquals( $default, $setting->value() );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $setting->preview();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $this->assertTrue( $setting->preview(), 'Preview should not no-op since setting has no existing value.' );
</ins><span class="cx" style="display: block; padding: 0 10px">                         $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 ) );
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertEquals( $default, $setting->value() );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -114,18 +114,18 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name ) );
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertEquals( $initial_value, $setting->value() );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $setting->preview();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $this->assertFalse( $setting->preview(), 'Preview should no-op since setting value was extant and no post value was present.' );
</ins><span class="cx" style="display: block; padding: 0 10px">                         $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods)
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods)
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name ) );
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertEquals( $initial_value, $setting->value() );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        // @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
</del><span class="cx" style="display: block; padding: 0 10px">                         $overridden_value = "overridden_value_$name";
</span><span class="cx" style="display: block; padding: 0 10px">                        call_user_func( $type_options['setter'], $name, $overridden_value );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name ) );
-                       $this->assertEquals( $initial_value, $setting->value() );
-                       $this->assertNotEquals( $overridden_value, $setting->value() );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $message = 'Initial value should be overridden because initial preview() was no-op due to setting having existing value and/or post value was absent.';
+                       $this->assertEquals( $overridden_value, call_user_func( $type_options['getter'], $name ), $message );
+                       $this->assertEquals( $overridden_value, $setting->value(), $message );
+                       $this->assertNotEquals( $initial_value, $setting->value(), $message );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        // Non-multidimensional: Test unset setting being overridden by a post value
</span><span class="cx" style="display: block; padding: 0 10px">                        $name = "unset_{$type}_overridden";
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -133,7 +133,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $name, $this->undefined ) );
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertEquals( $default, $setting->value() );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $setting->preview(); // activate post_data
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $this->assertTrue( $setting->preview(), 'Preview applies because setting has post_data_overrides.' ); // activate post_data
</ins><span class="cx" style="display: block; padding: 0 10px">                         $this->assertEquals( $this->post_data_overrides[ $name ], call_user_func( $type_options['getter'], $name, $this->undefined ) );
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertEquals( $this->post_data_overrides[ $name ], $setting->value() );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -145,7 +145,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertEquals( $initial_value, call_user_func( $type_options['getter'], $name, $this->undefined ) );
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertEquals( $initial_value, $setting->value() );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $setting->preview(); // activate post_data
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $this->assertTrue( $setting->preview(), 'Preview applies because setting has post_data_overrides.' ); // activate post_data
</ins><span class="cx" style="display: block; padding: 0 10px">                         $this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods)
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods)
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertEquals( $this->post_data_overrides[ $name ], call_user_func( $type_options['getter'], $name, $this->undefined ) );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -167,7 +167,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $base_name, $this->undefined ) );
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertEquals( $default, $setting->value() );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $setting->preview();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $this->assertTrue( $setting->preview() );
</ins><span class="cx" style="display: block; padding: 0 10px">                         $base_value = call_user_func( $type_options['getter'], $base_name, $this->undefined );
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertArrayHasKey( 'foo', $base_value );
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->assertEquals( $default, $base_value['foo'] );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -311,8 +311,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $initial_value, $this->custom_type_getter( $name, $this->undefined ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $initial_value, $setting->value() );
</span><span class="cx" style="display: block; padding: 0 10px">                $setting->preview();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) );
-               $this->assertEquals( 2, did_action( "customize_preview_{$setting->type}" ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $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.' );
+               $this->assertEquals( 1, did_action( "customize_preview_{$setting->type}" ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertEquals( $initial_value, $this->custom_type_getter( $name, $this->undefined ) ); // should be same as above
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $initial_value, $setting->value() ); // should be same as above
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -325,8 +325,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $this->undefined, $this->custom_type_getter( $name, $this->undefined ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $default, $setting->value() );
</span><span class="cx" style="display: block; padding: 0 10px">                $setting->preview();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) );
-               $this->assertEquals( 3, did_action( "customize_preview_{$setting->type}" ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $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.' );
+               $this->assertEquals( 2, did_action( "customize_preview_{$setting->type}" ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertEquals( $post_data_overrides[ $name ], $this->custom_type_getter( $name, $this->undefined ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $post_data_overrides[ $name ], $setting->value() );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -342,7 +342,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $initial_value, $setting->value() );
</span><span class="cx" style="display: block; padding: 0 10px">                $setting->preview();
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( 1, did_action( "customize_preview_{$setting->id}" ) );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertEquals( 4, did_action( "customize_preview_{$setting->type}" ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertEquals( 3, did_action( "customize_preview_{$setting->type}" ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $this->assertEquals( $post_data_overrides[ $name ], $this->custom_type_getter( $name, $this->undefined ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $post_data_overrides[ $name ], $setting->value() );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre>
</div>
</div>

</body>
</html>