[wp-trac] [WordPress Trac] #30936: Dynamically create WP_Customize_Settings for settings created on JS client

WordPress Trac noreply at wordpress.org
Wed Jan 7 06:46:16 UTC 2015


#30936: Dynamically create WP_Customize_Settings for settings created on JS client
-------------------------+-----------------------------
 Reporter:  westonruter  |       Owner:
     Type:  enhancement  |      Status:  new
 Priority:  normal       |   Milestone:  Future Release
Component:  Customize    |     Version:  3.9
 Severity:  normal       |  Resolution:
 Keywords:               |     Focuses:
-------------------------+-----------------------------
Description changed by westonruter:

Old description:

> When developing the Widget Customizer plugin, there was some hackery
> needed to support previewing the addition of new widgets. Normally when
> Widget Customizer boots up it creates a `WP_Customize_Setting` for each
> widget instance that exists in the DB.
>
> Another problem is that `widgets_init` action also fires before
> `customize_register`, so it is too late to call `$setting->preview()`
> anyway to ensure that added widgets get registered.
>
> To account for these issues, I implemented an ugly “pre-preview” system
> to intercept the incoming `$_POST['customized']` JSON and basically
> duplicate the `WP_Customize_Setting::preview()` logic to make sure that
> the newly-added widgets were being supplied via filters when
> `widgets_init` happened, and then when `customize_register` happened it
> could remove those filters, and then add the `WP_Customize_Setting`
> objects properly.
>
> This is all now in Core, and it is hacky.
>
> I've been working on an alternate solution which would clean up Widget
> Customizer in core, and will be very helpful for Menu Customizer feature-
> as-plugin, in addition to other plugins (e.g. Customize Posts) that
> dynamically create settings on the client.
>
> The work I've been doing is currently part of the introduction
> transactions for the Customizer, but the logic could be extracted to
> apply to the current system which relies on inspecting settings sent via
> `$_POST[customized]`.
>
> The work can be seen here: https://github.com/xwp/wordpress-
> develop/pull/61/files
>
> The main piece is this new
> `WP_Customize_Manager::add_dynamic_settings()`:
>
> {{{#!php
> final class WP_Customize_Manager {
>
>         /* ... */
>         public function __construct()  {
>                 /* ... */
>                 add_action( 'customize_register',  array( $this,
> 'register_controls' ) );
>                 add_action( 'customize_register',  array( $this,
> 'register_dynamic_settings' ), 11 ); // allow code to create settings
> first
>                 /* ... */
>         }
>
>         /* ... */
>
>         /**
>          * Register any dynamically-created settings, such as those in a
> transaction that have no corresponding setting created.
>          *
>          * This is a mechanism to "wake up" settings that have been
> dynamically created
>          * on the frontend and have been added to a transaction. When the
> transaction is
>          * loaded, the dynamically-created settings then will get created
> and previewed
>          * even though they are not directly created statically with
> code.
>          *
>          * @todo $customized should store more than just key/value, but
> also serialized settings. The update_transaction call should include the
> setting configs.
>          *
>          * @param array $customized mapping of settings IDs to values
>          * @return WP_Customize_Setting[]
>          */
>         public function add_dynamic_settings( $customized ) {
>                 $new_settings = array();
>                 foreach ( $customized as $setting_id => $value ) {
>                         if ( isset( $this->settings[ $setting_id ] ) ||
> $this->get_setting( $setting_id ) ) {
>                                 continue;
>                         }
>                         $setting_class = 'WP_Customize_Setting';
>                         $args = false;
>
>                         /**
>                          * Allow non-statically created settings to be
> constructed with custom WP_Customize_Setting subclass.
>                          *
>                          * @since 4.2.0
>                          *
>                          * @param string $class
>                          * @param string $setting_id
>                          */
>                         $setting_class = apply_filters(
> 'customize_dynamic_setting_class', $setting_class, $setting_id );
>
>                         /**
>                          * Filter a dynamic setting's constructor args.
>                          *
>                          * This filter must return an array, overriding
> the false default, to be
>                          *
>                          * @since 4.2.0
>                          *
>                          * @param false|array $args
>                          * @param string $setting_id
>                          */
>                         $setting_args = apply_filters(
> 'customize_dynamic_setting_args', $args, $setting_id );
>
>                         if ( false === $setting_args ) {
>                                 continue;
>                         }
>                         $setting = new $setting_class( $this,
> $setting_id, $setting_args );
>                         $this->add_setting( $setting );
>                         $new_settings[] = $setting;
>                 }
>                 return $new_settings;
>         }
>
>         /* ... */
>
>         /**
>          * Add settings in the transaction that were not added with code,
> e.g. dynamically-created settings for Widgets
>          *
>          * @since 4.2.0
>          */
>         public function register_dynamic_settings() {
>                 // note here I'm using a transaction, but it could
> instead use json_decode( wp_unslash( $_POST['customized'] ) )
>                 $this->add_dynamic_settings( $this->transaction->data()
> );
>         }
> }
> }}}
>
> So then for how this is actually used, see `WP_Customize_Widgets`:
>
> {{{#!php
> final class WP_Customize_Widgets {
>         /* ... */
>
>         /**
>          * Mapping of setting type to setting ID pattern.
>          *
>          * @since 4.2.0
>          * @access protected
>          * @var array
>          */
>         protected $setting_id_patterns = array(
>                 'widget_instance' => '/^(widget_.+?)(?:\[(\d+)\])?$/',
>                 'sidebar_widgets' => '/^sidebars_widgets\[(.+?)\]$/',
>         );
>
>         /* ... */
>         public function __construct( $manager ) {
>                 $this->manager = $manager;
>
>                 add_filter( 'customize_dynamic_setting_args', array(
> $this, 'filter_customize_dynamic_setting_args' ), 10, 2 );
>                 add_action( 'after_setup_theme', array( $this,
> 'register_settings' ) );
>                 /* ... */
>         }
>
>         /* ... */
>
>         /**
>          * Get the widget setting type given a setting ID.
>          *
>          * @since 4.2.0
>          *
>          * @param $setting_id
>          *
>          * @return string|null
>          */
>         protected function get_setting_type( $setting_id ) {
>                 static $cache = array();
>                 if ( isset( $cache[ $setting_id ] ) ) {
>                         return $cache[ $setting_id ];
>                 }
>                 foreach ( $this->setting_id_patterns as $type => $pattern
> ) {
>                         if ( preg_match( $pattern, $setting_id ) ) {
>                                 $cache[ $setting_id ] = $type;
>                                 return $type;
>                         }
>                 }
>                 return null;
>         }
>
>         /**
>          * Inspect the transaction for any widget settings, and
> dynamically add them up-front so widgets will be initialized properly.
>          *
>          * @since 4.2.0
>          */
>         public function register_settings() {
>                 $widget_customized = array();
>                 $all_customized = $this->manager->transaction->data(); //
> or this could get from json_decode( wp_unslash( $_POST['customized'] ) )
>                 foreach ( $all_customized as $setting_id => $value ) {
>                         if ( $this->get_setting_type( $setting_id ) ) {
>                                 $widget_customized[ $setting_id ] =
> $value;
>                         }
>                 }
>
>                 $settings = $this->manager->add_dynamic_settings(
> $widget_customized );
>
>                 /*
>                  * Preview settings right away so that widgets and
> sidebars will get registered properly.
>                  * But don't do this if a customize_save because this
> will cause WP to think there is nothing
>                  * changed that needs to be saved.
>                  */
>                 if ( ! $this->manager->doing_ajax( 'customize_save' ) ) {
>                         foreach ( $settings as $setting ) {
>                                 $setting->preview();
>                         }
>                 }
>         }
>
>         /* ... */
>
>         /**
>          * Determine the arguments for a dynamically-created setting.
> This is used
>          * when updating the transaction.
>          *
>          * @since 4.2.0
>          *
>          * @param false|array $args
>          * @param string $setting_id
>          * @return false|array
>          */
>         public function filter_customize_dynamic_setting_args( $args,
> $setting_id ) {
>                 if ( $this->get_setting_type( $setting_id ) ) {
>                         $args = $this->get_setting_args( $setting_id );
>                 }
>                 return $args;
>         }
> }
> }}}
>
> So you can see here that Widget Customizer is updated to register
> settings up front, eliminating the need for pre-preview. There is
> actually no need to defer settings to be created at `customize_register`.
> Additionally, the current `customized` data (aka the transaction) is
> looked at and any widget-specific settings are picked out and created as
> settings. This will ensure that added widgets will get recognized in time
> for `widgets_Init` since the filters would be applying.
>
> So as long as dynamically-created settings have IDs that follow a certain
> pattern (e.g. `scheduled_background_colors[2015-03-01]` or `term[421]`),
> then new settings can be created for them on the JS client and they'll be
> automatically created in PHP when the customized data is received if a
> filters are added such as:
>
> {{{#!php
> add_filter( 'customize_dynamic_setting_args', function ( $args,
> $setting_id ) {
>         if ( preg_match( '/^scheduled_background_colors\[.+?\]$/',
> $setting_id ) ) {
>                 $args = array( 'type' => 'option', 'transport' =>
> 'postMessage' );
>         } else if ( preg_match( '/^term\[\d+\]$/', $setting_id ) ) {
>                 $args = array( 'type' => 'term',  );
>         }
>         return $args;
> }, 10, 2 );
> }}}
>
> This would accompany any `$wp_customize->add_setting()` calls for
> existing data in the `customize_register` action.
>
> This is distinct from #28580.

