HPOS Features: Revert to one feature (#39525)

Co-authored-by: Vedanshu Jain <vedanshu.jain.2012@gmail.com>
This commit is contained in:
Corey McKrill 2023-10-03 02:34:41 -07:00 committed by GitHub
parent a9c8486b53
commit b9bfbcdc42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 589 additions and 413 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Consolidate HPOS back into a single "feature" for the purposes of showing it on the Features settings screen.

View File

@ -8,9 +8,7 @@
use Automattic\Jetpack\Constants; use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Admin\Notes\Notes; use Automattic\WooCommerce\Admin\Notes\Notes;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController; use Automattic\WooCommerce\Internal\DataStores\Orders\{ CustomOrdersTableController, DataSynchronizer, OrdersTableDataStore };
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Internal\Features\FeaturesController; use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator; use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories; use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
@ -491,11 +489,17 @@ class WC_Install {
$schema = self::get_schema(); $schema = self::get_schema();
$feature_controller = wc_get_container()->get( FeaturesController::class ); $hpos_settings = filter_var_array(
if ( array(
$feature_controller->feature_is_enabled( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION ) 'cot' => get_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ),
|| $feature_controller->feature_is_enabled( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ) 'data_sync' => get_option( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION ),
) { ),
array(
'cot' => FILTER_VALIDATE_BOOLEAN,
'data_sync' => FILTER_VALIDATE_BOOLEAN,
)
);
if ( in_array( true, $hpos_settings, true ) ) {
$schema .= wc_get_container() $schema .= wc_get_container()
->get( OrdersTableDataStore::class ) ->get( OrdersTableDataStore::class )
->get_database_schema(); ->get_database_schema();

View File

@ -5,7 +5,6 @@
namespace Automattic\WooCommerce\Internal\DataStores\Orders; namespace Automattic\WooCommerce\Internal\DataStores\Orders;
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Caches\OrderCache; use Automattic\WooCommerce\Caches\OrderCache;
use Automattic\WooCommerce\Caches\OrderCacheController; use Automattic\WooCommerce\Caches\OrderCacheController;
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController; use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;
@ -120,10 +119,9 @@ class CustomOrdersTableController {
self::add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 ); self::add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 );
self::add_filter( 'pre_update_option', array( $this, 'process_pre_update_option' ), 999, 3 ); self::add_filter( 'pre_update_option', array( $this, 'process_pre_update_option' ), 999, 3 );
self::add_action( 'woocommerce_after_register_post_type', array( $this, 'register_post_type_for_order_placeholders' ), 10, 0 ); self::add_action( 'woocommerce_after_register_post_type', array( $this, 'register_post_type_for_order_placeholders' ), 10, 0 );
self::add_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( $this, 'handle_feature_enabled_changed' ), 10, 2 );
self::add_action( 'woocommerce_feature_setting', array( $this, 'get_hpos_feature_setting' ), 10, 2 );
self::add_action( 'woocommerce_sections_advanced', array( $this, 'sync_now' ) ); self::add_action( 'woocommerce_sections_advanced', array( $this, 'sync_now' ) );
self::add_filter( 'removable_query_args', array( $this, 'register_removable_query_arg' ) ); self::add_filter( 'removable_query_args', array( $this, 'register_removable_query_arg' ) );
self::add_action( 'woocommerce_register_feature_definitions', array( $this, 'add_feature_definition' ) );
} }
/** /**
@ -295,12 +293,19 @@ class CustomOrdersTableController {
return $value; return $value;
} }
if ( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION !== $option || $value === $old_value || false === $old_value ) { if ( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION !== $option || 'no' === $value ) {
return $value; return $value;
} }
$this->order_cache->flush(); $this->order_cache->flush();
if ( ! $this->data_synchronizer->check_orders_table_exists() ) {
$this->data_synchronizer->create_database_tables();
}
$tables_created = get_option( DataSynchronizer::ORDERS_TABLE_CREATED ) === 'yes';
if ( ! $tables_created ) {
return 'no';
}
/** /**
* Re-enable the following code once the COT to posts table sync is implemented (it's currently commented out to ease testing). * Re-enable the following code once the COT to posts table sync is implemented (it's currently commented out to ease testing).
$sync_is_pending = 0 !== $this->data_synchronizer->get_current_orders_pending_sync_count(); $sync_is_pending = 0 !== $this->data_synchronizer->get_current_orders_pending_sync_count();
@ -341,26 +346,6 @@ class CustomOrdersTableController {
return $query_args; return $query_args;
} }
/**
* Handle the 'woocommerce_feature_enabled_changed' action,
* if the custom orders table feature is enabled create the database tables if they don't exist.
*
* @param string $feature_id The id of the feature that is being enabled or disabled.
* @param bool $is_enabled True if the feature is being enabled, false if it's being disabled.
*/
private function handle_feature_enabled_changed( $feature_id, $is_enabled ): void {
if ( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION !== $feature_id || ! $is_enabled ) {
return;
}
if ( ! $this->data_synchronizer->check_orders_table_exists() ) {
$success = $this->data_synchronizer->create_database_tables();
if ( ! $success ) {
update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' );
}
}
}
/** /**
* Handler for the woocommerce_after_register_post_type post, * Handler for the woocommerce_after_register_post_type post,
* registers the post type for placeholder orders. * registers the post type for placeholder orders.
@ -393,54 +378,72 @@ class CustomOrdersTableController {
} }
/** /**
* Returns the HPOS setting for rendering in Features section of the settings page. * Add the definition for the HPOS feature.
* *
* @param array $feature_setting HPOS feature value as defined in the feature controller. * @param FeaturesController $features_controller The instance of FeaturesController.
* @param string $feature_id ID of the feature.
* *
* @return array Feature setting object. * @return void
*/ */
private function get_hpos_feature_setting( array $feature_setting, string $feature_id ) { private function add_feature_definition( $features_controller ) {
if ( ! in_array( $feature_id, array( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, 'custom_order_tables' ), true ) ) { $definition = array(
return $feature_setting; 'option_key' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
} 'is_experimental' => false,
'enabled_by_default' => false,
'order' => 50,
'setting' => $this->get_hpos_setting_for_feature(),
'additional_settings' => array(
$this->get_hpos_setting_for_sync(),
),
);
if ( 'yes' === get_transient( 'wc_installing' ) ) { $features_controller->add_feature_definition(
return $feature_setting; 'custom_order_tables',
} __( 'High-Performance order storage', 'woocommerce' ),
$definition
$sync_status = $this->data_synchronizer->get_sync_status(); );
switch ( $feature_id ) {
case self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION:
return $this->get_hpos_setting_for_feature( $sync_status );
case DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION:
return $this->get_hpos_setting_for_sync( $sync_status );
case 'custom_order_tables':
return array();
}
} }
/** /**
* Returns the HPOS setting for rendering HPOS vs Post setting block in Features section of the settings page. * Returns the HPOS setting for rendering HPOS vs Post setting block in Features section of the settings page.
* *
* @param array $sync_status Details of sync status, includes pending count, and count when sync started.
*
* @return array Feature setting object. * @return array Feature setting object.
*/ */
private function get_hpos_setting_for_feature( $sync_status ) { private function get_hpos_setting_for_feature() {
$hpos_enabled = $this->custom_orders_table_usage_is_enabled(); if ( 'yes' === get_transient( 'wc_installing' ) ) {
$plugin_info = $this->features_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true ); return array();
$plugin_incompat_warning = $this->plugin_util->generate_incompatible_plugin_feature_warning( 'custom_order_tables', $plugin_info );
$sync_complete = 0 === $sync_status['current_pending_count'];
$disabled_option = array();
// Changing something here? might also want to look at `enable|disable` functions in CLIRunner.
if ( count( array_merge( $plugin_info['uncertain'], $plugin_info['incompatible'] ) ) > 0 ) {
$disabled_option = array( 'yes' );
}
if ( ! $sync_complete ) {
$disabled_option = array( 'yes', 'no' );
} }
$get_value = function() {
return $this->custom_orders_table_usage_is_enabled() ? 'yes' : 'no';
};
/**
* The FeaturesController instance must only be accessed from within the callback functions. Otherwise it
* gets called while it's still being instantiated and creates and endless loop.
*/
$get_desc = function() {
$plugin_compatibility = $this->features_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
return $this->plugin_util->generate_incompatible_plugin_feature_warning( 'custom_order_tables', $plugin_compatibility );
};
$get_disabled = function() {
$plugin_compatibility = $this->features_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
$sync_status = $this->data_synchronizer->get_sync_status();
$sync_complete = 0 === $sync_status['current_pending_count'];
$disabled = array();
// Changing something here? might also want to look at `enable|disable` functions in CLIRunner.
if ( count( array_merge( $plugin_compatibility['uncertain'], $plugin_compatibility['incompatible'] ) ) > 0 ) {
$disabled = array( 'yes' );
}
if ( ! $sync_complete ) {
$disabled = array( 'yes', 'no' );
}
return $disabled;
};
return array( return array(
'id' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'id' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
'title' => __( 'Order data storage', 'woocommerce' ), 'title' => __( 'Order data storage', 'woocommerce' ),
@ -449,9 +452,9 @@ class CustomOrdersTableController {
'no' => __( 'WordPress posts storage (legacy)', 'woocommerce' ), 'no' => __( 'WordPress posts storage (legacy)', 'woocommerce' ),
'yes' => __( 'High-performance order storage (recommended)', 'woocommerce' ), 'yes' => __( 'High-performance order storage (recommended)', 'woocommerce' ),
), ),
'value' => $hpos_enabled ? 'yes' : 'no', 'value' => $get_value,
'disabled' => $disabled_option, 'disabled' => $get_disabled,
'desc' => $plugin_incompat_warning, 'desc' => $get_desc,
'desc_at_end' => true, 'desc_at_end' => true,
'row_class' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'row_class' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
); );
@ -460,63 +463,74 @@ class CustomOrdersTableController {
/** /**
* Returns the setting for rendering sync enabling setting block in Features section of the settings page. * Returns the setting for rendering sync enabling setting block in Features section of the settings page.
* *
* @param array $sync_status Details of sync status, includes pending count, and count when sync started.
*
* @return array Feature setting object. * @return array Feature setting object.
*/ */
private function get_hpos_setting_for_sync( $sync_status ) { private function get_hpos_setting_for_sync() {
$sync_in_progress = $this->batch_processing_controller->is_enqueued( get_class( $this->data_synchronizer ) ); if ( 'yes' === get_transient( 'wc_installing' ) ) {
$sync_enabled = $this->data_synchronizer->data_sync_is_enabled(); return array();
$sync_message = array();
if ( ! $sync_enabled && $this->data_synchronizer->background_sync_is_enabled() ) {
$sync_message[] = __( 'Background sync is enabled.', 'woocommerce' );
} }
if ( $sync_in_progress && $sync_status['current_pending_count'] > 0 ) { $get_value = function() {
$sync_message[] = sprintf( return get_option( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION );
// translators: %d: number of pending orders. };
__( 'Currently syncing orders... %d pending', 'woocommerce' ),
$sync_status['current_pending_count']
);
} elseif ( $sync_status['current_pending_count'] > 0 ) {
$sync_now_url = add_query_arg(
array(
self::SYNC_QUERY_ARG => true,
),
wc_get_container()->get( FeaturesController::class )->get_features_page_url()
);
$sync_message[] = wp_kses_data( $get_sync_message = function() {
__( $sync_status = $this->data_synchronizer->get_sync_status();
'You can switch order data storage <strong>only when the posts and orders tables are in sync</strong>.', $sync_in_progress = $this->batch_processing_controller->is_enqueued( get_class( $this->data_synchronizer ) );
'woocommerce' $sync_enabled = $this->data_synchronizer->data_sync_is_enabled();
) $sync_message = array();
);
$sync_message[] = sprintf( if ( ! $sync_enabled && $this->data_synchronizer->background_sync_is_enabled() ) {
'<a href="%1$s" class="button button-link">%2$s</a>', $sync_message[] = __( 'Background sync is enabled.', 'woocommerce' );
esc_url( $sync_now_url ), }
sprintf(
if ( $sync_in_progress && $sync_status['current_pending_count'] > 0 ) {
$sync_message[] = sprintf(
// translators: %d: number of pending orders. // translators: %d: number of pending orders.
_n( __( 'Currently syncing orders... %d pending', 'woocommerce' ),
'Sync %s pending order', $sync_status['current_pending_count']
'Sync %s pending orders', );
$sync_status['current_pending_count'], } elseif ( $sync_status['current_pending_count'] > 0 ) {
'woocommerce' $sync_now_url = add_query_arg(
array(
self::SYNC_QUERY_ARG => true,
), ),
number_format_i18n( $sync_status['current_pending_count'] ) wc_get_container()->get( FeaturesController::class )->get_features_page_url()
) );
);
} $sync_message[] = wp_kses_data(
__(
'You can switch order data storage <strong>only when the posts and orders tables are in sync</strong>.',
'woocommerce'
)
);
$sync_message[] = sprintf(
'<a href="%1$s" class="button button-link">%2$s</a>',
esc_url( $sync_now_url ),
sprintf(
// translators: %d: number of pending orders.
_n(
'Sync %s pending order',
'Sync %s pending orders',
$sync_status['current_pending_count'],
'woocommerce'
),
number_format_i18n( $sync_status['current_pending_count'] )
)
);
}
return implode( '<br />', $sync_message );
};
return array( return array(
'id' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, 'id' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
'title' => '', 'title' => '',
'type' => 'checkbox', 'type' => 'checkbox',
'desc' => __( 'Enable compatibility mode (synchronizes orders to the posts table).', 'woocommerce' ), 'desc' => __( 'Enable compatibility mode (synchronizes orders to the posts table).', 'woocommerce' ),
'value' => $sync_enabled ? 'yes' : 'no', 'value' => $get_value,
'desc_tip' => implode( '<br />', $sync_message ), 'desc_tip' => $get_sync_message,
'row_class' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, 'row_class' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
); );
} }

View File

@ -5,12 +5,10 @@
namespace Automattic\WooCommerce\Internal\Features; namespace Automattic\WooCommerce\Internal\Features;
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Internal\Admin\Analytics; use Automattic\WooCommerce\Internal\Admin\Analytics;
use Automattic\WooCommerce\Admin\Features\Navigation\Init; use Automattic\WooCommerce\Admin\Features\Navigation\Init;
use Automattic\WooCommerce\Admin\Features\NewProductManagementExperience; use Automattic\WooCommerce\Admin\Features\NewProductManagementExperience;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController; use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Proxies\LegacyProxy; use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\ArrayUtil; use Automattic\WooCommerce\Utilities\ArrayUtil;
@ -34,28 +32,21 @@ class FeaturesController {
* *
* @var array[] * @var array[]
*/ */
private $features; private $features = array();
/** /**
* The registered compatibility info for WooCommerce plugins, with plugin names as keys. * The registered compatibility info for WooCommerce plugins, with plugin names as keys.
* *
* @var array * @var array
*/ */
private $compatibility_info_by_plugin; private $compatibility_info_by_plugin = array();
/**
* Ids of the legacy features (they existed before the features engine was implemented).
*
* @var array
*/
private $legacy_feature_ids;
/** /**
* The registered compatibility info for WooCommerce plugins, with feature ids as keys. * The registered compatibility info for WooCommerce plugins, with feature ids as keys.
* *
* @var array * @var array
*/ */
private $compatibility_info_by_feature; private $compatibility_info_by_feature = array();
/** /**
* The LegacyProxy instance to use. * The LegacyProxy instance to use.
@ -91,71 +82,6 @@ class FeaturesController {
* Creates a new instance of the class. * Creates a new instance of the class.
*/ */
public function __construct() { public function __construct() {
$hpos_enable_sync = DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION;
$hpos_authoritative = CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION;
$features = array(
'analytics' => array(
'name' => __( 'Analytics', 'woocommerce' ),
'description' => __( 'Enable WooCommerce Analytics', 'woocommerce' ),
'is_experimental' => false,
'enabled_by_default' => true,
'disable_ui' => false,
),
'new_navigation' => array(
'name' => __( 'Navigation', 'woocommerce' ),
'description' => __( 'Add the new WooCommerce navigation experience to the dashboard', 'woocommerce' ),
'is_experimental' => false,
'disable_ui' => false,
),
'product_block_editor' => array(
'name' => __( 'New product editor', 'woocommerce' ),
'description' => __( 'Try the new product editor (Beta)', 'woocommerce' ),
'is_experimental' => true,
'disable_ui' => false,
),
// Options for HPOS features are added in CustomOrdersTableController to keep the logic in same place.
'custom_order_tables' => array( // This exists for back-compat only, otherwise it's value is superseded by $hpos_authoritative option.
'name' => __( 'High-Performance Order Storage (HPOS)', 'woocommerce' ),
'enabled_by_default' => false,
),
$hpos_authoritative => array(
'name' => __( 'High-Performance Order Storage', 'woocommerce' ),
'order' => 10,
),
$hpos_enable_sync => array(
'name' => '',
'order' => 9,
),
'cart_checkout_blocks' => array(
'name' => __( 'Cart & Checkout Blocks', 'woocommerce' ),
'description' => __( 'Optimize for faster checkout', 'woocommerce' ),
'is_experimental' => false,
'disable_ui' => true,
),
'marketplace' => array(
'name' => __( 'Marketplace', 'woocommerce' ),
'description' => __(
'New, faster way to find extensions and themes for your WooCommerce store',
'woocommerce'
),
'is_experimental' => false,
'enabled_by_default' => true,
'disable_ui' => false,
),
);
$this->legacy_feature_ids = array(
'analytics',
'new_navigation',
'product_block_editor',
'marketplace',
// Compatibility for COT is determined by `custom_order_tables'.
CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
);
$this->init_features( $features );
self::add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 ); self::add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 );
self::add_filter( 'added_option', array( $this, 'process_added_option' ), 999, 3 ); self::add_filter( 'added_option', array( $this, 'process_added_option' ), 999, 3 );
self::add_filter( 'woocommerce_get_sections_advanced', array( $this, 'add_features_section' ), 10, 1 ); self::add_filter( 'woocommerce_get_sections_advanced', array( $this, 'add_features_section' ), 10, 1 );
@ -172,22 +98,139 @@ class FeaturesController {
} }
/** /**
* Initialize the class according to the existing features. * Register a feature.
* *
* @param array $features Information about the existing features. * This should be called during the `woocommerce_register_feature_definitions` action hook.
*
* @param string $slug The ID slug of the feature.
* @param string $name The name of the feature that will appear on the Features screen and elsewhere.
* @param array $args {
* Optional. Properties that make up the feature definition. Each of these properties can also be set as a
* callback function, as long as that function returns the specified type.
*
* @type array[] $additional_settings An array of definitions for additional settings controls related to
* the feature that will display on the Features screen. See the Settings API
* for the schema of these props.
* @type string $description A brief description of the feature, used as an input label if the feature
* setting is a checkbox.
* @type bool $disabled True to disable the setting field for this feature on the Features screen,
* so it can't be changed.
* @type bool $disable_ui Set to true to hide the setting field for this feature on the
* Features screen. Defaults to false.
* @type bool $enabled_by_default Set to true to have this feature by opt-out instead of opt-in.
* Defaults to false.
* @type bool $is_experimental Set to true to display this feature under the "Experimental" heading on
* the Features screen. Features set to experimental are also omitted from
* the features list in some cases. Defaults to true.
* @type bool $is_legacy Set to true if this feature existed before the FeaturesController class
* was introduced. Features set to legacy also do not produce warnings about
* incompatible plugins. Defaults to false.
* @type string $option_key The key name for the option that enables/disables the feature.
* @type int $order The order that the feature will appear in the list on the Features screen.
* Higher number = higher in the list. Defaults to 10.
* @type array $setting The properties used by the Settings API to render the setting control on
* the Features screen. See the Settings API for the schema of these props.
* }
*
* @return void
*/ */
private function init_features( array $features ) { public function add_feature_definition( $slug, $name, array $args = array() ) {
$this->compatibility_info_by_plugin = array(); $defaults = array(
$this->compatibility_info_by_feature = array(); 'disable_ui' => false,
'enabled_by_default' => false,
'is_experimental' => true,
'is_legacy' => false,
'name' => $name,
'order' => 10,
);
$args = wp_parse_args( $args, $defaults );
$this->features = $features; $this->features[ $slug ] = $args;
}
foreach ( array_keys( $this->features ) as $feature_id ) { /**
$this->compatibility_info_by_feature[ $feature_id ] = array( * Generate and cache the feature definitions.
'compatible' => array(), *
'incompatible' => array(), * @return array[]
*/
private function get_feature_definitions() {
if ( empty( $this->features ) ) {
$legacy_features = array(
'analytics' => array(
'name' => __( 'Analytics', 'woocommerce' ),
'description' => __( 'Enable WooCommerce Analytics', 'woocommerce' ),
'option_key' => Analytics::TOGGLE_OPTION_NAME,
'is_experimental' => false,
'enabled_by_default' => true,
'disable_ui' => false,
'is_legacy' => true,
),
'new_navigation' => array(
'name' => __( 'Navigation', 'woocommerce' ),
'description' => __( 'Add the new WooCommerce navigation experience to the dashboard', 'woocommerce' ),
'option_key' => Init::TOGGLE_OPTION_NAME,
'is_experimental' => false,
'disable_ui' => false,
'is_legacy' => true,
),
'product_block_editor' => array(
'name' => __( 'New product editor', 'woocommerce' ),
'description' => __( 'Try the new product editor (Beta)', 'woocommerce' ),
'is_experimental' => true,
'disable_ui' => false,
'is_legacy' => true,
'disabled' => function() {
return version_compare( get_bloginfo( 'version' ), '6.2', '<' );
},
'desc_tip' => function() {
$string = '';
if ( version_compare( get_bloginfo( 'version' ), '6.2', '<' ) ) {
$string = __( '⚠ This feature is compatible with WordPress version 6.2 or higher.', 'woocommerce' );
}
return $string;
},
),
'cart_checkout_blocks' => array(
'name' => __( 'Cart & Checkout Blocks', 'woocommerce' ),
'description' => __( 'Optimize for faster checkout', 'woocommerce' ),
'is_experimental' => false,
'disable_ui' => true,
),
'marketplace' => array(
'name' => __( 'Marketplace', 'woocommerce' ),
'description' => __(
'New, faster way to find extensions and themes for your WooCommerce store',
'woocommerce'
),
'is_experimental' => false,
'enabled_by_default' => true,
'disable_ui' => false,
'is_legacy' => true,
),
); );
foreach ( $legacy_features as $slug => $definition ) {
$this->add_feature_definition( $slug, $definition['name'], $definition );
}
/**
* The action for registering features.
*
* @since 8.3.0
*
* @param FeaturesController $features_controller The instance of FeaturesController.
*/
do_action( 'woocommerce_register_feature_definitions', $this );
foreach ( array_keys( $this->features ) as $feature_id ) {
$this->compatibility_info_by_feature[ $feature_id ] = array(
'compatible' => array(),
'incompatible' => array(),
);
}
} }
return $this->features;
} }
/** /**
@ -219,7 +262,7 @@ class FeaturesController {
* @returns array An array of information about existing features. * @returns array An array of information about existing features.
*/ */
public function get_features( bool $include_experimental = false, bool $include_enabled_info = false ): array { public function get_features( bool $include_experimental = false, bool $include_enabled_info = false ): array {
$features = $this->features; $features = $this->get_feature_definitions();
if ( ! $include_experimental ) { if ( ! $include_experimental ) {
$features = array_filter( $features = array_filter(
@ -263,7 +306,9 @@ class FeaturesController {
* @return boolean TRUE if the feature is enabled by default, FALSE otherwise. * @return boolean TRUE if the feature is enabled by default, FALSE otherwise.
*/ */
private function feature_is_enabled_by_default( string $feature_id ): bool { private function feature_is_enabled_by_default( string $feature_id ): bool {
return ! empty( $this->features[ $feature_id ]['enabled_by_default'] ); $features = $this->get_feature_definitions();
return ! empty( $features[ $feature_id ]['enabled_by_default'] );
} }
/** /**
@ -345,7 +390,9 @@ class FeaturesController {
* @return bool True if the feature exists. * @return bool True if the feature exists.
*/ */
private function feature_exists( string $feature_id ): bool { private function feature_exists( string $feature_id ): bool {
return isset( $this->features[ $feature_id ] ); $features = $this->get_feature_definitions();
return isset( $features[ $feature_id ] );
} }
/** /**
@ -360,7 +407,7 @@ class FeaturesController {
public function get_compatible_features_for_plugin( string $plugin_name, bool $enabled_features_only = false ) : array { public function get_compatible_features_for_plugin( string $plugin_name, bool $enabled_features_only = false ) : array {
$this->verify_did_woocommerce_init( __FUNCTION__ ); $this->verify_did_woocommerce_init( __FUNCTION__ );
$features = $this->features; $features = $this->get_feature_definitions();
if ( $enabled_features_only ) { if ( $enabled_features_only ) {
$features = array_filter( $features = array_filter(
$features, $features,
@ -432,25 +479,22 @@ class FeaturesController {
/** /**
* Get the name of the option that enables/disables a given feature. * Get the name of the option that enables/disables a given feature.
* Note that it doesn't check if the feature actually exists.
* *
* @param string $feature_id The id of the feature. * Note that it doesn't check if the feature actually exists. Instead it
* defaults to "woocommerce_feature_{$feature_id}_enabled" if a different
* name isn't specified in the feature registration.
*
* @param string $feature_id The id of the feature.
* @return string The option that enables or disables the feature. * @return string The option that enables or disables the feature.
*/ */
public function feature_enable_option_name( string $feature_id ): string { public function feature_enable_option_name( string $feature_id ): string {
switch ( $feature_id ) { $features = $this->get_feature_definitions();
case 'analytics':
return Analytics::TOGGLE_OPTION_NAME; if ( ! empty( $features[ $feature_id ]['option_key'] ) ) {
case 'new_navigation': return $features[ $feature_id ]['option_key'];
return Init::TOGGLE_OPTION_NAME;
case 'custom_order_tables':
case CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION:
return CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION;
case DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION:
return DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION;
default:
return "woocommerce_feature_{$feature_id}_enabled";
} }
return "woocommerce_feature_{$feature_id}_enabled";
} }
/** /**
@ -461,7 +505,9 @@ class FeaturesController {
* @return bool True if the id corresponds to a legacy feature. * @return bool True if the id corresponds to a legacy feature.
*/ */
public function is_legacy_feature( string $feature_id ): bool { public function is_legacy_feature( string $feature_id ): bool {
return in_array( $feature_id, $this->legacy_feature_ids, true ); $features = $this->get_feature_definitions();
return ! empty( $features[ $feature_id ]['is_legacy'] );
} }
/** /**
@ -497,23 +543,24 @@ class FeaturesController {
* *
* It fires FEATURE_ENABLED_CHANGED_ACTION when a feature is enabled or disabled. * It fires FEATURE_ENABLED_CHANGED_ACTION when a feature is enabled or disabled.
* *
* @param string $option The option that has been modified. * @param string $option The option that has been modified.
* @param mixed $old_value The old value of the option. * @param mixed $old_value The old value of the option.
* @param mixed $value The new value of the option. * @param mixed $value The new value of the option.
*
* @return void
*/ */
private function process_updated_option( string $option, $old_value, $value ) { private function process_updated_option( string $option, $old_value, $value ) {
$matches = array(); $matches = array();
$success = preg_match( '/^woocommerce_feature_([a-zA-Z0-9_]+)_enabled$/', $option, $matches ); $is_default_key = preg_match( '/^woocommerce_feature_([a-zA-Z0-9_]+)_enabled$/', $option, $matches );
$features_with_custom_keys = array_filter(
$known_features = array( $this->get_feature_definitions(),
Analytics::TOGGLE_OPTION_NAME, function( $feature ) {
Init::TOGGLE_OPTION_NAME, return ! empty( $feature['option_key'] );
NewProductManagementExperience::TOGGLE_OPTION_NAME, }
DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
); );
$custom_keys = wp_list_pluck( $features_with_custom_keys, 'option_key' );
if ( ! $success && ! in_array( $option, $known_features, true ) ) { if ( ! $is_default_key && ! in_array( $option, $custom_keys, true ) ) {
return; return;
} }
@ -521,14 +568,15 @@ class FeaturesController {
return; return;
} }
if ( Analytics::TOGGLE_OPTION_NAME === $option ) { $feature_id = '';
$feature_id = 'analytics'; if ( $is_default_key ) {
} elseif ( Init::TOGGLE_OPTION_NAME === $option ) {
$feature_id = 'new_navigation';
} elseif ( in_array( $option, $known_features, true ) ) {
$feature_id = $option;
} else {
$feature_id = $matches[1]; $feature_id = $matches[1];
} elseif ( in_array( $option, $custom_keys, true ) ) {
$feature_id = array_search( $option, $custom_keys, true );
}
if ( ! $feature_id ) {
return;
} }
/** /**
@ -572,24 +620,14 @@ class FeaturesController {
return $settings; return $settings;
} }
// phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment $feature_settings = array(
/**
* Filter allowing WooCommerce Admin to be disabled.
*
* @param bool $disabled False.
*/
$admin_features_disabled = apply_filters( 'woocommerce_admin_disabled', false );
// phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment
$feature_settings =
array( array(
array( 'title' => __( 'Features', 'woocommerce' ),
'title' => __( 'Features', 'woocommerce' ), 'type' => 'title',
'type' => 'title', 'desc' => __( 'Start using new features that are being progressively rolled out to improve the store management experience.', 'woocommerce' ),
'desc' => __( 'Start using new features that are being progressively rolled out to improve the store management experience.', 'woocommerce' ), 'id' => 'features_options',
'id' => 'features_options', ),
), );
);
$features = $this->get_features( true ); $features = $this->get_features( true );
@ -637,7 +675,12 @@ class FeaturesController {
continue; continue;
} }
$feature_settings[] = $this->get_setting_for_feature( $id, $features[ $id ], $admin_features_disabled ); $feature_settings[] = $this->get_setting_for_feature( $id, $features[ $id ] );
$additional_settings = $features[ $id ]['additional_settings'] ?? array();
if ( count( $additional_settings ) > 0 ) {
$feature_settings = array_merge( $feature_settings, $additional_settings );
}
} }
$feature_settings[] = array( $feature_settings[] = array(
@ -645,6 +688,20 @@ class FeaturesController {
'id' => empty( $experimental_feature_ids ) ? 'features_options' : 'experimental_features_options', 'id' => empty( $experimental_feature_ids ) ? 'features_options' : 'experimental_features_options',
); );
// Allow feature setting properties to be determined dynamically just before being rendered.
$feature_settings = array_map(
function( $feature_setting ) {
foreach ( $feature_setting as $prop => $value ) {
if ( is_callable( $value ) ) {
$feature_setting[ $prop ] = call_user_func( $value );
}
}
return $feature_setting;
},
$feature_settings
);
return $feature_settings; return $feature_settings;
} }
@ -653,15 +710,24 @@ class FeaturesController {
* *
* @param string $feature_id The feature id. * @param string $feature_id The feature id.
* @param array $feature The feature parameters, as returned by get_features. * @param array $feature The feature parameters, as returned by get_features.
* @param bool $admin_features_disabled True if admin features have been disabled via 'woocommerce_admin_disabled' filter.
* @return array The parameters to add to the settings array. * @return array The parameters to add to the settings array.
*/ */
private function get_setting_for_feature( string $feature_id, array $feature, bool $admin_features_disabled ): array { private function get_setting_for_feature( string $feature_id, array $feature ): array {
$description = $feature['description'] ?? ''; $description = $feature['description'] ?? '';
$disabled = false; $disabled = false;
$desc_tip = ''; $desc_tip = '';
$tooltip = $feature['tooltip'] ?? ''; $tooltip = $feature['tooltip'] ?? '';
$type = $feature['type'] ?? 'checkbox'; $type = $feature['type'] ?? 'checkbox';
$setting_definition = $feature['setting'] ?? array();
// phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
/**
* Filter allowing WooCommerce Admin to be disabled.
*
* @param bool $disabled False.
*/
$admin_features_disabled = apply_filters( 'woocommerce_admin_disabled', false );
// phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment
if ( ( 'analytics' === $feature_id || 'new_navigation' === $feature_id ) && $admin_features_disabled ) { if ( ( 'analytics' === $feature_id || 'new_navigation' === $feature_id ) && $admin_features_disabled ) {
$disabled = true; $disabled = true;
@ -671,13 +737,13 @@ class FeaturesController {
if ( $disabled ) { if ( $disabled ) {
$update_text = sprintf( $update_text = sprintf(
// translators: 1: line break tag. // translators: 1: line break tag.
__( '%1$s The development of this feature is currently on hold.', 'woocommerce' ), __( '%1$s The development of this feature is currently on hold.', 'woocommerce' ),
'<br/>' '<br/>'
); );
} else { } else {
$update_text = sprintf( $update_text = sprintf(
// translators: 1: line break tag. // translators: 1: line break tag.
__( __(
'%1$s This navigation will soon become unavailable while we make necessary improvements. '%1$s This navigation will soon become unavailable while we make necessary improvements.
If you turn it off now, you will not be able to turn it back on.', If you turn it off now, you will not be able to turn it back on.',
@ -690,7 +756,7 @@ class FeaturesController {
$needs_update = version_compare( get_bloginfo( 'version' ), '5.6', '<' ); $needs_update = version_compare( get_bloginfo( 'version' ), '5.6', '<' );
if ( $needs_update && current_user_can( 'update_core' ) && current_user_can( 'update_php' ) ) { if ( $needs_update && current_user_can( 'update_core' ) && current_user_can( 'update_php' ) ) {
$update_text = sprintf( $update_text = sprintf(
// translators: 1: line break tag, 2: open link to WordPress update link, 3: close link tag. // translators: 1: line break tag, 2: open link to WordPress update link, 3: close link tag.
__( '%1$s %2$sUpdate WordPress to enable the new navigation%3$s', 'woocommerce' ), __( '%1$s %2$sUpdate WordPress to enable the new navigation%3$s', 'woocommerce' ),
'<br/>', '<br/>',
'<a href="' . self_admin_url( 'update-core.php' ) . '" target="_blank">', '<a href="' . self_admin_url( 'update-core.php' ) . '" target="_blank">',
@ -704,13 +770,6 @@ class FeaturesController {
} }
} }
if ( 'product_block_editor' === $feature_id ) {
$disabled = version_compare( get_bloginfo( 'version' ), '6.2', '<' );
if ( $disabled ) {
$desc_tip = __( '⚠ This feature is compatible with WordPress version 6.2 or higher.', 'woocommerce' );
}
}
if ( ! $this->is_legacy_feature( $feature_id ) && ! $disabled && $this->verify_did_woocommerce_init() ) { if ( ! $this->is_legacy_feature( $feature_id ) && ! $disabled && $this->verify_did_woocommerce_init() ) {
$disabled = ! $this->feature_is_enabled( $feature_id ); $disabled = ! $this->feature_is_enabled( $feature_id );
$plugin_info_for_feature = $this->get_compatible_plugins_for_feature( $feature_id, true ); $plugin_info_for_feature = $this->get_compatible_plugins_for_feature( $feature_id, true );
@ -729,7 +788,7 @@ class FeaturesController {
*/ */
$desc_tip = apply_filters( 'woocommerce_feature_description_tip', $desc_tip, $feature_id, $disabled ); $desc_tip = apply_filters( 'woocommerce_feature_description_tip', $desc_tip, $feature_id, $disabled );
$feature_setting = array( $feature_setting_defaults = array(
'title' => $feature['name'], 'title' => $feature['name'],
'desc' => $description, 'desc' => $description,
'type' => $type, 'type' => $type,
@ -740,6 +799,8 @@ class FeaturesController {
'default' => $this->feature_is_enabled_by_default( $feature_id ) ? 'yes' : 'no', 'default' => $this->feature_is_enabled_by_default( $feature_id ) ? 'yes' : 'no',
); );
$feature_setting = wp_parse_args( $setting_definition, $feature_setting_defaults );
/** /**
* Allows to modify feature setting that will be used to render in the feature page. * Allows to modify feature setting that will be used to render in the feature page.
* *
@ -772,7 +833,6 @@ class FeaturesController {
$incompatibles = $this->compatibility_info_by_feature[ $feature ]['incompatible']; $incompatibles = $this->compatibility_info_by_feature[ $feature ]['incompatible'];
$this->compatibility_info_by_feature[ $feature ]['incompatible'] = array_diff( $incompatibles, array( $plugin_name ) ); $this->compatibility_info_by_feature[ $feature ]['incompatible'] = array_diff( $incompatibles, array( $plugin_name ) );
} }
} }
@ -821,9 +881,11 @@ class FeaturesController {
} }
$compatibility = $this->get_compatible_features_for_plugin( $plugin_name ); $compatibility = $this->get_compatible_features_for_plugin( $plugin_name );
$incompatible_with = array_diff( $incompatible_with = array_filter(
array_merge( $compatibility['incompatible'], $compatibility['uncertain'] ), array_merge( $compatibility['incompatible'], $compatibility['uncertain'] ),
$this->legacy_feature_ids function( $feature_id ) {
return ! $this->is_legacy_feature( $feature_id );
}
); );
if ( ( 'all' === $feature_id && ! empty( $incompatible_with ) ) || in_array( $feature_id, $incompatible_with, true ) ) { if ( ( 'all' === $feature_id && ! empty( $incompatible_with ) ) || in_array( $feature_id, $incompatible_with, true ) ) {
@ -862,9 +924,11 @@ class FeaturesController {
foreach ( $this->plugin_util->get_woocommerce_aware_plugins( true ) as $plugin ) { foreach ( $this->plugin_util->get_woocommerce_aware_plugins( true ) as $plugin ) {
$compatibility = $this->get_compatible_features_for_plugin( $plugin, true ); $compatibility = $this->get_compatible_features_for_plugin( $plugin, true );
$incompatible_with = array_diff( $incompatible_with = array_filter(
array_merge( $compatibility['incompatible'], $compatibility['uncertain'] ), array_merge( $compatibility['incompatible'], $compatibility['uncertain'] ),
$this->legacy_feature_ids function( $feature_id ) {
return ! $this->is_legacy_feature( $feature_id );
}
); );
if ( $incompatible_with ) { if ( $incompatible_with ) {
@ -917,7 +981,7 @@ class FeaturesController {
return false; return false;
} }
// phpcs:enable WordPress.Security.NonceVerification $features = $this->get_feature_definitions();
$plugins_page_url = admin_url( 'plugins.php' ); $plugins_page_url = admin_url( 'plugins.php' );
$features_page_url = $this->get_features_page_url(); $features_page_url = $this->get_features_page_url();
@ -927,7 +991,7 @@ class FeaturesController {
: sprintf( : sprintf(
/* translators: %s is a feature name. */ /* translators: %s is a feature name. */
__( "You are viewing the active plugins that are incompatible with the '%s' feature.", 'woocommerce' ), __( "You are viewing the active plugins that are incompatible with the '%s' feature.", 'woocommerce' ),
$this->features[ $feature_id ]['name'] $features[ $feature_id ]['name']
); );
$message .= '<br />'; $message .= '<br />';
@ -988,6 +1052,7 @@ class FeaturesController {
return; return;
} }
$features = $this->get_feature_definitions();
$feature_compatibility_info = $this->get_compatible_features_for_plugin( $plugin_file, true ); $feature_compatibility_info = $this->get_compatible_features_for_plugin( $plugin_file, true );
$incompatible_features = array_merge( $feature_compatibility_info['incompatible'], $feature_compatibility_info['uncertain'] ); $incompatible_features = array_merge( $feature_compatibility_info['incompatible'], $feature_compatibility_info['uncertain'] );
$incompatible_features = array_values( $incompatible_features = array_values(
@ -1010,21 +1075,21 @@ class FeaturesController {
$message = sprintf( $message = sprintf(
/* translators: %s = printable plugin name */ /* translators: %s = printable plugin name */
__( "⚠ This plugin is incompatible with the enabled WooCommerce feature '%s', it shouldn't be activated.", 'woocommerce' ), __( "⚠ This plugin is incompatible with the enabled WooCommerce feature '%s', it shouldn't be activated.", 'woocommerce' ),
$this->features[ $incompatible_features[0] ]['name'] $features[ $incompatible_features[0] ]['name']
); );
} elseif ( 2 === $incompatible_features_count ) { } elseif ( 2 === $incompatible_features_count ) {
/* translators: %1\$s, %2\$s = printable plugin names */ /* translators: %1\$s, %2\$s = printable plugin names */
$message = sprintf( $message = sprintf(
__( "⚠ This plugin is incompatible with the enabled WooCommerce features '%1\$s' and '%2\$s', it shouldn't be activated.", 'woocommerce' ), __( "⚠ This plugin is incompatible with the enabled WooCommerce features '%1\$s' and '%2\$s', it shouldn't be activated.", 'woocommerce' ),
$this->features[ $incompatible_features[0] ]['name'], $features[ $incompatible_features[0] ]['name'],
$this->features[ $incompatible_features[1] ]['name'] $features[ $incompatible_features[1] ]['name']
); );
} else { } else {
/* translators: %1\$s, %2\$s = printable plugin names, %3\$d = plugins count */ /* translators: %1\$s, %2\$s = printable plugin names, %3\$d = plugins count */
$message = sprintf( $message = sprintf(
__( "⚠ This plugin is incompatible with the enabled WooCommerce features '%1\$s', '%2\$s' and %3\$d more, it shouldn't be activated.", 'woocommerce' ), __( "⚠ This plugin is incompatible with the enabled WooCommerce features '%1\$s', '%2\$s' and %3\$d more, it shouldn't be activated.", 'woocommerce' ),
$this->features[ $incompatible_features[0] ]['name'], $features[ $incompatible_features[0] ]['name'],
$this->features[ $incompatible_features[1] ]['name'], $features[ $incompatible_features[1] ]['name'],
$incompatible_features_count - 2 $incompatible_features_count - 2
); );
} }
@ -1125,13 +1190,14 @@ class FeaturesController {
// phpcs:enable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput // phpcs:enable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
$all_items = get_plugins(); $all_items = get_plugins();
$features = $this->get_feature_definitions();
$incompatible_plugins_count = count( $this->filter_plugins_list( $all_items ) ); $incompatible_plugins_count = count( $this->filter_plugins_list( $all_items ) );
$incompatible_text = $incompatible_text =
'all' === $feature_id 'all' === $feature_id
? __( 'Incompatible with WooCommerce features', 'woocommerce' ) ? __( 'Incompatible with WooCommerce features', 'woocommerce' )
/* translators: %s = name of a WooCommerce feature */ /* translators: %s = name of a WooCommerce feature */
: sprintf( __( "Incompatible with '%s'", 'woocommerce' ), $this->features[ $feature_id ]['name'] ); : sprintf( __( "Incompatible with '%s'", 'woocommerce' ), $features[ $feature_id ]['name'] );
$incompatible_link = "<a href='plugins.php?plugin_status=incompatible_with_feature&feature_id={$feature_id}' class='current' aria-current='page'>{$incompatible_text} <span class='count'>({$incompatible_plugins_count})</span></a>"; $incompatible_link = "<a href='plugins.php?plugin_status=incompatible_with_feature&feature_id={$feature_id}' class='current' aria-current='page'>{$incompatible_text} <span class='count'>({$incompatible_plugins_count})</span></a>";
$all_plugins_count = count( $all_items ); $all_plugins_count = count( $all_items );
@ -1171,7 +1237,7 @@ class FeaturesController {
$query_params_to_remove = array( '_feature_nonce' ); $query_params_to_remove = array( '_feature_nonce' );
foreach ( array_keys( $this->features ) as $feature_id ) { foreach ( array_keys( $this->get_feature_definitions() ) as $feature_id ) {
if ( isset( $_GET[ $feature_id ] ) && is_numeric( $_GET[ $feature_id ] ) ) { if ( isset( $_GET[ $feature_id ] ) && is_numeric( $_GET[ $feature_id ] ) ) {
$value = absint( $_GET[ $feature_id ] ); $value = absint( $_GET[ $feature_id ] );

View File

@ -572,13 +572,26 @@ class DataSynchronizerTests extends HposTestCase {
$this->disable_cot_sync(); $this->disable_cot_sync();
OrderHelper::create_order(); OrderHelper::create_order();
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- This is a test. // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- test code.
$cot_setting = apply_filters( 'woocommerce_feature_setting', array(), CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ); $features = apply_filters( 'woocommerce_get_settings_advanced', array(), 'features' );
$cot_setting = array_filter(
$features,
function( $feature ) {
return CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION === $feature['id'];
}
);
$cot_setting = array_values( $cot_setting )[0];
$this->assertEquals( $cot_setting['value'], 'no' ); $this->assertEquals( $cot_setting['value'], 'no' );
$this->assertEquals( $cot_setting['disabled'], array( 'yes', 'no' ) ); $this->assertEquals( $cot_setting['disabled'], array( 'yes', 'no' ) );
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- This is a test. $sync_setting = array_filter(
$sync_setting = apply_filters( 'woocommerce_feature_setting', array(), $this->sut::ORDERS_DATA_SYNC_ENABLED_OPTION ); $features,
function( $feature ) {
return DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION === $feature['id'];
}
);
$sync_setting = array_values( $sync_setting )[0];
$this->assertEquals( $sync_setting['value'], 'no' ); $this->assertEquals( $sync_setting['value'], 'no' );
$this->assertTrue( str_contains( $sync_setting['desc_tip'], 'Sync 1 pending order' ) ); $this->assertTrue( str_contains( $sync_setting['desc_tip'], 'Sync 1 pending order' ) );
} }

View File

@ -33,38 +33,60 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
* Runs before each test. * Runs before each test.
*/ */
public function setUp(): void { public function setUp(): void {
$features = array( parent::setUp();
'mature1' => array(
'name' => 'Mature feature 1', $this->set_up_plugins();
'description' => 'The mature feature number 1',
'is_experimental' => false, add_action(
), 'woocommerce_register_feature_definitions',
'mature2' => array( function( $features_controller ) {
'name' => 'Mature feature 2', $this->reset_features_list( $this->sut );
'description' => 'The mature feature number 2',
'is_experimental' => false, $features = array(
), 'mature1' => array(
'experimental1' => array( 'name' => 'Mature feature 1',
'name' => 'Experimental feature 1', 'description' => 'The mature feature number 1',
'description' => 'The experimental feature number 1', 'is_experimental' => false,
'is_experimental' => true, ),
), 'mature2' => array(
'experimental2' => array( 'name' => 'Mature feature 2',
'name' => 'Experimental feature 2', 'description' => 'The mature feature number 2',
'description' => 'The experimental feature number 2', 'is_experimental' => false,
'is_experimental' => true, ),
), 'experimental1' => array(
'name' => 'Experimental feature 1',
'description' => 'The experimental feature number 1',
'is_experimental' => true,
),
'experimental2' => array(
'name' => 'Experimental feature 2',
'description' => 'The experimental feature number 2',
'is_experimental' => true,
),
);
foreach ( $features as $slug => $definition ) {
$features_controller->add_feature_definition( $slug, $definition['name'], $definition );
}
},
11
); );
$this->do_set_up( $features ); $this->sut = new FeaturesController();
$this->sut->init( wc_get_container()->get( LegacyProxy::class ), $this->fake_plugin_util );
delete_option( 'woocommerce_feature_mature1_enabled' );
delete_option( 'woocommerce_feature_mature2_enabled' );
delete_option( 'woocommerce_feature_experimental1_enabled' );
delete_option( 'woocommerce_feature_experimental2_enabled' );
remove_all_filters( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION );
} }
/** /**
* Runs before each test. * Runs before each test.
*
* @param array $features The fake features list to use.
*/ */
public function do_set_up( array $features ): void { private function set_up_plugins(): void {
$this->reset_container_resolutions(); $this->reset_container_resolutions();
$this->reset_legacy_proxy_mocks(); $this->reset_legacy_proxy_mocks();
@ -98,38 +120,42 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
'the_plugin_4', 'the_plugin_4',
) )
); );
}
$this->sut = new FeaturesController(); /**
$this->sut->init( wc_get_container()->get( LegacyProxy::class ), $this->fake_plugin_util ); * Resets the array of registered features so we can populate it with test features.
$init_features_method = new \ReflectionMethod( $this->sut, 'init_features' ); *
$init_features_method->setAccessible( true ); * @param FeaturesController $sut The instance of the FeaturesController class.
$init_features_method->invoke( $this->sut, $features ); *
* @return void
*/
private function reset_features_list( $sut ) {
$reflection_class = new \ReflectionClass( $sut );
delete_option( 'woocommerce_feature_mature1_enabled' ); $features = $reflection_class->getProperty( 'features' );
delete_option( 'woocommerce_feature_mature2_enabled' ); $features->setAccessible( true );
delete_option( 'woocommerce_feature_experimental1_enabled' ); $features->setValue( $sut, array() );
delete_option( 'woocommerce_feature_experimental2_enabled' ); }
remove_all_filters( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION ); /**
* Runs after each test.
*/
public function tearDown(): void {
$this->reset_features_list( $this->sut );
remove_all_actions( 'woocommerce_register_feature_definitions' );
parent::tearDown();
} }
/** /**
* @testdox 'get_features' returns existing non-experimental features without enabling information if requested to do so. * @testdox 'get_features' returns existing non-experimental features without enabling information if requested to do so.
*/ */
public function test_get_features_not_including_experimental_not_including_values() { public function test_get_features_not_including_experimental_not_including_values() {
$actual = $this->sut->get_features( false, false ); $actual = array_keys( $this->sut->get_features( false, false ) );
$expected = array( $expected = array(
'mature1' => array( 'mature1',
'name' => 'Mature feature 1', 'mature2',
'description' => 'The mature feature number 1',
'is_experimental' => false,
),
'mature2' => array(
'name' => 'Mature feature 2',
'description' => 'The mature feature number 2',
'is_experimental' => false,
),
); );
$this->assertEquals( $expected, $actual ); $this->assertEquals( $expected, $actual );
@ -139,29 +165,13 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
* @testdox 'get_features' returns all existing features without enabling information if requested to do so. * @testdox 'get_features' returns all existing features without enabling information if requested to do so.
*/ */
public function test_get_features_including_experimental_not_including_values() { public function test_get_features_including_experimental_not_including_values() {
$actual = $this->sut->get_features( true, false ); $actual = array_keys( $this->sut->get_features( true, false ) );
$expected = array( $expected = array(
'mature1' => array( 'mature1',
'name' => 'Mature feature 1', 'mature2',
'description' => 'The mature feature number 1', 'experimental1',
'is_experimental' => false, 'experimental2',
),
'mature2' => array(
'name' => 'Mature feature 2',
'description' => 'The mature feature number 2',
'is_experimental' => false,
),
'experimental1' => array(
'name' => 'Experimental feature 1',
'description' => 'The experimental feature number 1',
'is_experimental' => true,
),
'experimental2' => array(
'name' => 'Experimental feature 2',
'description' => 'The experimental feature number 2',
'is_experimental' => true,
),
); );
$this->assertEquals( $expected, $actual ); $this->assertEquals( $expected, $actual );
@ -176,32 +186,28 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
update_option( 'woocommerce_feature_experimental1_enabled', 'yes' ); update_option( 'woocommerce_feature_experimental1_enabled', 'yes' );
// No option for experimental2. // No option for experimental2.
$actual = $this->sut->get_features( true, true ); $actual = array_map(
function( $feature ) {
return array_intersect_key(
$feature,
array( 'is_enabled' => '' )
);
},
$this->sut->get_features( true, true )
);
$expected = array( $expected = array(
'mature1' => array( 'mature1' => array(
'name' => 'Mature feature 1', 'is_enabled' => true,
'description' => 'The mature feature number 1',
'is_experimental' => false,
'is_enabled' => true,
), ),
'mature2' => array( 'mature2' => array(
'name' => 'Mature feature 2', 'is_enabled' => false,
'description' => 'The mature feature number 2',
'is_experimental' => false,
'is_enabled' => false,
), ),
'experimental1' => array( 'experimental1' => array(
'name' => 'Experimental feature 1', 'is_enabled' => true,
'description' => 'The experimental feature number 1',
'is_experimental' => true,
'is_enabled' => true,
), ),
'experimental2' => array( 'experimental2' => array(
'name' => 'Experimental feature 2', 'is_enabled' => false,
'description' => 'The experimental feature number 2',
'is_experimental' => true,
'is_enabled' => false,
), ),
); );
@ -483,40 +489,50 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
* @testdox 'get_compatible_features_for_plugin' returns proper information for a plugin that has declared compatibility with the passed feature, when only enabled features are requested. * @testdox 'get_compatible_features_for_plugin' returns proper information for a plugin that has declared compatibility with the passed feature, when only enabled features are requested.
*/ */
public function test_get_compatible_enabled_features_for_registered_plugin() { public function test_get_compatible_enabled_features_for_registered_plugin() {
$features = array( add_action(
'mature1' => array( 'woocommerce_register_feature_definitions',
'name' => 'Mature feature 1', function( $features_controller ) {
'description' => 'The mature feature number 1', $this->reset_features_list( $this->sut );
'is_experimental' => false,
),
'mature2' => array(
'name' => 'Mature feature 2',
'description' => 'The mature feature number 2',
'is_experimental' => false,
),
'mature3' => array(
'name' => 'Mature feature 3',
'description' => 'The mature feature number 3',
'is_experimental' => false,
),
'experimental1' => array(
'name' => 'Experimental feature 1',
'description' => 'The experimental feature number 1',
'is_experimental' => true,
),
'experimental2' => array(
'name' => 'Experimental feature 2',
'description' => 'The experimental feature number 2',
'is_experimental' => true,
),
'experimental3' => array(
'name' => 'Experimental feature 3',
'description' => 'The experimental feature number 3',
'is_experimental' => true,
),
);
$this->do_set_up( $features ); $features = array(
'mature1' => array(
'name' => 'Mature feature 1',
'description' => 'The mature feature number 1',
'is_experimental' => false,
),
'mature2' => array(
'name' => 'Mature feature 2',
'description' => 'The mature feature number 2',
'is_experimental' => false,
),
'mature3' => array(
'name' => 'Mature feature 3',
'description' => 'The mature feature number 3',
'is_experimental' => false,
),
'experimental1' => array(
'name' => 'Experimental feature 1',
'description' => 'The experimental feature number 1',
'is_experimental' => true,
),
'experimental2' => array(
'name' => 'Experimental feature 2',
'description' => 'The experimental feature number 2',
'is_experimental' => true,
),
'experimental3' => array(
'name' => 'Experimental feature 3',
'description' => 'The experimental feature number 3',
'is_experimental' => true,
),
);
foreach ( $features as $slug => $definition ) {
$features_controller->add_feature_definition( $slug, $definition['name'], $definition );
}
},
20
);
$this->simulate_inside_before_woocommerce_init_hook(); $this->simulate_inside_before_woocommerce_init_hook();
@ -809,6 +825,33 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
// phpcs:enable // phpcs:enable
$local_sut = new FeaturesController(); $local_sut = new FeaturesController();
add_action(
'woocommerce_register_feature_definitions',
function( $features_controller ) use ( $local_sut ) {
$this->reset_features_list( $local_sut );
$features = array(
'custom_order_tables' => array(
'name' => __( 'High-Performance order storage', 'woocommerce' ),
'is_experimental' => true,
'enabled_by_default' => false,
),
'cart_checkout_blocks' => array(
'name' => __( 'Cart & Checkout Blocks', 'woocommerce' ),
'description' => __( 'Optimize for faster checkout', 'woocommerce' ),
'is_experimental' => false,
'disable_ui' => true,
),
);
foreach ( $features as $slug => $definition ) {
$features_controller->add_feature_definition( $slug, $definition['name'], $definition );
}
},
20
);
$local_sut->init( wc_get_container()->get( LegacyProxy::class ), $fake_plugin_util ); $local_sut->init( wc_get_container()->get( LegacyProxy::class ), $fake_plugin_util );
$plugins = array( 'compatible_plugin1', 'compatible_plugin2' ); $plugins = array( 'compatible_plugin1', 'compatible_plugin2' );
$fake_plugin_util->set_active_plugins( $plugins ); $fake_plugin_util->set_active_plugins( $plugins );
@ -816,16 +859,19 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
$local_sut->declare_compatibility( 'custom_order_tables', $plugin ); $local_sut->declare_compatibility( 'custom_order_tables', $plugin );
$local_sut->declare_compatibility( 'cart_checkout_blocks', $plugin ); $local_sut->declare_compatibility( 'cart_checkout_blocks', $plugin );
} }
$cot_controller = new CustomOrdersTableController(); $cot_controller = new CustomOrdersTableController();
$cot_setting_call = function () use ( $fake_plugin_util, $local_sut ) { $cot_setting_call = function () use ( $fake_plugin_util, $local_sut ) {
$this->plugin_util = $fake_plugin_util; $this->plugin_util = $fake_plugin_util;
$this->features_controller = $local_sut; $this->features_controller = $local_sut;
$this->data_synchronizer = wc_get_container()->get( DataSynchronizer::class ); $this->data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
return $this->get_hpos_feature_setting( array(), CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION );
return $this->get_hpos_setting_for_feature();
}; };
$cot_setting = $cot_setting_call->call( $cot_controller ); $cot_setting = $cot_setting_call->call( $cot_controller );
$actual = call_user_func( $cot_setting['disabled'] );
$this->assertEquals( array(), $actual );
$this->assertEquals( $cot_setting['disabled'], array() );
$incompatible_plugins = function () use ( $plugins ) { $incompatible_plugins = function () use ( $plugins ) {
return $this->get_incompatible_plugins( 'all', array_flip( $plugins ) ); return $this->get_incompatible_plugins( 'all', array_flip( $plugins ) );
}; };
@ -863,6 +909,33 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
); );
$local_sut = new FeaturesController(); $local_sut = new FeaturesController();
add_action(
'woocommerce_register_feature_definitions',
function( $features_controller ) use ( $local_sut ) {
$this->reset_features_list( $local_sut );
$features = array(
'custom_order_tables' => array(
'name' => __( 'High-Performance order storage', 'woocommerce' ),
'is_experimental' => true,
'enabled_by_default' => false,
),
'cart_checkout_blocks' => array(
'name' => __( 'Cart & Checkout Blocks', 'woocommerce' ),
'description' => __( 'Optimize for faster checkout', 'woocommerce' ),
'is_experimental' => false,
'disable_ui' => true,
),
);
foreach ( $features as $slug => $definition ) {
$features_controller->add_feature_definition( $slug, $definition['name'], $definition );
}
},
20
);
$local_sut->init( wc_get_container()->get( LegacyProxy::class ), $fake_plugin_util ); $local_sut->init( wc_get_container()->get( LegacyProxy::class ), $fake_plugin_util );
$plugins = array( 'compatible_plugin', 'incompatible_plugin' ); $plugins = array( 'compatible_plugin', 'incompatible_plugin' );
$fake_plugin_util->set_active_plugins( $plugins ); $fake_plugin_util->set_active_plugins( $plugins );
@ -875,11 +948,13 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
$this->plugin_util = $fake_plugin_util; $this->plugin_util = $fake_plugin_util;
$this->features_controller = $local_sut; $this->features_controller = $local_sut;
$this->data_synchronizer = wc_get_container()->get( DataSynchronizer::class ); $this->data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
return $this->get_hpos_feature_setting( array(), CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION );
return $this->get_hpos_setting_for_feature();
}; };
$cot_setting = $cot_setting_call->call( $cot_controller ); $cot_setting = $cot_setting_call->call( $cot_controller );
$actual = call_user_func( $cot_setting['disabled'] );
$this->assertEquals( array( 'yes' ), $actual );
$this->assertEquals( $cot_setting['disabled'], array( 'yes' ) );
$incompatible_plugins = function () use ( $plugins ) { $incompatible_plugins = function () use ( $plugins ) {
return $this->get_incompatible_plugins( 'all', array_flip( $plugins ) ); return $this->get_incompatible_plugins( 'all', array_flip( $plugins ) );
}; };