$value && 'yes' === $value ) {
$this->data_store->clear_all_cached_data();
}
}
/**
* Process option that enables FTS index on orders table. Tries to create an FTS index when option is enabled.
*
* @param string $option Option name.
* @param string $old_value Old value of the option.
* @param string $value New value of the option.
*
* @return void
*
* @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
*/
public function process_updated_option_fts_index( $option, $old_value, $value ) {
if ( self::HPOS_FTS_INDEX_OPTION !== $option ) {
return;
}
if ( 'yes' !== $value ) {
return;
}
if ( ! $this->custom_orders_table_usage_is_enabled() ) {
update_option( self::HPOS_FTS_INDEX_OPTION, 'no', true );
if ( class_exists( 'WC_Admin_Settings' ) ) {
WC_Admin_Settings::add_error( __( 'Failed to create FTS index on orders table. This feature is only available when High-performance order storage is enabled.', 'woocommerce' ) );
}
return;
}
if ( ! $this->db_util->fts_index_on_order_address_table_exists() ) {
$this->db_util->create_fts_index_order_address_table();
}
// Check again to see if index was actually created.
if ( $this->db_util->fts_index_on_order_address_table_exists() ) {
update_option( self::HPOS_FTS_ADDRESS_INDEX_CREATED_OPTION, 'yes', false );
} else {
update_option( self::HPOS_FTS_ADDRESS_INDEX_CREATED_OPTION, 'no', false );
if ( class_exists( 'WC_Admin_Settings ' ) ) {
WC_Admin_Settings::add_error( __( 'Failed to create FTS index on address table', 'woocommerce' ) );
}
}
if ( ! $this->db_util->fts_index_on_order_item_table_exists() ) {
$this->db_util->create_fts_index_order_item_table();
}
// Check again to see if index was actually created.
if ( $this->db_util->fts_index_on_order_item_table_exists() ) {
update_option( self::HPOS_FTS_ORDER_ITEM_INDEX_CREATED_OPTION, 'yes', false );
} else {
update_option( self::HPOS_FTS_ORDER_ITEM_INDEX_CREATED_OPTION, 'no', false );
if ( class_exists( 'WC_Admin_Settings ' ) ) {
WC_Admin_Settings::add_error( __( 'Failed to create FTS index on order item table', 'woocommerce' ) );
}
}
}
/**
* Recreate order addresses FTS index. Useful when updating to 9.4 when phone number was added to index, or when other recreating index is needed.
*
* @since 9.4.0.
*
* @return array Array with keys status (bool) and message (string).
*/
public function recreate_order_address_fts_index(): array {
$this->db_util->drop_fts_index_order_address_table();
if ( $this->db_util->fts_index_on_order_address_table_exists() ) {
return array(
'status' => false,
'message' => __( 'Failed to modify existing FTS index. Please go to WooCommerce > Status > Tools and run the "Re-create Order Address FTS index" tool.', 'woocommerce' ),
);
} else {
update_option( self::HPOS_FTS_ADDRESS_INDEX_CREATED_OPTION, 'no', false );
}
$this->db_util->create_fts_index_order_address_table();
if ( ! $this->db_util->fts_index_on_order_address_table_exists() ) {
return array(
'status' => false,
'message' => __( 'Failed to create FTS index on order address table. Please go to WooCommerce > Status > Tools and run the "Re-create Order Address FTS index" tool.', 'woocommerce' ),
);
} else {
update_option( self::HPOS_FTS_ADDRESS_INDEX_CREATED_OPTION, 'yes', false );
return array(
'status' => true,
'message' => __( 'FTS index recreated.', 'woocommerce' ),
);
}
}
/**
* 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 mixed $value New value of the setting.
* @param string $option Setting name.
* @param mixed $old_value Old value of the setting.
*
* @throws \Exception Attempt to change the authoritative orders table while orders sync is pending.
*
* @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
*/
public function process_pre_update_option( $value, $option, $old_value ) {
if ( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION === $option && $value !== $old_value ) {
$this->order_cache->flush();
return $value;
}
if ( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION !== $option ) {
return $value;
}
if ( $old_value === $value ) {
return $value;
}
$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';
}
$sync_is_pending = 0 !== $this->data_synchronizer->get_current_orders_pending_sync_count();
if ( $sync_is_pending && ! $this->changing_data_source_with_sync_pending_is_allowed() ) {
throw new \Exception( "The authoritative table for orders storage can't be changed while there are orders out of sync" );
}
return $value;
}
/**
* Callback to trigger a sync immediately by clicking a button on the Features screen.
*
* @return void
*
* @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
*/
public function sync_now() {
$section = filter_input( INPUT_GET, 'section' );
if ( 'features' !== $section ) {
return;
}
if ( filter_input( INPUT_GET, self::SYNC_QUERY_ARG, FILTER_VALIDATE_BOOLEAN ) ) {
$action = 'sync-now';
} elseif ( filter_input( INPUT_GET, self::STOP_SYNC_QUERY_ARG, FILTER_VALIDATE_BOOLEAN ) ) {
$action = 'stop-sync';
} else {
return;
}
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), "hpos-{$action}" ) ) {
WC_Admin_Settings::add_error(
'sync-now' === $action ?
esc_html__( 'Unable to start synchronization. The link you followed may have expired.', 'woocommerce' )
: esc_html__( 'Unable to stop synchronization. The link you followed may have expired.', 'woocommerce' )
);
return;
}
$this->data_cleanup->toggle_flag( false );
if ( 'sync-now' === $action ) {
$this->batch_processing_controller->enqueue_processor( DataSynchronizer::class );
} else {
$this->batch_processing_controller->remove_processor( DataSynchronizer::class );
}
}
/**
* Tell WP Admin to remove the sync query arg from the URL.
*
* @param array $query_args The query args that are removable.
*
* @return array
*
* @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
*/
public function register_removable_query_arg( $query_args ) {
$query_args[] = self::SYNC_QUERY_ARG;
$query_args[] = self::STOP_SYNC_QUERY_ARG;
return $query_args;
}
/**
* Handler for the woocommerce_after_register_post_type post,
* registers the post type for placeholder orders.
*
* @return void
*
* @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
*/
public function register_post_type_for_order_placeholders(): void {
wc_register_order_type(
DataSynchronizer::PLACEHOLDER_ORDER_POST_TYPE,
array(
'public' => false,
'exclude_from_search' => true,
'publicly_queryable' => false,
'show_ui' => false,
'show_in_menu' => false,
'show_in_nav_menus' => false,
'show_in_admin_bar' => false,
'show_in_rest' => false,
'rewrite' => false,
'query_var' => false,
'can_export' => false,
'supports' => array(),
'capabilities' => array(),
'exclude_from_order_count' => true,
'exclude_from_order_views' => true,
'exclude_from_order_reports' => true,
'exclude_from_order_sales_reports' => true,
)
);
}
/**
* Add the definition for the HPOS feature.
*
* @param FeaturesController $features_controller The instance of FeaturesController.
*
* @return void
*
* @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
*/
public function add_feature_definition( $features_controller ) {
$definition = array(
'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(),
'plugins_are_incompatible_by_default' => true,
'additional_settings' => array(
$this->get_hpos_setting_for_sync(),
),
);
$features_controller->add_feature_definition(
'custom_order_tables',
__( 'High-Performance order storage', 'woocommerce' ),
$definition
);
}
/**
* Returns the HPOS setting for rendering HPOS vs Post setting block in Features section of the settings page.
*
* @return array Feature setting object.
*/
private function get_hpos_setting_for_feature() {
if ( 'yes' === get_transient( 'wc_installing' ) ) {
return array();
}
$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 () {
$compatibility_info = $this->features_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
$sync_complete = 0 === $this->data_synchronizer->get_current_orders_pending_sync_count();
$disabled = array();
// Changing something here? You might also want to look at `enable|disable` functions in Automattic\WooCommerce\Database\Migrations\CustomOrderTable\CLIRunner.
$incompatible_plugins = $this->plugin_util->get_items_considered_incompatible( 'custom_order_tables', $compatibility_info );
$incompatible_plugins = array_diff( $incompatible_plugins, $this->plugin_util->get_plugins_excluded_from_compatibility_ui() );
if ( count( $incompatible_plugins ) > 0 ) {
$disabled = array( 'yes' );
}
if ( ! $sync_complete && ! $this->changing_data_source_with_sync_pending_is_allowed() ) {
$disabled = array( 'yes', 'no' );
}
return $disabled;
};
return array(
'id' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
'title' => __( 'Order data storage', 'woocommerce' ),
'type' => 'radio',
'options' => array(
'no' => __( 'WordPress posts storage (legacy)', 'woocommerce' ),
'yes' => __( 'High-performance order storage (recommended)', 'woocommerce' ),
),
'value' => $get_value,
'disabled' => $get_disabled,
'desc' => $get_desc,
'desc_at_end' => true,
'row_class' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
);
}
/**
* Returns the setting for rendering sync enabling setting block in Features section of the settings page.
*
* @return array Feature setting object.
*/
private function get_hpos_setting_for_sync() {
if ( 'yes' === get_transient( 'wc_installing' ) ) {
return array();
}
$get_value = function () {
return get_option( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION );
};
$get_sync_message = function () {
$orders_pending_sync_count = $this->data_synchronizer->get_current_orders_pending_sync_count( true );
$sync_in_progress = $this->batch_processing_controller->is_enqueued( get_class( $this->data_synchronizer ) );
$sync_enabled = $this->data_synchronizer->data_sync_is_enabled();
$sync_is_pending = $orders_pending_sync_count > 0;
$sync_message = array();
$is_dangerous = $sync_is_pending && $this->changing_data_source_with_sync_pending_is_allowed();
if ( $is_dangerous ) {
$sync_message[] = wp_kses_data(
sprintf(
// translators: %s: number of pending orders.
_n( "There's %s order pending sync.", 'There are %s orders pending sync.', $orders_pending_sync_count, 'woocommerce' ),
number_format_i18n( $orders_pending_sync_count ),
)
. ' '
. ''
. __( 'Switching data storage while sync is incomplete is dangerous and can lead to order data corruption or loss!', 'woocommerce' )
. ''
);
}
if ( ! $sync_enabled && $this->data_synchronizer->background_sync_is_enabled() ) {
$sync_message[] = __( 'Background sync is enabled.', 'woocommerce' );
}
if ( $sync_in_progress && $sync_is_pending ) {
$sync_message[] = sprintf(
// translators: %s: number of pending orders.
__( 'Currently syncing orders... %s pending', 'woocommerce' ),
number_format_i18n( $orders_pending_sync_count )
);
if ( ! $sync_enabled ) {
$stop_sync_url = wp_nonce_url(
add_query_arg(
array(
self::STOP_SYNC_QUERY_ARG => true,
),
wc_get_container()->get( FeaturesController::class )->get_features_page_url()
),
'hpos-stop-sync'
);
$sync_message[] = sprintf(
'%2$s',
esc_url( $stop_sync_url ),
__( 'Stop sync', 'woocommerce' )
);
}
} elseif ( $sync_is_pending ) {
$sync_now_url = wp_nonce_url(
add_query_arg(
array(
self::SYNC_QUERY_ARG => true,
),
wc_get_container()->get( FeaturesController::class )->get_features_page_url()
),
'hpos-sync-now'
);
if ( ! $is_dangerous ) {
$sync_message[] = wp_kses_data(
sprintf(
// translators: %s: number of pending orders.
_n(
"You can switch order data storage only when the posts and orders tables are in sync. There's currently %s order out of sync.",
'You can switch order data storage only when the posts and orders tables are in sync. There are currently %s orders out of sync. ',
$orders_pending_sync_count,
'woocommerce'
),
number_format_i18n( $orders_pending_sync_count )
)
);
}
$sync_message[] = sprintf(
'%2$s',
esc_url( $sync_now_url ),
__( 'Sync orders now', 'woocommerce' )
);
}
return implode( '
', $sync_message );
};
$get_description_is_error = function () {
$sync_is_pending = $this->data_synchronizer->get_current_orders_pending_sync_count( true ) > 0;
return $sync_is_pending && $this->changing_data_source_with_sync_pending_is_allowed();
};
return array(
'id' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
'title' => '',
'type' => 'checkbox',
'desc' => __( 'Enable compatibility mode (Synchronize orders between High-performance order storage and WordPress posts storage).', 'woocommerce' ),
'value' => $get_value,
'desc_tip' => $get_sync_message,
'description_is_error' => $get_description_is_error,
'row_class' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
);
}
/**
* Returns a value indicating if changing the authoritative data source for orders while there are orders pending synchronization is allowed.
*
* @return bool
*/
private function changing_data_source_with_sync_pending_is_allowed(): bool {
/**
* Filter to allow changing where order data is stored, even when there are orders pending synchronization.
*
* DANGER! This filter is intended for usage when doing manual and automated testing in development environments only,
* it should NEVER be used in production environments. Order data corruption or loss can happen!
*
* @param bool $allow True to allow changing order storage when there are orders pending synchronization, false to disallow.
* @returns bool
*
* @since 8.3.0
*/
return apply_filters( 'wc_allow_changing_orders_storage_while_sync_is_pending', false );
}
/**
* Rewrites post edit links for HPOS placeholder posts so that they go to the HPOS order itself.
* Hooked onto `get_edit_post_link`.
*
* @since 9.0.0
*
* @param string $link The edit link.
* @param int $post_id Post ID.
* @return string
*
* @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
*/
public function maybe_rewrite_order_edit_link( $link, $post_id ) {
if ( DataSynchronizer::PLACEHOLDER_ORDER_POST_TYPE === get_post_type( $post_id ) ) {
$link = OrderUtil::get_order_admin_edit_url( $post_id );
}
return $link;
}
/**
* Set the `order_objects` cache group as non-persistent if Custom Order data caching is enabled.
*
* With order datastore cache enabled, caching of raw data is now handled by the datastore, rather than full object
* being stored in persistent cache.
*
* @return void
*/
public function maybe_set_order_cache_group_as_non_persistent() {
if ( OrderUtil::custom_orders_table_datastore_cache_enabled() ) {
// If we're using datastore cache, we don't want to persist the order objects in cache. It should be in-memory only.
wp_cache_add_non_persistent_groups( array( $this->order_cache->get_object_type() ) );
}
}
}