[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