New description:

 When developing the Widget Customizer plugin, there was some hackery
 needed to support previewing the addition of new widgets. Normally when
 Widget Customizer boots up it creates a `WP_Customize_Setting` for each
 widget instance that exists in the DB.

 Another problem is that `widgets_init` action also fires before
 `customize_register`, so it is too late to call `$setting->preview()`
 anyway to ensure that added widgets get registered.

 To account for these issues, I implemented an ugly “pre-preview” system to
 intercept the incoming `$_POST['customized']` JSON and basically duplicate
 the `WP_Customize_Setting::preview()` logic to make sure that the newly-
 added widgets were being supplied via filters when `widgets_init`
 happened, and then when `customize_register` happened it could remove
 those filters, and then add the `WP_Customize_Setting` objects properly.

 This is all now in Core, and it is hacky.

 I've been working on an alternate solution which would clean up Widget
 Customizer in core, and will be very helpful for Menu Customizer feature-
 as-plugin, in addition to other plugins (e.g. Customize Posts) that
 dynamically create settings on the client.

 The work I've been doing is currently part of the introduction
 transactions for the Customizer, but the logic could be extracted to apply
 to the current system which relies on inspecting settings sent via
 `$_POST[customized]`.

 The work can be seen here: https://github.com/xwp/wordpress-
 develop/pull/61/files

 The main piece is this new `WP_Customize_Manager::add_dynamic_settings()`:

 {{{#!php
 <?php
 final class WP_Customize_Manager {

         /* ... */
         public function __construct()  {
                 /* ... */
                 add_action( 'customize_register',  array( $this,
 'register_controls' ) );
                 add_action( 'customize_register',  array( $this,
 'register_dynamic_settings' ), 11 ); // allow code to create settings
 first
                 /* ... */
         }

         /* ... */

         /**
          * Register any dynamically-created settings, such as those in a
 transaction that have no corresponding setting created.
          *
          * This is a mechanism to "wake up" settings that have been
 dynamically created
          * on the frontend and have been added to a transaction. When the
 transaction is
          * loaded, the dynamically-created settings then will get created
 and previewed
          * even though they are not directly created statically with code.
          *
          * @todo $customized should store more than just key/value, but
 also serialized settings. The update_transaction call should include the
 setting configs.
          *
          * @param array $customized mapping of settings IDs to values
          * @return WP_Customize_Setting[]
          */
         public function add_dynamic_settings( $customized ) {
                 $new_settings = array();
                 foreach ( $customized as $setting_id => $value ) {
                         if ( isset( $this->settings[ $setting_id ] ) ||
 $this->get_setting( $setting_id ) ) {
                                 continue;
                         }
                         $setting_class = 'WP_Customize_Setting';
                         $args = false;

                         /**
                          * Allow non-statically created settings to be
 constructed with custom WP_Customize_Setting subclass.
                          *
                          * @since 4.2.0
                          *
                          * @param string $class
                          * @param string $setting_id
                          */
                         $setting_class = apply_filters(
 'customize_dynamic_setting_class', $setting_class, $setting_id );

                         /**
                          * Filter a dynamic setting's constructor args.
                          *
                          * This filter must return an array, overriding
 the false default, to be
                          *
                          * @since 4.2.0
                          *
                          * @param false|array $args
                          * @param string $setting_id
                          */
                         $setting_args = apply_filters(
 'customize_dynamic_setting_args', $args, $setting_id );

                         if ( false === $setting_args ) {
                                 continue;
                         }
                         $setting = new $setting_class( $this, $setting_id,
 $setting_args );
                         $this->add_setting( $setting );
                         $new_settings[] = $setting;
                 }
                 return $new_settings;
         }

         /* ... */

         /**
          * Add settings in the transaction that were not added with code,
 e.g. dynamically-created settings for Widgets
          *
          * @since 4.2.0
          */
         public function register_dynamic_settings() {
                 // note here I'm using a transaction, but it could instead
 use json_decode( wp_unslash( $_POST['customized'] ) )
                 $this->add_dynamic_settings( $this->transaction->data() );
         }
 }
 }}}

 So then for how this is actually used, see `WP_Customize_Widgets`:

 {{{#!php
 <?php
 final class WP_Customize_Widgets {
         /* ... */

         /**
          * Mapping of setting type to setting ID pattern.
          *
          * @since 4.2.0
          * @access protected
          * @var array
          */
         protected $setting_id_patterns = array(
                 'widget_instance' => '/^(widget_.+?)(?:\[(\d+)\])?$/',
                 'sidebar_widgets' => '/^sidebars_widgets\[(.+?)\]$/',
         );

         /* ... */
         public function __construct( $manager ) {
                 $this->manager = $manager;

                 add_filter( 'customize_dynamic_setting_args', array(
 $this, 'filter_customize_dynamic_setting_args' ), 10, 2 );
                 add_action( 'after_setup_theme', array( $this,
 'register_settings' ) );
                 /* ... */
         }

         /* ... */

         /**
          * Get the widget setting type given a setting ID.
          *
          * @since 4.2.0
          *
          * @param $setting_id
          *
          * @return string|null
          */
         protected function get_setting_type( $setting_id ) {
                 static $cache = array();
                 if ( isset( $cache[ $setting_id ] ) ) {
                         return $cache[ $setting_id ];
                 }
                 foreach ( $this->setting_id_patterns as $type => $pattern
 ) {
                         if ( preg_match( $pattern, $setting_id ) ) {
                                 $cache[ $setting_id ] = $type;
                                 return $type;
                         }
                 }
                 return null;
         }

         /**
          * Inspect the transaction for any widget settings, and
 dynamically add them up-front so widgets will be initialized properly.
          *
          * @since 4.2.0
          */
         public function register_settings() {
                 $widget_customized = array();
                 $all_customized = $this->manager->transaction->data(); //
 or this could get from json_decode( wp_unslash( $_POST['customized'] ) )
                 foreach ( $all_customized as $setting_id => $value ) {
                         if ( $this->get_setting_type( $setting_id ) ) {
                                 $widget_customized[ $setting_id ] =
 $value;
                         }
                 }

                 $settings = $this->manager->add_dynamic_settings(
 $widget_customized );

                 /*
                  * Preview settings right away so that widgets and
 sidebars will get registered properly.
                  * But don't do this if a customize_save because this will
 cause WP to think there is nothing
                  * changed that needs to be saved.
                  */
                 if ( ! $this->manager->doing_ajax( 'customize_save' ) ) {
                         foreach ( $settings as $setting ) {
                                 $setting->preview();
                         }
                 }
         }

         /* ... */

         /**
          * Determine the arguments for a dynamically-created setting. This
 is used
          * when updating the transaction.
          *
          * @since 4.2.0
          *
          * @param false|array $args
          * @param string $setting_id
          * @return false|array
          */
         public function filter_customize_dynamic_setting_args( $args,
 $setting_id ) {
                 if ( $this->get_setting_type( $setting_id ) ) {
                         $args = $this->get_setting_args( $setting_id );
                 }
                 return $args;
         }
 }
 }}}

 So you can see here that Widget Customizer is updated to register settings
 up front, eliminating the need for pre-preview. There is actually no need
 to defer settings to be created at `customize_register`. Additionally, the
 current `customized` data (aka the transaction) is looked at and any
 widget-specific settings are picked out and created as settings. This will
 ensure that added widgets will get recognized in time for `widgets_Init`
 since the filters would be applying.

 So as long as dynamically-created settings have IDs that follow a certain
 pattern (e.g. `scheduled_background_colors[2015-03-01]` or `term[421]`),
 then new settings can be created for them on the JS client and they'll be
 automatically created in PHP when the customized data is received if a
 filters are added such as:

 {{{#!php
 <?php
 add_filter( 'customize_dynamic_setting_args', function ( $args,
 $setting_id ) {
         if ( preg_match( '/^scheduled_background_colors\[.+?\]$/',
 $setting_id ) ) {
                 $args = array( 'type' => 'option', 'transport' =>
 'postMessage' );
         } else if ( preg_match( '/^term\[\d+\]$/', $setting_id ) ) {
                 $args = array( 'type' => 'term',  );
         }
         return $args;
 }, 10, 2 );
 }}}

 This would accompany any `$wp_customize->add_setting()` calls for existing
 data in the `customize_register` action.

 This is distinct from #28580.

--

--
Ticket URL: <https://core.trac.wordpress.org/ticket/30936#comment:1>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform


More information about the wp-trac mailing list