diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-settings.php b/plugins/woocommerce/includes/admin/class-wc-admin-settings.php
index 69506066749..48bbe0e6e2a 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-settings.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-settings.php
@@ -7,6 +7,7 @@
*/
use Automattic\Jetpack\Constants;
+use Automattic\WooCommerce\Utilities\ArrayUtil;
if ( ! defined( 'ABSPATH' ) ) {
exit;
@@ -273,6 +274,12 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
}
break;
+ case 'info':
+ echo '
| ';
+ echo wp_kses_post( wpautop( wptexturize( $value['text'] ) ) );
+ echo ' |
';
+ break;
+
// Section Ends.
case 'sectionend':
if ( ! empty( $value['id'] ) ) {
@@ -417,6 +424,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
// Radio inputs.
case 'radio':
$option_value = $value['value'];
+ $disabled_values = ArrayUtil::get_value_or_default($value, 'disabled', array());
?>
@@ -435,6 +443,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
name=""
value=""
type="radio"
+
style=""
class=""
diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php
index aaaa777505e..338a6e17608 100644
--- a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php
+++ b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php
@@ -92,6 +92,38 @@ class CustomOrdersTableController {
999,
2
);
+
+ add_filter(
+ 'updated_option',
+ function( $option, $old_value, $value ) {
+ $this->process_updated_option( $option, $old_value, $value );
+ },
+ 999,
+ 3
+ );
+
+ add_filter(
+ 'pre_update_option',
+ function( $value, $option, $old_value ) {
+ return $this->process_pre_update_option( $option, $old_value, $value );
+ },
+ 999,
+ 3
+ );
+
+ add_filter(
+ DataSynchronizer::PENDING_SYNCHRONIZATION_FINISHED_ACTION,
+ function() {
+ $this->process_sync_finished();
+ }
+ );
+
+ add_action(
+ 'woocommerce_update_options_advanced_custom_data_stores',
+ function() {
+ $this->process_options_updated();
+ }
+ );
}
/**
@@ -213,6 +245,7 @@ class CustomOrdersTableController {
}
$this->data_synchronizer->create_database_tables();
+ update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' );
}
/**
@@ -258,40 +291,182 @@ class CustomOrdersTableController {
return $settings;
}
- $title_item = array(
- 'title' => __( 'Custom orders tables', 'woocommerce' ),
- 'type' => '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' ),
- '',
- ''
- ),
- );
-
if ( $this->data_synchronizer->check_orders_table_exists() ) {
- $settings[] = $title_item;
+ $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 = 0 !== $sync_status['current_pending_count'];
$settings[] = array(
- 'title' => __( 'Enable tables usage', 'woocommerce' ),
- 'desc' => __( 'Use the custom orders tables as the main orders data store.', 'woocommerce' ),
+ 'title' => __( 'Data store for orders', 'woocommerce' ),
'id' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
'default' => 'no',
- 'type' => 'checkbox',
+ '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'];
+ if ( $initial_pending_count ) {
+ $text = sprintf(
+ /* translators: %1$s=current number of orders pending sync, %2$s=initial number of orders pending sync */
+ __( 'There are %1$s orders (out of a total of %2$s) pending sync!', 'woocommerce' ),
+ $sync_status['current_pending_count'],
+ $initial_pending_count
+ );
+ } else {
+ $text = sprintf(
+ /* translators: %1$s=current number of orders pending sync */
+ __( 'There are %1$s orders pending sync!', 'woocommerce' ),
+ $sync_status['current_pending_count']
+ );
+ }
+
+ if ( $this->data_synchronizer->pending_data_sync_is_in_progress() ) {
+ $text .= __( '
Syncrhonization for these orders is currently in progress.', 'woocommerce' );
+ }
+
+ $text .= __( "
The authoritative table can't be changed until sync completes.", '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' => DataSynchronizer::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION,
+ 'type' => 'checkbox',
+ );
+ }
+ }
} else {
- $title_item['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' ),
- '',
- ''
+ $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' ),
+ '',
+ ''
+ ),
);
- $settings[] = $title_item;
}
$settings[] = array( 'type' => 'sectionend' );
return $settings;
}
+
+ /**
+ * Handler for the individual setting updated hook.
+ *
+ * @param string $option Setting name.
+ * @param mixed $old_value Old value of the setting.
+ * @param mixed $value New value of the setting.
+ */
+ private function process_updated_option( $option, $old_value, $value ) {
+ if ( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION === $option && 'no' === $value ) {
+ $this->data_synchronizer->cleanup_synchronization_state();
+ }
+ }
+
+ /**
+ * Handler for the setting pre-update hook.
+ * We use it to verify that authoritative orders table switch doesn't happen while sync is pending.
+ *
+ * @param string $option Setting name.
+ * @param mixed $old_value Old value of the setting.
+ * @param mixed $value New value of the setting.
+ *
+ * @throws \Exception Attempt to change the authoritative orders table while orders sync is pending.
+ */
+ private function process_pre_update_option( $option, $old_value, $value ) {
+ if ( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION !== $option || $value === $old_value || false === $old_value ) {
+ return $value;
+ }
+
+ $sync_is_pending = 0 !== $this->data_synchronizer->get_current_orders_pending_sync_count();
+ if ( $sync_is_pending ) {
+ throw new \Exception( "The authoritative table for orders storage can't be changed while there are orders out of sync" );
+ }
+
+ return $value;
+ }
+
+ /**
+ * Handler for the synchronization finished hook.
+ * Here we switch the authoritative table if needed.
+ */
+ private function process_sync_finished() {
+ if ( $this->auto_flip_authoritative_table_enabled() ) {
+ return;
+ }
+
+ update_option( DataSynchronizer::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION, 'no' );
+
+ if ( $this->custom_orders_table_usage_is_enabled() ) {
+ update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' );
+ } else {
+ update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'yes' );
+ }
+ }
+
+ /**
+ * Is the automatic authoritative table switch setting set?
+ *
+ * @return bool
+ */
+ private function auto_flip_authoritative_table_enabled(): bool {
+ return 'yes' === get_option( DataSynchronizer::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION );
+ }
+
+ /**
+ * Handler for the all settings updated hook.
+ */
+ private function process_options_updated() {
+ $data_sync_is_enabled = $this->data_synchronizer->data_sync_is_enabled();
+
+ // Disabling the sync implies disabling the automatic authoritative table switch too.
+ if ( ! $data_sync_is_enabled && $this->auto_flip_authoritative_table_enabled() ) {
+ update_option( DataSynchronizer::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION, 'no' );
+ }
+
+ // Enabling the sync implies starting it too, if needed.
+ // We do this check here, and not in process_pre_update_option, so that if for some reason
+ // the setting is enabled but no sync is in process, sync will start by just saving the
+ // settings even without modifying them.
+ if ( $data_sync_is_enabled && ! $this->data_synchronizer->pending_data_sync_is_in_progress() ) {
+ $this->data_synchronizer->start_synchronizing_pending_orders();
+ }
+ }
}
diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php
index 19894cfcb18..579a419e43b 100644
--- a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php
+++ b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php
@@ -17,6 +17,13 @@ defined( 'ABSPATH' ) || exit;
*/
class DataSynchronizer {
+ const ORDERS_DATA_SYNC_ENABLED_OPTION = 'woocommerce_custom_orders_table_data_sync_enabled';
+ const INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION = 'woocommerce_initial_orders_pending_sync_count';
+ const AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION = 'woocommerce_auto_flip_authoritative_table_roles';
+ const PENDING_SYNC_IS_IN_PROGRESS_OPTION = 'woocommerce_custom_orders_table_pending_sync_in_progress';
+ const ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK = 'woocommerce_run_orders_sync_callback';
+ const PENDING_SYNCHRONIZATION_FINISHED_ACTION = 'woocommerce_orders_sync_finished';
+
/**
* The data store object to use.
*
@@ -31,7 +38,17 @@ class DataSynchronizer {
*/
private $database_util;
- // TODO: Add a constructor to handle hooks as appropriate.
+ /**
+ * Class constructor.
+ */
+ public function __construct() {
+ add_action(
+ self::ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK,
+ function() {
+ $this->do_pending_orders_synchronization();
+ }
+ );
+ }
/**
* Class initialization, invoked by the DI container.
@@ -74,5 +91,113 @@ class DataSynchronizer {
}
}
+ /**
+ * Is the data sync between old and new tables currently enabled?
+ *
+ * @return bool
+ */
+ public function data_sync_is_enabled(): bool {
+ return 'yes' === get_option( self::ORDERS_DATA_SYNC_ENABLED_OPTION );
+ }
+ /**
+ * Is a sync process currently in progress?
+ *
+ * @return bool
+ */
+ public function pending_data_sync_is_in_progress(): bool {
+ return 'yes' === get_option( self::PENDING_SYNC_IS_IN_PROGRESS_OPTION );
+ }
+
+ /**
+ * Get the current sync process status.
+ * The information is meaningful only if pending_data_sync_is_in_progress return true.
+ *
+ * @return array
+ */
+ public function get_sync_status() {
+ return array(
+ 'initial_pending_count' => (int) get_option( self::INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION, 0 ),
+ 'current_pending_count' => $this->get_current_orders_pending_sync_count(),
+ 'auto_flip' => 'yes' === get_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION ),
+ 'sync_in_progress' => $this->pending_data_sync_is_in_progress(),
+ );
+ }
+
+ /**
+ * Calculate how many orders need to be synchronized currently.
+ */
+ public function get_current_orders_pending_sync_count(): int {
+ // TODO: get this value by querying the database.
+ return get_option( 'woocommerce_fake_orders_pending_sync_count', 0 );
+ }
+
+ /**
+ * Start an orders synchronization process.
+ * This will setup the appropriate status information and schedule the first synchronization batch.
+ */
+ public function start_synchronizing_pending_orders() {
+ $initial_pending_count = $this->get_current_orders_pending_sync_count();
+ if ( 0 === $initial_pending_count ) {
+ return;
+ }
+
+ update_option( self::INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION, $initial_pending_count );
+
+ $queue = WC()->get_instance_of( \WC_Queue::class );
+ $queue->cancel_all( self::ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK );
+
+ update_option( self::PENDING_SYNC_IS_IN_PROGRESS_OPTION, 'yes' );
+ $this->schedule_pending_orders_synchronization();
+ }
+
+ /**
+ * Schedule the next orders synchronization batch.
+ */
+ private function schedule_pending_orders_synchronization() {
+ $queue = WC()->get_instance_of( \WC_Queue::class );
+ $queue->schedule_single(
+ WC()->call_function( 'time' ) + 1,
+ self::ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK,
+ array(),
+ 'woocommerce-db-updates'
+ );
+ }
+
+ /**
+ * Run one orders synchronization batch.
+ */
+ private function do_pending_orders_synchronization() {
+ if ( ! $this->pending_data_sync_is_in_progress() ) {
+ return;
+ }
+
+ // TODO: Syncrhonize a batch of orders.
+ $fake_count = (int) get_option( 'woocommerce_fake_orders_pending_sync_count', 0 );
+ if ( $fake_count > 0 ) {
+ update_option( 'woocommerce_fake_orders_pending_sync_count', $fake_count - 1 );
+ }
+
+ if ( 0 === $this->get_current_orders_pending_sync_count() ) {
+ $this->cleanup_synchronization_state();
+
+ /**
+ * Hook to signal that the orders tables synchronization process has finised (nothing left to synchronize).
+ */
+ do_action( self::PENDING_SYNCHRONIZATION_FINISHED_ACTION );
+ } else {
+ $this->schedule_pending_orders_synchronization();
+ }
+ }
+
+ /**
+ * Cleanup all the synchronization status information,
+ * because the process has been disabled by the user via settings,
+ * or because there's nothing left to syncrhonize.
+ */
+ public function cleanup_synchronization_state() {
+ delete_option( self::INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION );
+ delete_option( self::PENDING_SYNC_IS_IN_PROGRESS_OPTION );
+ delete_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION );
+ }
}