diff --git a/plugins/woocommerce/changelog/create_cot_tables_on_feature_enable b/plugins/woocommerce/changelog/create_cot_tables_on_feature_enable new file mode 100644 index 00000000000..ff29e865c82 --- /dev/null +++ b/plugins/woocommerce/changelog/create_cot_tables_on_feature_enable @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Automatically create the custom order tables if the corresponding feature is enabled diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php index caac143ee92..d38305c2e8c 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php @@ -102,6 +102,7 @@ class CustomOrdersTableController { self::add_filter( DataSynchronizer::PENDING_SYNCHRONIZATION_FINISHED_ACTION, array( $this, 'process_sync_finished' ), 10, 0 ); self::add_action( 'woocommerce_update_options_advanced_custom_data_stores', array( $this, 'process_options_updated' ), 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 ); } /** @@ -225,57 +226,51 @@ class CustomOrdersTableController { * @return array The updated array of tools- */ private function add_initiate_regeneration_entry_to_tools_array( array $tools_array ): array { - if ( ! $this->is_feature_visible() ) { + if ( ! $this->data_synchronizer->check_orders_table_exists() ) { return $tools_array; } - if ( $this->data_synchronizer->check_orders_table_exists() ) { - $tools_array['delete_custom_orders_table'] = array( - 'name' => __( 'Delete the custom orders tables', 'woocommerce' ), - 'desc' => sprintf( - '%1$s %2$s', - __( 'Note:', 'woocommerce' ), - __( 'This will delete the custom orders tables. The tables can be deleted only if they are not not in use (via Settings > Advanced > Custom data stores). You can create them again at any time with the "Create the custom orders tables" tool.', 'woocommerce' ) - ), - 'requires_refresh' => true, - 'callback' => function () { - $this->delete_custom_orders_tables(); - return __( 'Custom orders tables have been deleted.', 'woocommerce' ); - }, - 'button' => __( 'Delete', 'woocommerce' ), - 'disabled' => $this->custom_orders_table_usage_is_enabled(), - ); + if ( $this->is_feature_visible() ) { + $disabled = true; + $message = __( 'This will delete the custom orders tables. The tables can be deleted only if the "High-Performance order storage" feature is disabled (via Settings > Advanced > Features).', 'woocommerce' ); } else { - $tools_array['create_custom_orders_table'] = array( - 'name' => __( 'Create the custom orders tables', 'woocommerce' ), - 'desc' => __( 'This tool will create the custom orders tables. Once created you can go to WooCommerce > Settings > Advanced > Custom data stores and configure the usage of the tables.', 'woocommerce' ), - 'requires_refresh' => true, - 'callback' => function() { - $this->create_custom_orders_tables(); - return __( 'Custom orders tables have been created. You can now go to WooCommerce > Settings > Advanced > Custom data stores.', 'woocommerce' ); - }, - 'button' => __( 'Create', 'woocommerce' ), - ); + $disabled = false; + $message = __( 'This will delete the custom orders tables. To create them again enable the "High-Performance order storage" feature (via Settings > Advanced > Features).', 'woocommerce' ); } + $tools_array['delete_custom_orders_table'] = array( + 'name' => __( 'Delete the custom orders tables', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + $message + ), + 'requires_refresh' => true, + 'callback' => function () { + $this->features_controller->change_feature_enable( 'custom_order_tables', false ); + $this->delete_custom_orders_tables(); + return __( 'Custom orders tables have been deleted.', 'woocommerce' ); + }, + 'button' => __( 'Delete', 'woocommerce' ), + 'disabled' => $disabled, + ); + return $tools_array; } /** * Create the custom orders tables in response to the user pressing the tool button. * + * @param bool $verify_nonce True to perform the nonce verification, false to skip it. + * * @throws \Exception Can't create the tables. */ - private function create_custom_orders_tables() { + private function create_custom_orders_tables( bool $verify_nonce = true ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput - if ( ! isset( $_REQUEST['_wpnonce'] ) || wp_verify_nonce( $_REQUEST['_wpnonce'], 'debug_action' ) === false ) { + if ( $verify_nonce && ( ! isset( $_REQUEST['_wpnonce'] ) || wp_verify_nonce( $_REQUEST['_wpnonce'], 'debug_action' ) === false ) ) { throw new \Exception( 'Invalid nonce' ); } - if ( ! $this->is_feature_visible() ) { - throw new \Exception( "Can't create the custom orders tables: the feature isn't enabled" ); - } - $this->data_synchronizer->create_database_tables(); update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' ); } @@ -319,116 +314,103 @@ class CustomOrdersTableController { * @return array The updated settings array. */ private function get_settings( array $settings, string $section_id ): array { - if ( ! $this->is_feature_visible() || $section_id !== 'custom_data_stores' ) { + if ( ! $this->is_feature_visible() || 'custom_data_stores' !== $section_id ) { return $settings; } - if ( $this->data_synchronizer->check_orders_table_exists() ) { - $settings[] = array( - 'title' => __( 'Custom orders tables', 'woocommerce' ), - 'type' => 'title', - 'id' => 'cot-title', - 'desc' => sprintf( - /* translators: %1$s = tag, %2$s = tag. */ - __( '%1$sWARNING:%2$s This feature is currently under development and may cause database instability. For contributors only.', 'woocommerce' ), - '', - '' - ), - ); + $settings[] = array( + 'title' => __( 'Custom orders tables', 'woocommerce' ), + 'type' => 'title', + 'id' => 'cot-title', + 'desc' => sprintf( + /* translators: %1$s = tag, %2$s = tag. */ + __( '%1$sWARNING:%2$s This feature is currently under development and may cause database instability. For contributors only.', 'woocommerce' ), + '', + '' + ), + ); - $sync_status = $this->data_synchronizer->get_sync_status(); - $sync_is_pending = $sync_status['current_pending_count'] !== 0; + $sync_status = $this->data_synchronizer->get_sync_status(); + $sync_is_pending = 0 !== $sync_status['current_pending_count']; - $settings[] = array( - 'title' => __( 'Data store for orders', 'woocommerce' ), - 'id' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, - 'default' => 'no', - 'type' => 'radio', - 'options' => array( - 'yes' => __( 'Use the WooCommerce orders tables', 'woocommerce' ), - 'no' => __( 'Use the WordPress posts table', 'woocommerce' ), - ), - 'checkboxgroup' => 'start', - 'disabled' => $sync_is_pending ? array( 'yes', 'no' ) : array(), - ); + $settings[] = array( + 'title' => __( 'Data store for orders', 'woocommerce' ), + 'id' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, + 'default' => 'no', + 'type' => 'radio', + 'options' => array( + 'yes' => __( 'Use the WooCommerce orders tables', 'woocommerce' ), + 'no' => __( 'Use the WordPress posts table', 'woocommerce' ), + ), + 'checkboxgroup' => 'start', + 'disabled' => $sync_is_pending ? array( 'yes', 'no' ) : array(), + ); - if ( $sync_is_pending ) { - $initial_pending_count = $sync_status['initial_pending_count']; - $current_pending_count = $sync_status['current_pending_count']; - if ( $initial_pending_count ) { - $text = - sprintf( - /* translators: %1$s=current number of orders pending sync, %2$s=initial number of orders pending sync */ - _n( 'There\'s %1$s order (out of a total of %2$s) pending sync!', 'There are %1$s orders (out of a total of %2$s) pending sync!', $current_pending_count, 'woocommerce' ), - $current_pending_count, - $initial_pending_count - ); - } else { - $text = - /* translators: %s=initial number of orders pending sync */ - sprintf( _n( 'There\'s %s order pending sync!', 'There are %s orders pending sync!', $current_pending_count, 'woocommerce' ), $current_pending_count, 'woocommerce' ); - } - - if ( $this->batch_processing_controller->is_enqueued( get_class( $this->data_synchronizer ) ) ) { - $text .= __( "
Synchronization for these orders is currently in progress.
The authoritative table can't be changed until sync completes.", 'woocommerce' ); - } else { - $text .= __( "
The authoritative table can't be changed until these orders are synchronized.", 'woocommerce' ); - } - - $settings[] = array( - 'type' => 'info', - 'id' => 'cot-out-of-sync-warning', - 'css' => 'color: #C00000', - 'text' => $text, - ); - } - - $settings[] = array( - 'desc' => __( 'Keep the posts table and the orders tables synchronized', 'woocommerce' ), - 'id' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, - 'type' => 'checkbox', - ); - - if ( $sync_is_pending ) { - if ( $this->data_synchronizer->data_sync_is_enabled() ) { - $message = $this->custom_orders_table_usage_is_enabled() ? - __( 'Switch to using the posts table as the authoritative data store for orders when sync finishes', 'woocommerce' ) : - __( 'Switch to using the orders table as the authoritative data store for orders when sync finishes', 'woocommerce' ); - $settings[] = array( - 'desc' => $message, - 'id' => self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION, - 'type' => 'checkbox', + if ( $sync_is_pending ) { + $initial_pending_count = $sync_status['initial_pending_count']; + $current_pending_count = $sync_status['current_pending_count']; + if ( $initial_pending_count ) { + $text = + sprintf( + /* translators: %1$s=current number of orders pending sync, %2$s=initial number of orders pending sync */ + _n( 'There\'s %1$s order (out of a total of %2$s) pending sync!', 'There are %1$s orders (out of a total of %2$s) pending sync!', $current_pending_count, 'woocommerce' ), + $current_pending_count, + $initial_pending_count ); - } + } else { + $text = + /* translators: %s=initial number of orders pending sync */ + sprintf( _n( 'There\'s %s order pending sync!', 'There are %s orders pending sync!', $current_pending_count, 'woocommerce' ), $current_pending_count, 'woocommerce' ); + } + + if ( $this->batch_processing_controller->is_enqueued( get_class( $this->data_synchronizer ) ) ) { + $text .= __( "
Synchronization for these orders is currently in progress.
The authoritative table can't be changed until sync completes.", 'woocommerce' ); + } else { + $text .= __( "
The authoritative table can't be changed until these orders are synchronized.", 'woocommerce' ); } $settings[] = array( - 'desc' => __( 'Use database transactions for the orders data synchronization', 'woocommerce' ), - 'id' => self::USE_DB_TRANSACTIONS_OPTION, - 'type' => 'checkbox', - ); - - $isolation_level_names = self::get_valid_transaction_isolation_levels(); - $settings[] = array( - 'desc' => __( 'Database transaction isolation level to use', 'woocommerce' ), - 'id' => self::DB_TRANSACTIONS_ISOLATION_LEVEL_OPTION, - 'type' => 'select', - 'options' => array_combine( $isolation_level_names, $isolation_level_names ), - 'default' => self::DEFAULT_DB_TRANSACTIONS_ISOLATION_LEVEL, - ); - } else { - $settings[] = array( - 'title' => __( 'Custom orders tables', 'woocommerce' ), - 'type' => 'title', - 'desc' => sprintf( - /* translators: %1$s = tag, %2$s = tag. */ - __( 'Create the tables first by going to %1$sWooCommerce > Status > Tools%2$s and running %1$sCreate the custom orders tables%2$s.', 'woocommerce' ), - '', - '' - ), + 'type' => 'info', + 'id' => 'cot-out-of-sync-warning', + 'css' => 'color: #C00000', + 'text' => $text, ); } + $settings[] = array( + 'desc' => __( 'Keep the posts table and the orders tables synchronized', 'woocommerce' ), + 'id' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, + 'type' => 'checkbox', + ); + + if ( $sync_is_pending ) { + if ( $this->data_synchronizer->data_sync_is_enabled() ) { + $message = $this->custom_orders_table_usage_is_enabled() ? + __( 'Switch to using the posts table as the authoritative data store for orders when sync finishes', 'woocommerce' ) : + __( 'Switch to using the orders table as the authoritative data store for orders when sync finishes', 'woocommerce' ); + $settings[] = array( + 'desc' => $message, + 'id' => self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION, + 'type' => 'checkbox', + ); + } + } + + $settings[] = array( + 'desc' => __( 'Use database transactions for the orders data synchronization', 'woocommerce' ), + 'id' => self::USE_DB_TRANSACTIONS_OPTION, + 'type' => 'checkbox', + ); + + $isolation_level_names = self::get_valid_transaction_isolation_levels(); + $settings[] = array( + 'desc' => __( 'Database transaction isolation level to use', 'woocommerce' ), + 'id' => self::DB_TRANSACTIONS_ISOLATION_LEVEL_OPTION, + 'type' => 'select', + 'options' => array_combine( $isolation_level_names, $isolation_level_names ), + 'default' => self::DEFAULT_DB_TRANSACTIONS_ISOLATION_LEVEL, + ); + $settings[] = array( 'type' => 'sectionend' ); return $settings; @@ -456,7 +438,7 @@ class CustomOrdersTableController { * @param mixed $value New value of the setting. */ private function process_updated_option( $option, $old_value, $value ) { - if ( $option === DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION && $value === 'no' ) { + if ( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION === $option && 'no' === $value ) { $this->data_synchronizer->cleanup_synchronization_state(); } } @@ -472,7 +454,7 @@ class CustomOrdersTableController { * @throws \Exception Attempt to change the authoritative orders table while orders sync is pending. */ private function process_pre_update_option( $value, $option, $old_value ) { - if ( $option !== self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION || $value === $old_value || $old_value === false ) { + if ( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION !== $option || $value === $old_value || false === $old_value ) { return $value; } @@ -537,6 +519,24 @@ class CustomOrdersTableController { } } + /** + * 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 ( 'custom_order_tables' !== $feature_id || ! $is_enabled ) { + return; + } + + if ( ! $this->data_synchronizer->check_orders_table_exists() ) { + update_option( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, 'no' ); + $this->create_custom_orders_tables( false ); + } + } + /** * Handler for the woocommerce_after_register_post_type post, * registers the post type for placeholder orders. diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php index ec9f20d64ec..cf2b2174f21 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php @@ -280,6 +280,7 @@ WHERE AND orders.id IS NULL", $order_post_types ); + // phpcs:enable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare break; case self::ID_TYPE_MISSING_IN_POSTS_TABLE: $sql = "