Merged in feature/MAW-855-import-code-into-aws (pull request #2)

code import from pantheon

* code import from pantheon
This commit is contained in:
Tony Volpe
2023-12-04 23:08:14 +00:00
parent 8c9b1312bc
commit 8f4b5efda6
4766 changed files with 185592 additions and 239967 deletions

View File

@@ -81,7 +81,7 @@ abstract class CustomMetaDataStore {
*
* @return bool
*/
public function delete_meta( &$object, $meta ) {
public function delete_meta( &$object, $meta ) : bool {
global $wpdb;
if ( ! isset( $meta->id ) ) {
@@ -99,7 +99,8 @@ abstract class CustomMetaDataStore {
*
* @param WC_Data $object WC_Data object.
* @param stdClass $meta (containing ->key and ->value).
* @return int meta ID
*
* @return int|false meta ID
*/
public function add_meta( &$object, $meta ) {
global $wpdb;
@@ -132,7 +133,7 @@ abstract class CustomMetaDataStore {
*
* @return bool
*/
public function update_meta( &$object, $meta ) {
public function update_meta( &$object, $meta ) : bool {
global $wpdb;
if ( ! isset( $meta->id ) || empty( $meta->key ) ) {
@@ -194,4 +195,41 @@ abstract class CustomMetaDataStore {
return $meta;
}
/**
* Retrieves metadata by meta key.
*
* @param \WC_Abstract_Order $object Object ID.
* @param string $meta_key Meta key.
*
* @return \stdClass|bool Metadata object or FALSE if not found.
*/
public function get_metadata_by_key( &$object, string $meta_key ) {
global $wpdb;
$db_info = $this->get_db_info();
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$meta = $wpdb->get_results(
$wpdb->prepare(
"SELECT {$db_info['meta_id_field']}, meta_key, meta_value, {$db_info['object_id_field']} FROM {$db_info['table']} WHERE meta_key = %s AND {$db_info['object_id_field']} = %d",
$meta_key,
$object->get_id(),
)
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ( empty( $meta ) ) {
return false;
}
foreach ( $meta as $row ) {
if ( isset( $row->meta_value ) ) {
$row->meta_value = maybe_unserialize( $row->meta_value );
}
}
return $meta;
}
}

View File

@@ -5,20 +5,19 @@
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Caches\OrderCache;
use Automattic\WooCommerce\Caches\OrderCacheController;
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;
use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Utilities\PluginUtil;
use ActionScheduler;
defined( 'ABSPATH' ) || exit;
/**
* This is the main class that controls the custom orders tables feature. Its responsibilities are:
*
* - Allowing to enable and disable the feature while it's in development (show_feature method)
* - Displaying UI components (entries in the tools page and in settings)
* - Providing the proper data store for orders via 'woocommerce_order_data_store' hook
*
@@ -28,6 +27,8 @@ class CustomOrdersTableController {
use AccessiblePrivateMethods;
private const SYNC_QUERY_ARG = 'wc_hpos_sync_now';
/**
* The name of the option for enabling the usage of the custom orders tables
*/
@@ -117,10 +118,10 @@ class CustomOrdersTableController {
self::add_filter( 'woocommerce_debug_tools', array( $this, 'add_initiate_regeneration_entry_to_tools_array' ), 999, 1 );
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_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( $this, 'handle_data_sync_option_changed' ), 10, 1 );
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_filter( 'removable_query_args', array( $this, 'register_removable_query_arg' ) );
self::add_action( 'woocommerce_register_feature_definitions', array( $this, 'add_feature_definition' ) );
}
/**
@@ -156,33 +157,6 @@ class CustomOrdersTableController {
$this->plugin_util = $plugin_util;
}
/**
* Checks if the feature is visible (so that dedicated entries will be added to the debug tools page).
*
* @return bool True if the feature is visible.
*/
public function is_feature_visible(): bool {
return true;
}
/**
* Makes the feature visible, so that dedicated entries will be added to the debug tools page.
*
* This method shouldn't be used anymore, see the FeaturesController class.
*/
public function show_feature() {
$class_and_method = ( new \ReflectionClass( $this ) )->getShortName() . '::' . __FUNCTION__;
wc_doing_it_wrong(
$class_and_method,
sprintf(
// translators: %1$s the name of the class and method used.
__( '%1$s: The visibility of the custom orders table feature is now handled by the WooCommerce features engine. See the FeaturesController class, or go to WooCommerce - Settings - Advanced - Features.', 'woocommerce' ),
$class_and_method
),
'7.0'
);
}
/**
* Is the custom orders table usage enabled via settings?
* This can be true only if the feature is enabled and a table regeneration has been completed.
@@ -190,7 +164,7 @@ class CustomOrdersTableController {
* @return bool True if the custom orders table usage is enabled
*/
public function custom_orders_table_usage_is_enabled(): bool {
return $this->is_feature_visible() && get_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ) === 'yes';
return get_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ) === 'yes';
}
/**
@@ -276,23 +250,6 @@ class CustomOrdersTableController {
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( bool $verify_nonce = true ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
if ( $verify_nonce && ( ! isset( $_REQUEST['_wpnonce'] ) || wp_verify_nonce( $_REQUEST['_wpnonce'], 'debug_action' ) === false ) ) {
throw new \Exception( 'Invalid nonce' );
}
$this->data_synchronizer->create_database_tables();
update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' );
}
/**
* Delete the custom orders tables and any related options and data in response to the user pressing the tool button.
*
@@ -336,12 +293,19 @@ class CustomOrdersTableController {
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;
}
$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).
$sync_is_pending = 0 !== $this->data_synchronizer->get_current_orders_pending_sync_count();
@@ -354,47 +318,32 @@ class CustomOrdersTableController {
}
/**
* Handler for the all settings updated hook.
* Callback to trigger a sync immediately by clicking a button on the Features screen.
*
* @param string $feature_id Feature ID.
* @return void
*/
private function handle_data_sync_option_changed( string $feature_id ) {
if ( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION !== $feature_id ) {
private function sync_now() {
$section = filter_input( INPUT_GET, 'section' );
if ( 'features' !== $section ) {
return;
}
$data_sync_is_enabled = $this->data_synchronizer->data_sync_is_enabled();
if ( ! $this->data_synchronizer->check_orders_table_exists() ) {
$this->data_synchronizer->create_database_tables();
}
// Enabling/disabling the sync implies starting/stopping 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 (and the opposite: sync will be stopped if for
// some reason it was ongoing while it was disabled).
if ( $data_sync_is_enabled ) {
if ( filter_input( INPUT_GET, self::SYNC_QUERY_ARG, FILTER_VALIDATE_BOOLEAN ) ) {
$this->batch_processing_controller->enqueue_processor( DataSynchronizer::class );
} else {
$this->batch_processing_controller->remove_processor( DataSynchronizer::class );
}
}
/**
* Handle the 'woocommerce_feature_enabled_changed' action,
* if the custom orders table feature is enabled create the database tables if they don't exist.
* Tell WP Admin to remove the sync query arg from the URL.
*
* @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.
* @param array $query_args The query args that are removable.
*
* @return array
*/
private function handle_feature_enabled_changed( $feature_id, $is_enabled ): void {
if ( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION !== $feature_id || ! $is_enabled ) {
return;
}
private function register_removable_query_arg( $query_args ) {
$query_args[] = self::SYNC_QUERY_ARG;
if ( ! $this->data_synchronizer->check_orders_table_exists() ) {
$this->create_custom_orders_tables( false );
}
return $query_args;
}
/**
@@ -429,105 +378,160 @@ 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 string $feature_id ID of the feature.
* @param FeaturesController $features_controller The instance of FeaturesController.
*
* @return array Feature setting object.
* @return void
*/
private function get_hpos_feature_setting( array $feature_setting, string $feature_id ) {
if ( ! in_array( $feature_id, array( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, 'custom_order_tables' ), true ) ) {
return $feature_setting;
}
private 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(),
'additional_settings' => array(
$this->get_hpos_setting_for_sync(),
),
);
if ( 'yes' === get_transient( 'wc_installing' ) ) {
return $feature_setting;
}
$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();
}
$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.
*
* @param array $sync_status Details of sync status, includes pending count, and count when sync started.
*
* @return array Feature setting object.
*/
private function get_hpos_setting_for_feature( $sync_status ) {
$hpos_enabled = $this->custom_orders_table_usage_is_enabled();
$plugin_info = $this->features_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
$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();
if ( count( array_merge( $plugin_info['uncertain'], $plugin_info['incompatible'] ) ) > 0 ) {
$disabled_option = array( 'yes' );
}
if ( ! $sync_complete ) {
$disabled_option = array( 'yes', 'no' );
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() {
$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(
'id' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
'title' => __( 'Data storage for orders', 'woocommerce' ),
'title' => __( 'Order data storage', 'woocommerce' ),
'type' => 'radio',
'options' => array(
'no' => __( 'WordPress post tables', 'woocommerce' ),
'yes' => __( 'High performance order storage (new)', 'woocommerce' ),
'no' => __( 'WordPress posts storage (legacy)', 'woocommerce' ),
'yes' => __( 'High-performance order storage (recommended)', 'woocommerce' ),
),
'value' => $hpos_enabled ? 'yes' : 'no',
'disabled' => $disabled_option,
'desc' => $plugin_incompat_warning,
'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.
*
* @param array $sync_status Details of sync status, includes pending count, and count when sync started.
*
* @return array Feature setting object.
*/
private function get_hpos_setting_for_sync( $sync_status ) {
$sync_in_progress = $this->batch_processing_controller->is_enqueued( get_class( $this->data_synchronizer ) );
$sync_enabled = get_option( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION );
$sync_message = '';
if ( $sync_in_progress && $sync_status['current_pending_count'] > 0 ) {
$sync_message = sprintf(
// 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_message = sprintf(
// translators: %d: number of pending orders.
_n(
'Sync %d pending order. You can switch data storage for orders only when posts and orders table are in sync.',
'Sync %d pending orders. You can switch data storage for orders only when posts and orders table are in sync.',
$sync_status['current_pending_count'],
'woocommerce'
),
$sync_status['current_pending_count'],
);
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() {
$sync_status = $this->data_synchronizer->get_sync_status();
$sync_in_progress = $this->batch_processing_controller->is_enqueued( get_class( $this->data_synchronizer ) );
$sync_enabled = $this->data_synchronizer->data_sync_is_enabled();
$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 ) {
$sync_message[] = sprintf(
// 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(
__(
'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(
'id' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
'title' => '',
'type' => 'checkbox',
'desc' => __( 'Keep the posts and orders tables in sync (compatibility mode).', 'woocommerce' ),
'value' => $sync_enabled,
'desc_tip' => $sync_message,
'id' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
'title' => '',
'type' => 'checkbox',
'desc' => __( 'Enable compatibility mode (synchronizes orders to the posts table).', 'woocommerce' ),
'value' => $get_value,
'desc_tip' => $get_sync_message,
'row_class' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
);
}
}

View File

@@ -8,7 +8,7 @@ namespace Automattic\WooCommerce\Internal\DataStores\Orders;
use Automattic\WooCommerce\Caches\OrderCache;
use Automattic\WooCommerce\Caches\OrderCacheController;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessorInterface;
use Automattic\WooCommerce\Internal\BatchProcessing\{ BatchProcessingController, BatchProcessorInterface };
use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
@@ -45,6 +45,13 @@ class DataSynchronizer implements BatchProcessorInterface {
public const ID_TYPE_DELETED_FROM_ORDERS_TABLE = 3;
public const ID_TYPE_DELETED_FROM_POSTS_TABLE = 4;
public const BACKGROUND_SYNC_MODE_OPTION = 'woocommerce_custom_orders_table_background_sync_mode';
public const BACKGROUND_SYNC_INTERVAL_OPTION = 'woocommerce_custom_orders_table_background_sync_interval';
public const BACKGROUND_SYNC_MODE_INTERVAL = 'interval';
public const BACKGROUND_SYNC_MODE_CONTINUOUS = 'continuous';
public const BACKGROUND_SYNC_MODE_OFF = 'off';
public const BACKGROUND_SYNC_EVENT_HOOK = 'woocommerce_custom_orders_table_background_sync';
/**
* The data store object to use.
*
@@ -80,6 +87,13 @@ class DataSynchronizer implements BatchProcessorInterface {
*/
private $order_cache_controller;
/**
* The batch processing controller.
*
* @var BatchProcessingController
*/
private $batch_processing_controller;
/**
* Class constructor.
*/
@@ -89,6 +103,13 @@ class DataSynchronizer implements BatchProcessorInterface {
self::add_action( 'woocommerce_refund_created', array( $this, 'handle_updated_order' ), 100 );
self::add_action( 'woocommerce_update_order', array( $this, 'handle_updated_order' ), 100 );
self::add_action( 'wp_scheduled_auto_draft_delete', array( $this, 'delete_auto_draft_orders' ), 9 );
self::add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 );
self::add_filter( 'added_option', array( $this, 'process_added_option' ), 999, 2 );
self::add_filter( 'deleted_option', array( $this, 'process_deleted_option' ), 999 );
self::add_action( self::BACKGROUND_SYNC_EVENT_HOOK, array( $this, 'handle_interval_background_sync' ) );
if ( self::BACKGROUND_SYNC_MODE_CONTINUOUS === $this->get_background_sync_mode() ) {
self::add_action( 'shutdown', array( $this, 'handle_continuous_background_sync' ) );
}
self::add_filter( 'woocommerce_feature_description_tip', array( $this, 'handle_feature_description_tip' ), 10, 3 );
}
@@ -101,6 +122,7 @@ class DataSynchronizer implements BatchProcessorInterface {
* @param PostsToOrdersMigrationController $posts_to_cot_migrator The posts to COT migration class to use.
* @param LegacyProxy $legacy_proxy The legacy proxy instance to use.
* @param OrderCacheController $order_cache_controller The order cache controller instance to use.
* @param BatchProcessingController $batch_processing_controller The batch processing controller to use.
* @internal
*/
final public function init(
@@ -108,13 +130,15 @@ class DataSynchronizer implements BatchProcessorInterface {
DatabaseUtil $database_util,
PostsToOrdersMigrationController $posts_to_cot_migrator,
LegacyProxy $legacy_proxy,
OrderCacheController $order_cache_controller
OrderCacheController $order_cache_controller,
BatchProcessingController $batch_processing_controller
) {
$this->data_store = $data_store;
$this->database_util = $database_util;
$this->posts_to_cot_migrator = $posts_to_cot_migrator;
$this->error_logger = $legacy_proxy->call_function( 'wc_get_logger' );
$this->order_cache_controller = $order_cache_controller;
$this->data_store = $data_store;
$this->database_util = $database_util;
$this->posts_to_cot_migrator = $posts_to_cot_migrator;
$this->error_logger = $legacy_proxy->call_function( 'wc_get_logger' );
$this->order_cache_controller = $order_cache_controller;
$this->batch_processing_controller = $batch_processing_controller;
}
/**
@@ -151,11 +175,19 @@ class DataSynchronizer implements BatchProcessorInterface {
}
/**
* Create the custom orders database tables.
* Create the custom orders database tables and log an error if that's not possible.
*
* @return bool True if all the tables were successfully created, false otherwise.
*/
public function create_database_tables() {
$this->database_util->dbdelta( $this->data_store->get_database_schema() );
$this->check_orders_table_exists();
$success = $this->check_orders_table_exists();
if ( ! $success ) {
$missing_tables = $this->database_util->get_missing_tables( $this->data_store->get_database_schema() );
$missing_tables = implode( ', ', $missing_tables );
$this->error_logger->error( "HPOS tables are missing in the database and couldn't be created. The missing tables are: $missing_tables" );
}
return $success;
}
/**
@@ -171,7 +203,7 @@ class DataSynchronizer implements BatchProcessorInterface {
}
/**
* Is the data sync between old and new tables currently enabled?
* Is the real-time data sync between old and new tables currently enabled?
*
* @return bool
*/
@@ -179,6 +211,181 @@ class DataSynchronizer implements BatchProcessorInterface {
return 'yes' === get_option( self::ORDERS_DATA_SYNC_ENABLED_OPTION );
}
/**
* Get the current background data sync mode.
*
* @return string
*/
public function get_background_sync_mode(): string {
$default = $this->data_sync_is_enabled() ? self::BACKGROUND_SYNC_MODE_INTERVAL : self::BACKGROUND_SYNC_MODE_OFF;
return get_option( self::BACKGROUND_SYNC_MODE_OPTION, $default );
}
/**
* Is the background data sync between old and new tables currently enabled?
*
* @return bool
*/
public function background_sync_is_enabled(): bool {
$enabled_modes = array( self::BACKGROUND_SYNC_MODE_INTERVAL, self::BACKGROUND_SYNC_MODE_CONTINUOUS );
$mode = $this->get_background_sync_mode();
return in_array( $mode, $enabled_modes, true );
}
/**
* Process an option change for specific keys.
*
* @param string $option_key The option key.
* @param string $old_value The previous value.
* @param string $new_value The new value.
*
* @return void
*/
private function process_updated_option( $option_key, $old_value, $new_value ) {
$sync_option_keys = array( self::ORDERS_DATA_SYNC_ENABLED_OPTION, self::BACKGROUND_SYNC_MODE_OPTION );
if ( ! in_array( $option_key, $sync_option_keys, true ) || $new_value === $old_value ) {
return;
}
if ( self::BACKGROUND_SYNC_MODE_OPTION === $option_key ) {
$mode = $new_value;
} else {
$mode = $this->get_background_sync_mode();
}
switch ( $mode ) {
case self::BACKGROUND_SYNC_MODE_INTERVAL:
$this->schedule_background_sync();
break;
case self::BACKGROUND_SYNC_MODE_CONTINUOUS:
case self::BACKGROUND_SYNC_MODE_OFF:
default:
$this->unschedule_background_sync();
break;
}
if ( self::ORDERS_DATA_SYNC_ENABLED_OPTION === $option_key ) {
if ( ! $this->check_orders_table_exists() ) {
$this->create_database_tables();
}
if ( $this->data_sync_is_enabled() ) {
$this->batch_processing_controller->enqueue_processor( self::class );
} else {
$this->batch_processing_controller->remove_processor( self::class );
}
}
}
/**
* Process an option change when the key didn't exist before.
*
* @param string $option_key The option key.
* @param string $value The new value.
*
* @return void
*/
private function process_added_option( $option_key, $value ) {
$this->process_updated_option( $option_key, false, $value );
}
/**
* Process an option deletion for specific keys.
*
* @param string $option_key The option key.
*
* @return void
*/
private function process_deleted_option( $option_key ) {
if ( self::BACKGROUND_SYNC_MODE_OPTION !== $option_key ) {
return;
}
$this->unschedule_background_sync();
$this->batch_processing_controller->remove_processor( self::class );
}
/**
* Get the time interval, in seconds, between background syncs.
*
* @return int
*/
public function get_background_sync_interval(): int {
$interval = filter_var(
get_option( self::BACKGROUND_SYNC_INTERVAL_OPTION, HOUR_IN_SECONDS ),
FILTER_VALIDATE_INT,
array(
'options' => array(
'default' => HOUR_IN_SECONDS,
),
)
);
return $interval;
}
/**
* Schedule an event to run background sync when the mode is set to interval.
*
* @return void
*/
private function schedule_background_sync() {
$interval = $this->get_background_sync_interval();
// Calling Action Scheduler directly because WC_Action_Queue doesn't support the unique parameter yet.
as_schedule_recurring_action(
time() + $interval,
$interval,
self::BACKGROUND_SYNC_EVENT_HOOK,
array(),
'',
true
);
}
/**
* Remove any pending background sync events.
*
* @return void
*/
private function unschedule_background_sync() {
WC()->queue()->cancel_all( self::BACKGROUND_SYNC_EVENT_HOOK );
}
/**
* Callback to check for pending syncs and enqueue the background data sync processor when in interval mode.
*
* @return void
*/
private function handle_interval_background_sync() {
if ( self::BACKGROUND_SYNC_MODE_INTERVAL !== $this->get_background_sync_mode() ) {
$this->unschedule_background_sync();
return;
}
$pending_count = $this->get_total_pending_count();
if ( $pending_count > 0 ) {
$this->batch_processing_controller->enqueue_processor( self::class );
}
}
/**
* Callback to keep the background data sync processor enqueued when in continuous mode.
*
* @return void
*/
private function handle_continuous_background_sync() {
if ( self::BACKGROUND_SYNC_MODE_CONTINUOUS !== $this->get_background_sync_mode() ) {
$this->batch_processing_controller->remove_processor( self::class );
return;
}
// This method already checks if a processor is enqueued before adding it to avoid duplication.
$this->batch_processing_controller->enqueue_processor( self::class );
}
/**
* Get the current sync process status.
* The information is meaningful only if pending_data_sync_is_in_progress return true.
@@ -251,12 +458,15 @@ class DataSynchronizer implements BatchProcessorInterface {
}
if ( $this->custom_orders_table_is_authoritative() ) {
$missing_orders_count_sql = "
$missing_orders_count_sql = $wpdb->prepare(
"
SELECT COUNT(1) FROM $wpdb->posts posts
INNER JOIN $orders_table orders ON posts.id=orders.id
WHERE posts.post_type = '" . self::PLACEHOLDER_ORDER_POST_TYPE . "'
AND orders.status not in ( 'auto-draft' )
";
AND orders.type IN ($order_post_type_placeholder)",
$order_post_types
);
$operator = '>';
} else {
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_post_type_placeholder is prepared.
@@ -374,13 +584,16 @@ ORDER BY posts.ID ASC",
// phpcs:enable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
break;
case self::ID_TYPE_MISSING_IN_POSTS_TABLE:
$sql = "
$sql = $wpdb->prepare(
"
SELECT posts.ID FROM $wpdb->posts posts
INNER JOIN $orders_table orders ON posts.id=orders.id
WHERE posts.post_type = '" . self::PLACEHOLDER_ORDER_POST_TYPE . "'
AND orders.status not in ( 'auto-draft' )
ORDER BY posts.id ASC
";
AND orders.type IN ($order_post_type_placeholders)
ORDER BY posts.id ASC",
$order_post_types
);
break;
case self::ID_TYPE_DIFFERENT_UPDATE_DATE:
$operator = $this->custom_orders_table_is_authoritative() ? '>' : '<';

View File

@@ -7,6 +7,7 @@ namespace Automattic\WooCommerce\Internal\DataStores\Orders;
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Caches\OrderCache;
use Automattic\WooCommerce\Internal\Admin\Orders\EditLock;
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\ArrayUtil;
@@ -23,12 +24,19 @@ defined( 'ABSPATH' ) || exit;
class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements \WC_Object_Data_Store_Interface, \WC_Order_Data_Store_Interface {
/**
* Order IDs for which we are checking read on sync in the current request.
* Order IDs for which we are checking sync on read in the current request. In WooCommerce, using wc_get_order is a very common pattern, to avoid performance issues, we only sync on read once per request per order. This works because we consider out of sync orders to be an anomaly, so we don't recommend running HPOS with incompatible plugins.
*
* @var array.
*/
private static $reading_order_ids = array();
/**
* Keep track of order IDs that are actively being backfilled. We use this to prevent further read on sync from add_|update_|delete_postmeta etc hooks. If we allow this, then we would end up syncing the same order multiple times as it is being backfilled.
*
* @var array
*/
private static $backfilling_order_ids = array();
/**
* Data stored in meta keys, but not considered "meta" for an order.
*
@@ -509,24 +517,33 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
return $this->all_order_column_mapping;
}
/**
* Helper function to get alias for order table, this is used in select query.
*
* @return string Alias.
*/
private function get_order_table_alias() : string {
return 'o';
}
/**
* Helper function to get alias for op table, this is used in select query.
*
* @return string Alias.
*/
private function get_op_table_alias() : string {
return 'order_operational_data';
return 'p';
}
/**
* Helper function to get alias for address table, this is used in select query.
*
* @param string $type Address type.
* @param string $type Type of address; 'billing' or 'shipping'.
*
* @return string Alias.
*/
private function get_address_table_alias( string $type ) : string {
return "address_$type";
return 'billing' === $type ? 'b' : 's';
}
/**
@@ -562,7 +579,18 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
return;
}
$cpt_data_store->update_order_from_object( $order );
self::$backfilling_order_ids[] = $order->get_id();
$this->update_order_meta_from_object( $order );
$order_class = get_class( $order );
$post_order = new $order_class();
$post_order->set_id( $order->get_id() );
$cpt_data_store->read( $post_order );
// This compares the order data to the post data and set changes array for props that are changed.
$post_order->set_props( $order->get_data() );
$cpt_data_store->update_order_from_object( $post_order );
foreach ( $cpt_data_store->get_internal_data_store_key_getters() as $key => $getter_name ) {
if (
is_callable( array( $cpt_data_store, "set_$getter_name" ) ) &&
@@ -580,6 +608,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
);
}
}
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $order->get_id() ) );
}
/**
@@ -762,6 +791,47 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
$this->set_stock_reduced( $order, $set );
}
/**
* Get token ids for an order.
*
* @param WC_Order $order Order object.
* @return array
*/
public function get_payment_token_ids( $order ) {
/**
* We don't store _payment_tokens in props to preserve backward compatibility. In CPT data store, `_payment_tokens` is always fetched directly from DB instead of from prop.
*/
$payment_tokens = $this->data_store_meta->get_metadata_by_key( $order, '_payment_tokens' );
if ( $payment_tokens ) {
$payment_tokens = $payment_tokens[0]->meta_value;
}
if ( ! $payment_tokens && version_compare( $order->get_version(), '8.0.0', '<' ) ) {
// Before 8.0 we were incorrectly storing payment_tokens in the order meta. So we need to check there too.
$payment_tokens = get_post_meta( $order->get_id(), '_payment_tokens', true );
}
return array_filter( (array) $payment_tokens );
}
/**
* Update token ids for an order.
*
* @param WC_Order $order Order object.
* @param array $token_ids Payment token ids.
*/
public function update_payment_token_ids( $order, $token_ids ) {
$meta = new \WC_Meta_Data();
$meta->key = '_payment_tokens';
$meta->value = $token_ids;
$existing_meta = $this->data_store_meta->get_metadata_by_key( $order, '_payment_tokens' );
if ( $existing_meta ) {
$existing_meta = $existing_meta[0];
$meta->id = $existing_meta->id;
$this->data_store_meta->update_meta( $order, $meta );
} else {
$this->data_store_meta->add_meta( $order, $meta );
}
}
/**
* Get amount already refunded.
*
@@ -1051,8 +1121,20 @@ WHERE
}
$data_sync_enabled = $data_synchronizer->data_sync_is_enabled();
$load_posts_for = array_diff( $order_ids, self::$reading_order_ids );
$post_orders = $data_sync_enabled ? $this->get_post_orders_for_ids( array_intersect_key( $orders, array_flip( $load_posts_for ) ) ) : array();
if ( $data_sync_enabled ) {
/**
* Allow opportunity to disable sync on read, while keeping sync on write enabled. This adds another step as a large shop progresses from full sync to no sync with HPOS authoritative.
* This filter is only executed if data sync is enabled from settings in the first place as it's meant to be a step between full sync -> no sync, rather than be a control for enabling just the sync on read. Sync on read without sync on write is problematic as any update will reset on the next read, but sync on write without sync on read is fine.
*
* @param bool $read_on_sync_enabled Whether to sync on read.
*
* @since 8.1.0
*/
$data_sync_enabled = apply_filters( 'woocommerce_hpos_enable_sync_on_read', $data_sync_enabled );
}
$load_posts_for = array_diff( $order_ids, array_merge( self::$reading_order_ids, self::$backfilling_order_ids ) );
$post_orders = $data_sync_enabled ? $this->get_post_orders_for_ids( array_intersect_key( $orders, array_flip( $load_posts_for ) ) ) : array();
foreach ( $data as $order_data ) {
$order_id = absint( $order_data->id );
@@ -1446,14 +1528,19 @@ WHERE
* @return \stdClass[]|object|null DB Order objects or error.
*/
protected function get_order_data_for_ids( $ids ) {
if ( ! $ids ) {
global $wpdb;
if ( ! $ids || empty( $ids ) ) {
return array();
}
global $wpdb;
if ( empty( $ids ) ) {
return array();
}
$table_aliases = array(
'orders' => $this->get_order_table_alias(),
'billing_address' => $this->get_address_table_alias( 'billing' ),
'shipping_address' => $this->get_address_table_alias( 'shipping' ),
'operational_data' => $this->get_op_table_alias(),
);
$order_table_alias = $table_aliases['orders'];
$order_table_query = $this->get_order_table_select_statement();
$id_placeholder = implode( ', ', array_fill( 0, count( $ids ), '%d' ) );
$order_meta_table = self::get_meta_table_name();
@@ -1461,7 +1548,7 @@ WHERE
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_table_query is autogenerated and should already be prepared.
$table_data = $wpdb->get_results(
$wpdb->prepare(
"$order_table_query WHERE wc_order.id in ( $id_placeholder )",
"$order_table_query WHERE $order_table_alias.id in ( $id_placeholder )",
$ids
)
);
@@ -1476,9 +1563,27 @@ WHERE
$ids
)
);
foreach ( $table_data as $table_datum ) {
$order_data[ $table_datum->id ] = $table_datum;
$order_data[ $table_datum->id ]->meta_data = array();
$id = $table_datum->{"{$order_table_alias}_id"};
$order_data[ $id ] = new \stdClass();
foreach ( $this->get_all_order_column_mappings() as $table_name => $column_mappings ) {
$table_alias = $table_aliases[ $table_name ];
// This remapping is required to keep the query length small enough to be supported by implementations such as HyperDB (i.e. fetching some tables in join via alias.*, while others via full name). We can revert this commit if HyperDB starts supporting SRTM for query length more than 3076 characters.
foreach ( $column_mappings as $field => $map ) {
$field_name = $map['name'] ?? "{$table_name}_$field";
if ( property_exists( $table_datum, $field_name ) ) {
$field_value = $table_datum->{ $field_name }; // Unique column, field name is different prop name.
} elseif ( property_exists( $table_datum, "{$table_alias}_$field" ) ) {
$field_value = $table_datum->{"{$table_alias}_$field"}; // Non-unique column (billing, shipping etc).
} else {
$field_value = $table_datum->{ $field }; // Unique column, field name is same as prop name.
}
$order_data[ $id ]->{$field_name} = $field_value;
}
}
$order_data[ $id ]->id = $id;
$order_data[ $id ]->meta_data = array();
}
foreach ( $meta_data as $meta_datum ) {
@@ -1500,8 +1605,7 @@ WHERE
*/
private function get_order_table_select_statement() {
$order_table = $this::get_orders_table_name();
$order_table_alias = 'wc_order';
$select_clause = $this->generate_select_clause_for_props( $order_table_alias, $this->order_column_mapping );
$order_table_alias = $this->get_order_table_alias();
$billing_address_table_alias = $this->get_address_table_alias( 'billing' );
$shipping_address_table_alias = $this->get_address_table_alias( 'shipping' );
$op_data_table_alias = $this->get_op_table_alias();
@@ -1509,8 +1613,12 @@ WHERE
$shipping_address_clauses = $this->join_shipping_address_table_to_order_query( $order_table_alias, $shipping_address_table_alias );
$operational_data_clauses = $this->join_operational_data_table_to_order_query( $order_table_alias, $op_data_table_alias );
/**
* We fully spell out address table columns because they have duplicate columns for billing and shipping and would be overwritten if we don't spell them out. There is not such duplication in the operational data table and orders table, so select with `alias`.* is fine.
* We do spell ID columns manually, as they are duplicate.
*/
return "
SELECT $select_clause, {$billing_address_clauses['select']}, {$shipping_address_clauses['select']}, {$operational_data_clauses['select']}
SELECT $order_table_alias.id as o_id, $op_data_table_alias.id as p_id, $order_table_alias.*, {$billing_address_clauses['select']}, {$shipping_address_clauses['select']}, $op_data_table_alias.*
FROM $order_table $order_table_alias
LEFT JOIN {$billing_address_clauses['join']}
LEFT JOIN {$shipping_address_clauses['join']}
@@ -1558,7 +1666,7 @@ FROM $order_meta_table
/**
* Helper method to generate join and select query for address table.
*
* @param string $address_type Type of address. Typically will be `billing` or `shipping`.
* @param string $address_type Type of address; 'billing' or 'shipping'.
* @param string $order_table_alias Alias of order table to use.
* @param string $address_table_alias Alias for address table to use.
*
@@ -1656,9 +1764,11 @@ FROM $order_meta_table
if ( 'create' === $context ) {
$post_id = wp_insert_post(
array(
'post_type' => $data_sync->data_sync_is_enabled() ? $order->get_type() : $data_sync::PLACEHOLDER_ORDER_POST_TYPE,
'post_status' => 'draft',
'post_parent' => $order->get_changes()['parent_id'] ?? $order->get_data()['parent_id'] ?? 0,
'post_type' => $data_sync->data_sync_is_enabled() ? $order->get_type() : $data_sync::PLACEHOLDER_ORDER_POST_TYPE,
'post_status' => 'draft',
'post_parent' => $order->get_changes()['parent_id'] ?? $order->get_data()['parent_id'] ?? 0,
'post_date' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ),
'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
)
);
@@ -1796,8 +1906,20 @@ FROM $order_meta_table
if ( $row ) {
$result[] = array(
'table' => self::get_orders_table_name(),
'data' => array_merge( $row['data'], array( 'id' => $order->get_id() ) ),
'format' => array_merge( $row['format'], array( 'id' => '%d' ) ),
'data' => array_merge(
$row['data'],
array(
'id' => $order->get_id(),
'type' => $order->get_type(),
)
),
'format' => array_merge(
$row['format'],
array(
'id' => '%d',
'type' => '%s',
)
),
);
}
@@ -1866,8 +1988,6 @@ FROM $order_meta_table
protected function get_db_row_from_order( $order, $column_mapping, $only_changes = false ) {
$changes = $only_changes ? $order->get_changes() : array_merge( $order->get_data(), $order->get_changes() );
$changes['type'] = $order->get_type();
// Make sure 'status' is correctly prefixed.
if ( array_key_exists( 'status', $column_mapping ) && array_key_exists( 'status', $changes ) ) {
$changes['status'] = $this->get_post_status( $order );
@@ -2101,16 +2221,6 @@ FROM $order_meta_table
'_wp_trash_meta_time' => time(),
);
foreach ( $trash_metadata as $meta_key => $meta_value ) {
$this->add_meta(
$order,
(object) array(
'key' => $meta_key,
'value' => $meta_value,
)
);
}
$wpdb->update(
self::get_orders_table_name(),
array(
@@ -2124,6 +2234,16 @@ FROM $order_meta_table
$order->set_status( 'trash' );
foreach ( $trash_metadata as $meta_key => $meta_value ) {
$this->add_meta(
$order,
(object) array(
'key' => $meta_key,
'value' => $meta_value,
)
);
}
$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
if ( $data_synchronizer->data_sync_is_enabled() ) {
wp_trash_post( $order->get_id() );
@@ -2255,6 +2375,11 @@ FROM $order_meta_table
$this->persist_save( $order );
// Do not fire 'woocommerce_new_order' for draft statuses for backwards compatibility.
if ( 'auto-draft' === $order->get_status( 'edit' ) ) {
return;
}
/**
* Fires when a new order is created.
*
@@ -2287,15 +2412,22 @@ FROM $order_meta_table
$order->set_date_created( time() );
}
$this->update_order_meta( $order );
if ( ! $order->get_date_modified( 'edit' ) ) {
$order->set_date_modified( current_time( 'mysql' ) );
}
$this->persist_order_to_db( $order, $force_all_fields );
$this->update_order_meta( $order );
$order->save_meta_data();
$order->apply_changes();
if ( $backfill ) {
$this->maybe_backfill_post_record( $order );
self::$backfilling_order_ids[] = $order->get_id();
$r_order = wc_get_order( $order->get_id() ); // Refresh order to account for DB changes from post hooks.
$this->maybe_backfill_post_record( $r_order );
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $order->get_id() ) );
}
$this->clear_caches( $order );
}
@@ -2306,6 +2438,9 @@ FROM $order_meta_table
* @param \WC_Order $order Order object.
*/
public function update( &$order ) {
$previous_status = ArrayUtil::get_value_or_default( $order->get_data(), 'status' );
$changes = $order->get_changes();
// Before updating, ensure date paid is set if missing.
if (
! $order->get_date_paid( 'edit' )
@@ -2339,6 +2474,18 @@ FROM $order_meta_table
$order->apply_changes();
$this->clear_caches( $order );
// For backwards compatibility, moving an auto-draft order to a valid status triggers the 'woocommerce_new_order' hook.
if ( ! empty( $changes['status'] ) && 'auto-draft' === $previous_status ) {
do_action( 'woocommerce_new_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
return;
}
// For backwards compat with CPT, trashing/untrashing and changing previously datastore-level props does not trigger the update hook.
if ( ( ! empty( $changes['status'] ) && in_array( 'trash', array( $changes['status'], $previous_status ), true ) )
|| ( ! empty( $changes ) && ! array_diff_key( $changes, array_flip( $this->get_post_data_store_for_backfill()->get_internal_data_store_key_getters() ) ) ) ) {
return;
}
do_action( 'woocommerce_update_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
}
@@ -2370,19 +2517,33 @@ FROM $order_meta_table
$changes = $order->get_changes();
if ( ! isset( $changes['date_modified'] ) ) {
$order->set_date_modified( time() );
$order->set_date_modified( current_time( 'mysql' ) );
}
$this->persist_order_to_db( $order );
$order->save_meta_data();
if ( $backfill ) {
$this->maybe_backfill_post_record( $order );
self::$backfilling_order_ids[] = $order->get_id();
$this->clear_caches( $order );
$r_order = wc_get_order( $order->get_id() ); // Refresh order to account for DB changes from post hooks.
$this->maybe_backfill_post_record( $r_order );
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $order->get_id() ) );
}
return $changes;
}
/**
* Helper method to check whether to backfill post record.
*
* @return bool
*/
private function should_backfill_post_record() {
$data_sync = wc_get_container()->get( DataSynchronizer::class );
return $data_sync->data_sync_is_enabled();
}
/**
* Helper function to decide whether to backfill post record.
*
@@ -2391,8 +2552,7 @@ FROM $order_meta_table
* @return void
*/
private function maybe_backfill_post_record( $order ) {
$data_sync = wc_get_container()->get( DataSynchronizer::class );
if ( $data_sync->data_sync_is_enabled() ) {
if ( $this->should_backfill_post_record() ) {
$this->backfill_post_record( $order );
}
}
@@ -2421,8 +2581,10 @@ FROM $order_meta_table
private function update_address_index_meta( $order, $changes ) {
// If address changed, store concatenated version to make searches faster.
foreach ( array( 'billing', 'shipping' ) as $address_type ) {
if ( isset( $changes[ $address_type ] ) ) {
$order->update_meta_data( "_{$address_type}_address_index", implode( ' ', $order->get_address( $address_type ) ) );
$index_meta_key = "_{$address_type}_address_index";
if ( isset( $changes[ $address_type ] ) || ( is_a( $order, 'WC_Order' ) && empty( $order->get_meta( $index_meta_key ) ) ) ) {
$order->update_meta_data( $index_meta_key, implode( ' ', $order->get_address( $address_type ) ) );
}
}
}
@@ -2584,6 +2746,10 @@ FROM $order_meta_table
$operational_data_table_name = $this->get_operational_data_table_name();
$meta_table = $this->get_meta_table_name();
$max_index_length = $this->database_util->get_max_index_length();
$composite_meta_value_index_length = max( $max_index_length - 8 - 100 - 1, 20 ); // 8 for order_id, 100 for meta_key, 10 minimum for meta_value.
$composite_customer_id_email_length = max( $max_index_length - 20, 20 ); // 8 for customer_id, 20 minimum for email.
$sql = "
CREATE TABLE $orders_table_name (
id bigint(20) unsigned,
@@ -2606,9 +2772,9 @@ CREATE TABLE $orders_table_name (
PRIMARY KEY (id),
KEY status (status),
KEY date_created (date_created_gmt),
KEY customer_id_billing_email (customer_id, billing_email),
KEY billing_email (billing_email),
KEY type_status (type, status),
KEY customer_id_billing_email (customer_id, billing_email({$composite_customer_id_email_length})),
KEY billing_email (billing_email($max_index_length)),
KEY type_status_date (type, status, date_created_gmt),
KEY parent_order_id (parent_order_id),
KEY date_updated (date_updated_gmt)
) $collate;
@@ -2629,7 +2795,7 @@ CREATE TABLE $addresses_table_name (
phone varchar(100) null,
KEY order_id (order_id),
UNIQUE KEY address_type_order_id (address_type, order_id),
KEY email (email),
KEY email (email($max_index_length)),
KEY phone (phone)
) $collate;
CREATE TABLE $operational_data_table_name (
@@ -2646,10 +2812,10 @@ CREATE TABLE $operational_data_table_name (
order_stock_reduced tinyint(1) NULL,
date_paid_gmt datetime NULL,
date_completed_gmt datetime NULL,
shipping_tax_amount decimal(26, 8) NULL,
shipping_total_amount decimal(26, 8) NULL,
discount_tax_amount decimal(26, 8) NULL,
discount_total_amount decimal(26, 8) NULL,
shipping_tax_amount decimal(26,8) NULL,
shipping_total_amount decimal(26,8) NULL,
discount_tax_amount decimal(26,8) NULL,
discount_total_amount decimal(26,8) NULL,
recorded_sales tinyint(1) NULL,
UNIQUE KEY order_id (order_id),
KEY order_key (order_key)
@@ -2659,8 +2825,8 @@ CREATE TABLE $meta_table (
order_id bigint(20) unsigned null,
meta_key varchar(255),
meta_value text null,
KEY meta_key_value (meta_key, meta_value(100)),
KEY order_id_meta_key_meta_value (order_id, meta_key, meta_value(100))
KEY meta_key_value (meta_key(100), meta_value($composite_meta_value_index_length)),
KEY order_id_meta_key_meta_value (order_id, meta_key(100), meta_value($composite_meta_value_index_length))
) $collate;
";
@@ -2681,16 +2847,28 @@ CREATE TABLE $meta_table (
/**
* Deletes meta based on meta ID.
*
* @param WC_Data $object WC_Data object.
* @param stdClass $meta (containing at least ->id).
* @param WC_Data $object WC_Data object.
* @param \stdClass $meta (containing at least ->id).
*
* @return bool
*/
public function delete_meta( &$object, $meta ) {
$delete_meta = $this->data_store_meta->delete_meta( $object, $meta );
if ( $this->should_backfill_post_record() && isset( $meta->id ) ) {
// Let's get the actual meta key before its deleted for backfilling. We cannot delete just by ID because meta IDs are different in HPOS and posts tables.
$db_meta = $this->data_store_meta->get_metadata_by_id( $meta->id );
if ( $db_meta ) {
$meta->key = $db_meta->meta_key;
$meta->value = $db_meta->meta_value;
}
}
if ( $object instanceof WC_Abstract_Order ) {
$this->maybe_backfill_post_record( $object );
$delete_meta = $this->data_store_meta->delete_meta( $object, $meta );
$changes_applied = $this->after_meta_change( $object, $meta );
if ( ! $changes_applied && $object instanceof WC_Abstract_Order && $this->should_backfill_post_record() && isset( $meta->key ) ) {
self::$backfilling_order_ids[] = $object->get_id();
delete_post_meta( $object->get_id(), $meta->key, $meta->value );
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $object->get_id() ) );
}
return $delete_meta;
@@ -2699,16 +2877,20 @@ CREATE TABLE $meta_table (
/**
* Add new piece of meta.
*
* @param WC_Data $object WC_Data object.
* @param stdClass $meta (containing ->key and ->value).
* @param WC_Data $object WC_Data object.
* @param \stdClass $meta (containing ->key and ->value).
*
* @return int|bool meta ID or false on failure
*/
public function add_meta( &$object, $meta ) {
$add_meta = $this->data_store_meta->add_meta( $object, $meta );
$add_meta = $this->data_store_meta->add_meta( $object, $meta );
$meta->id = $add_meta;
$changes_applied = $this->after_meta_change( $object, $meta );
if ( $object instanceof WC_Abstract_Order ) {
$this->maybe_backfill_post_record( $object );
if ( ! $changes_applied && $object instanceof WC_Abstract_Order && $this->should_backfill_post_record() ) {
self::$backfilling_order_ids[] = $object->get_id();
add_post_meta( $object->get_id(), $meta->key, $meta->value );
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $object->get_id() ) );
}
return $add_meta;
@@ -2717,18 +2899,66 @@ CREATE TABLE $meta_table (
/**
* Update meta.
*
* @param WC_Data $object WC_Data object.
* @param stdClass $meta (containing ->id, ->key and ->value).
* @param WC_Data $object WC_Data object.
* @param \stdClass $meta (containing ->id, ->key and ->value).
*
* @return bool
* @return bool The number of rows updated, or false on error.
*/
public function update_meta( &$object, $meta ) {
$update_meta = $this->data_store_meta->update_meta( $object, $meta );
$update_meta = $this->data_store_meta->update_meta( $object, $meta );
$changes_applied = $this->after_meta_change( $object, $meta );
if ( $object instanceof WC_Abstract_Order ) {
$this->maybe_backfill_post_record( $object );
if ( ! $changes_applied && $object instanceof WC_Abstract_Order && $this->should_backfill_post_record() ) {
self::$backfilling_order_ids[] = $object->get_id();
update_post_meta( $object->get_id(), $meta->key, $meta->value );
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $object->get_id() ) );
}
return $update_meta;
}
/**
* Perform after meta change operations, including updating the date_modified field, clearing caches and applying changes.
*
* @param WC_Abstract_Order $order Order object.
* @param \WC_Meta_Data $meta Metadata object.
*
* @return bool True if changes were applied, false otherwise.
*/
protected function after_meta_change( &$order, $meta ) {
method_exists( $meta, 'apply_changes' ) && $meta->apply_changes();
// Prevent this happening multiple time in same request.
if ( $this->should_save_after_meta_change( $order, $meta ) ) {
$order->set_date_modified( current_time( 'mysql' ) );
$order->save();
return true;
} else {
$order_cache = wc_get_container()->get( OrderCache::class );
$order_cache->remove( $order->get_id() );
}
return false;
}
/**
* Helper function to check whether the modified date needs to be updated after a meta save.
*
* This method prevents order->save() call multiple times in the same request after any meta update by checking if:
* 1. Order modified date is already the current date, no updates needed in this case.
* 2. If there are changes already queued for order object, then we don't need to update the modified date as it will be updated ina subsequent save() call.
*
* @param WC_Order $order Order object.
* @param \WC_Meta_Data|null $meta Metadata object.
*
* @return bool Whether the modified date needs to be updated.
*/
private function should_save_after_meta_change( $order, $meta = null ) {
$current_time = $this->legacy_proxy->call_function( 'current_time', 'mysql', 1 );
$current_date_time = new \WC_DateTime( $current_time, new \DateTimeZone( 'GMT' ) );
$skip_for = array(
EditLock::META_KEY_NAME,
);
return $order->get_date_modified() < $current_date_time && empty( $order->get_changes() ) && ( ! is_object( $meta ) || ! in_array( $meta->key, $skip_for, true ) );
}
}

View File

@@ -199,7 +199,57 @@ class OrdersTableQuery {
unset( $this->args['customer_note'], $this->args['name'] );
$this->build_query();
$this->run_query();
if ( ! $this->maybe_override_query() ) {
$this->run_query();
}
}
/**
* Lets the `woocommerce_hpos_pre_query` filter override the query.
*
* @return boolean Whether the query was overridden or not.
*/
private function maybe_override_query(): bool {
/**
* Filters the orders array before the query takes place.
*
* Return a non-null value to bypass the HPOS default order queries.
*
* If the query includes limits via the `limit`, `page`, or `offset` arguments, we
* encourage the `found_orders` and `max_num_pages` properties to also be set.
*
* @since 8.2.0
*
* @param array|null $order_data {
* An array of order data.
* @type int[] $orders Return an array of order IDs data to short-circuit the HPOS query,
* or null to allow HPOS to run its normal query.
* @type int $found_orders The number of orders found.
* @type int $max_num_pages The number of pages.
* }
* @param OrdersTableQuery $query The OrdersTableQuery instance.
* @param string $sql The OrdersTableQuery instance.
*/
$pre_query = apply_filters( 'woocommerce_hpos_pre_query', null, $this, $this->sql );
if ( ! $pre_query || ! isset( $pre_query[0] ) || ! is_array( $pre_query[0] ) ) {
return false;
}
// If the filter set the orders, make sure the others values are set as well and skip running the query.
list( $this->orders, $this->found_orders, $this->max_num_pages ) = $pre_query;
if ( ! is_int( $this->found_orders ) || $this->found_orders < 1 ) {
$this->found_orders = count( $this->orders );
}
if ( ! is_int( $this->max_num_pages ) || $this->max_num_pages < 1 ) {
if ( ! $this->arg_isset( 'limit' ) || ! is_int( $this->args['limit'] ) || $this->args['limit'] < 1 ) {
$this->args['limit'] = 10;
}
$this->max_num_pages = (int) ceil( $this->found_orders / $this->args['limit'] );
}
return true;
}
/**
@@ -287,22 +337,22 @@ class OrdersTableQuery {
* YYYY-MM-DD queries have 'day' precision for backwards compatibility.
*
* @param mixed $date The date. Can be a {@see \WC_DateTime}, a timestamp or a string.
* @param string $timezone The timezone to use for the date.
* @return array An array with keys 'year', 'month', 'day' and possibly 'hour', 'minute' and 'second'.
*/
private function date_to_date_query_arg( $date, $timezone ): array {
private function date_to_date_query_arg( $date ): array {
$result = array(
'year' => '',
'month' => '',
'day' => '',
);
$precision = 'second';
if ( is_numeric( $date ) ) {
$date = new \WC_DateTime( "@{$date}", new \DateTimeZone( $timezone ) );
$date = new \WC_DateTime( "@{$date}", new \DateTimeZone( 'UTC' ) );
$precision = 'second';
} elseif ( ! is_a( $date, 'WC_DateTime' ) ) {
// YYYY-MM-DD queries have 'day' precision for backwards compat.
$date = wc_string_to_datetime( $date );
// For backwards compat (see https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query#date)
// only YYYY-MM-DD is considered for date values. Timestamps do support second precision.
$date = wc_string_to_datetime( date( 'Y-m-d', strtotime( $date ) ) );
$precision = 'day';
}
@@ -319,6 +369,80 @@ class OrdersTableQuery {
return $result;
}
/**
* Returns UTC-based date query arguments for a combination of local time dates and a date shorthand operator.
*
* @param array $dates_raw Array of dates (in local time) to use in combination with the operator.
* @param string $operator One of the operators supported by date queries (<, <=, =, ..., >, >=).
* @return array Partial date query arg with relevant dates now UTC-based.
*
* @since 8.2.0
*/
private function local_time_to_gmt_date_query( $dates_raw, $operator ) {
$result = array();
// Convert YYYY-MM-DD to UTC timestamp. Per https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query#date only date is relevant (time is ignored).
foreach ( $dates_raw as &$raw_date ) {
$raw_date = is_numeric( $raw_date ) ? $raw_date : strtotime( get_gmt_from_date( date( 'Y-m-d', strtotime( $raw_date ) ) ) );
}
$date1 = end( $dates_raw );
switch ( $operator ) {
case '>':
$result = array(
'after' => $this->date_to_date_query_arg( $date1 + DAY_IN_SECONDS ),
'inclusive' => true,
);
break;
case '>=':
$result = array(
'after' => $this->date_to_date_query_arg( $date1 ),
'inclusive' => true,
);
break;
case '=':
$result = array(
'relation' => 'AND',
array(
'after' => $this->date_to_date_query_arg( $date1 ),
'inclusive' => true,
),
array(
'before' => $this->date_to_date_query_arg( $date1 + DAY_IN_SECONDS ),
'inclusive' => false,
)
);
break;
case '<=':
$result = array(
'before' => $this->date_to_date_query_arg( $date1 + DAY_IN_SECONDS ),
'inclusive' => false,
);
break;
case '<':
$result = array(
'before' => $this->date_to_date_query_arg( $date1 ),
'inclusive' => false,
);
break;
case '...':
$result = array(
'relation' => 'AND',
$this->local_time_to_gmt_date_query( array( $dates_raw[1] ), '<=' ),
$this->local_time_to_gmt_date_query( array( $dates_raw[0] ), '>=' ),
);
break;
}
if ( ! $result ) {
throw new \Exception( 'Please specify a valid date shorthand operator.' );
}
return $result;
}
/**
* Processes date-related query args and merges the result into 'date_query'.
*
@@ -347,27 +471,45 @@ class OrdersTableQuery {
$date_keys = array_filter( $valid_date_keys, array( $this, 'arg_isset' ) );
foreach ( $date_keys as $date_key ) {
$is_local = in_array( $date_key, $local_date_keys, true );
$date_value = $this->args[ $date_key ];
$operator = '=';
$dates_raw = array();
$dates = array();
$timezone = in_array( $date_key, $gmt_date_keys, true ) ? '+0000' : wc_timezone_string();
if ( is_string( $date_value ) && preg_match( self::REGEX_SHORTHAND_DATES, $date_value, $matches ) ) {
$operator = in_array( $matches[2], $valid_operators, true ) ? $matches[2] : '';
if ( ! empty( $matches[1] ) ) {
$dates[] = $this->date_to_date_query_arg( $matches[1], $timezone );
$dates_raw[] = $matches[1];
}
$dates[] = $this->date_to_date_query_arg( $matches[3], $timezone );
$dates_raw[] = $matches[3];
} else {
$dates[] = $this->date_to_date_query_arg( $date_value, $timezone );
$dates_raw[] = $date_value;
}
if ( empty( $dates ) || ! $operator || ( '...' === $operator && count( $dates ) < 2 ) ) {
if ( empty( $dates_raw ) || ! $operator || ( '...' === $operator && count( $dates_raw ) < 2 ) ) {
throw new \Exception( 'Invalid date_query' );
}
if ( $is_local ) {
$date_key = $local_to_gmt_date_keys[ $date_key ];
if ( ! is_numeric( $dates_raw[0] ) && ( ! isset( $dates_raw[1] ) || ! is_numeric( $dates_raw[1] ) ) ) {
// Only non-numeric args can be considered local time. Timestamps are assumed to be UTC per https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query#date.
$date_queries[] = array_merge(
array(
'column' => $date_key,
),
$this->local_time_to_gmt_date_query( $dates_raw, $operator )
);
continue;
}
}
$operator_to_keys = array();
if ( in_array( $operator, array( '>', '>=', '...' ), true ) ) {
@@ -378,7 +520,7 @@ class OrdersTableQuery {
$operator_to_keys[] = 'before';
}
$date_key = in_array( $date_key, $local_date_keys, true ) ? $local_to_gmt_date_keys[ $date_key ] : $date_key;
$dates = array_map( array( $this, 'date_to_date_query_arg' ), $dates_raw );
$date_queries[] = array_merge(
array(
'column' => $date_key,
@@ -470,7 +612,7 @@ class OrdersTableQuery {
$op = isset( $query['after'] ) ? 'after' : 'before';
$date_value_local = $query[ $op ];
$date_value_gmt = wc_string_to_timestamp( get_gmt_from_date( wc_string_to_datetime( $date_value_local ) ) );
$query[ $op ] = $this->date_to_date_query_arg( $date_value_gmt, 'UTC' );
$query[ $op ] = $this->date_to_date_query_arg( $date_value_gmt );
}
return $query;

View File

@@ -6,6 +6,9 @@
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
use \WC_Cache_Helper;
use \WC_Meta_Data;
/**
* Class OrdersTableRefundDataStore.
*/
@@ -75,6 +78,9 @@ class OrdersTableRefundDataStore extends OrdersTableDataStore {
return;
}
$refund_cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $refund->get_parent_id();
wp_cache_delete( $refund_cache_key, 'orders' );
$this->delete_order_data_from_custom_order_tables( $refund_id );
$refund->set_id( 0 );
@@ -159,8 +165,17 @@ class OrdersTableRefundDataStore extends OrdersTableDataStore {
$props_to_update = $this->get_props_to_update( $refund, $meta_key_to_props );
foreach ( $props_to_update as $meta_key => $prop ) {
$value = $refund->{"get_$prop"}( 'edit' );
$refund->update_meta_data( $meta_key, $value );
$meta_object = new WC_Meta_Data();
$meta_object->key = $meta_key;
$meta_object->value = $refund->{"get_$prop"}( 'edit' );
$existing_meta = $this->data_store_meta->get_metadata_by_key( $refund, $meta_key );
if ( $existing_meta ) {
$existing_meta = $existing_meta[0];
$meta_object->id = $existing_meta->id;
$this->update_meta( $refund, $meta_object );
} else {
$this->add_meta( $refund, $meta_object );
}
$updated_props[] = $prop;
}