Merged in feature/280-dev-dev01 (pull request #21)

auto-patch  280-dev-dev01-2024-01-19T16_41_58

* auto-patch  280-dev-dev01-2024-01-19T16_41_58
This commit is contained in:
Tony Volpe
2024-01-19 16:44:43 +00:00
parent 2699b5437a
commit be83910651
2125 changed files with 179300 additions and 35639 deletions

View File

@@ -80,6 +80,13 @@ class DataSynchronizer implements BatchProcessorInterface {
*/
private $error_logger;
/**
* The instance of the LegacyProxy object to use.
*
* @var LegacyProxy
*/
private $legacy_proxy;
/**
* The order cache controller.
*
@@ -103,6 +110,7 @@ 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_action( 'wp_scheduled_delete', array( $this, 'delete_trashed_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 );
@@ -136,6 +144,7 @@ class DataSynchronizer implements BatchProcessorInterface {
$this->data_store = $data_store;
$this->database_util = $database_util;
$this->posts_to_cot_migrator = $posts_to_cot_migrator;
$this->legacy_proxy = $legacy_proxy;
$this->error_logger = $legacy_proxy->call_function( 'wc_get_logger' );
$this->order_cache_controller = $order_cache_controller;
$this->batch_processing_controller = $batch_processing_controller;
@@ -966,6 +975,41 @@ ORDER BY orders.id ASC
do_action( 'woocommerce_scheduled_auto_draft_delete' );
}
/**
* Handles deletion of trashed orders after `EMPTY_TRASH_DAYS` as defined by WordPress.
*
* @since 8.5.0
*
* @return void
*/
private function delete_trashed_orders() {
if ( ! $this->custom_orders_table_is_authoritative() ) {
return;
}
$delete_timestamp = $this->legacy_proxy->call_function( 'time' ) - ( DAY_IN_SECONDS * EMPTY_TRASH_DAYS );
$args = array(
'status' => 'trash',
'limit' => self::ORDERS_SYNC_BATCH_SIZE,
'date_modified' => '<' . $delete_timestamp,
);
$orders = wc_get_orders( $args );
if ( ! $orders || ! is_array( $orders ) ) {
return;
}
foreach ( $orders as $order ) {
if ( $order->get_status() !== 'trash' ) {
continue;
}
if ( $order->get_date_modified()->getTimestamp() >= $delete_timestamp ) {
continue;
}
$order->delete( true );
}
}
/**
* Handle the 'woocommerce_feature_description_tip' filter.
*

View File

@@ -0,0 +1,200 @@
<?php
/**
* LegacyDataHandler class file.
*/
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
defined( 'ABSPATH' ) || exit;
/**
* This class provides functionality to clean up post data from the posts table when HPOS is authoritative.
*/
class LegacyDataHandler {
/**
* Instance of the HPOS datastore.
*
* @var OrdersTableDataStore
*/
private OrdersTableDataStore $data_store;
/**
* Instance of the DataSynchronizer class.
*
* @var DataSynchronizer
*/
private DataSynchronizer $data_synchronizer;
/**
* Class initialization, invoked by the DI container.
*
* @param OrdersTableDataStore $data_store HPOS datastore instance to use.
* @param DataSynchronizer $data_synchronizer DataSynchronizer instance to use.
*
* @internal
*/
final public function init( OrdersTableDataStore $data_store, DataSynchronizer $data_synchronizer ) {
$this->data_store = $data_store;
$this->data_synchronizer = $data_synchronizer;
}
/**
* Returns the total number of orders for which legacy post data can be removed.
*
* @param array $order_ids If provided, total is computed only among IDs in this array, which can be either individual IDs or ranges like "100-200".
* @return int Number of orders.
*/
public function count_orders_for_cleanup( $order_ids = array() ) : int {
global $wpdb;
return (int) $wpdb->get_var( $this->build_sql_query_for_cleanup( $order_ids, 'count' ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- prepared in build_sql_query_for_cleanup().
}
/**
* Returns a set of orders for which legacy post data can be removed.
*
* @param array $order_ids If provided, result is a subset of the order IDs in this array, which can contain either individual order IDs or ranges like "100-200".
* @param int $limit Limit the number of results.
* @return array[int] Order IDs.
*/
public function get_orders_for_cleanup( $order_ids = array(), int $limit = 0 ): array {
global $wpdb;
return array_map(
'absint',
$wpdb->get_col( $this->build_sql_query_for_cleanup( $order_ids, 'ids', $limit ) ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- prepared in build_sql_query_for_cleanup().
);
}
/**
* Builds a SQL statement to either count or obtain IDs for orders in need of cleanup.
*
* @param array $order_ids If provided, the query will only include orders in this set of order IDs or ID ranges (like "10-100").
* @param string $result Use 'count' to build a query that returns a count. Otherwise, the query will return order IDs.
* @param integer $limit If provided, the query will be limited to this number of results. Does not apply when $result is 'count'.
* @return string SQL query.
*/
private function build_sql_query_for_cleanup( array $order_ids = array(), string $result = 'ids', int $limit = 0 ): string {
global $wpdb;
$sql_where = '';
if ( $order_ids ) {
// Expand ranges in $order_ids as needed to build the WHERE clause.
$where_ids = array();
$where_ranges = array();
foreach ( $order_ids as &$arg ) {
if ( is_numeric( $arg ) ) {
$where_ids[] = absint( $arg );
} elseif ( preg_match( '/^(\d+)-(\d+)$/', $arg, $matches ) ) {
$where_ranges[] = $wpdb->prepare( "({$wpdb->posts}.ID >= %d AND {$wpdb->posts}.ID <= %d)", absint( $matches[1] ), absint( $matches[2] ) );
}
}
if ( $where_ids ) {
$where_ranges[] = "{$wpdb->posts}.ID IN (" . implode( ',', $where_ids ) . ')';
}
if ( ! $where_ranges ) {
$sql_where .= '1=0';
} else {
$sql_where .= '(' . implode( ' OR ', $where_ranges ) . ')';
}
}
$sql_where .= $sql_where ? ' AND ' : '';
// Post type handling.
$sql_where .= '(';
$sql_where .= "{$wpdb->posts}.post_type IN ('" . implode( "', '", esc_sql( wc_get_order_types( 'cot-migration' ) ) ) . "')";
$sql_where .= $wpdb->prepare(
" OR (post_type = %s AND EXISTS(SELECT 1 FROM {$wpdb->postmeta} WHERE post_id = {$wpdb->posts}.ID))",
$this->data_synchronizer::PLACEHOLDER_ORDER_POST_TYPE
);
$sql_where .= ')';
// Exclude 'auto-draft' since those go away on their own.
$sql_where .= $wpdb->prepare( " AND {$wpdb->posts}.post_status != %s", 'auto-draft' );
if ( 'count' === $result ) {
$sql_fields = 'COUNT(*)';
$sql_limit = '';
} else {
$sql_fields = 'ID';
$sql_limit = $limit > 0 ? $wpdb->prepare( 'LIMIT %d', $limit ) : '';
}
return "SELECT {$sql_fields} FROM {$wpdb->posts} WHERE {$sql_where} {$sql_limit}";
}
/**
* Performs a cleanup of post data for a given order and also converts the post to the placeholder type in the backup table.
*
* @param int $order_id Order ID.
* @param bool $skip_checks Whether to skip the checks that happen before the order is cleaned up.
* @return void
* @throws \Exception When an error occurs.
*/
public function cleanup_post_data( int $order_id, bool $skip_checks = false ): void {
global $wpdb;
$order = wc_get_order( $order_id );
if ( ! $order ) {
// translators: %d is an order ID.
throw new \Exception( sprintf( __( '%d is not a valid order ID.', 'woocommerce' ), $order_id ) );
}
if ( ! $skip_checks && ! $this->is_order_newer_than_post( $order ) ) {
throw new \Exception( sprintf( __( 'Data in posts table appears to be more recent than in HPOS tables.', 'woocommerce' ) ) );
}
$meta_ids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM {$wpdb->postmeta} WHERE post_id = %d", $order->get_id() ) );
foreach ( $meta_ids as $meta_id ) {
delete_metadata_by_mid( 'post', $meta_id );
}
// wp_update_post() changes the post modified date, so we do this manually.
// Also, we suspect using wp_update_post() could lead to integrations mistakenly updating the entity.
$wpdb->query(
$wpdb->prepare(
"UPDATE {$wpdb->posts} SET post_type = %s, post_status = %s WHERE ID = %d",
$this->data_synchronizer::PLACEHOLDER_ORDER_POST_TYPE,
'draft',
$order->get_id()
)
);
clean_post_cache( $order->get_id() );
}
/**
* Checks whether an HPOS-backed order is newer than the corresponding post.
*
* @param int|\WC_Order $order An HPOS order.
* @return bool TRUE if the order is up to date with the corresponding post.
* @throws \Exception When the order is not an HPOS order.
*/
private function is_order_newer_than_post( $order ): bool {
$order = is_a( $order, 'WC_Order' ) ? $order : wc_get_order( absint( $order ) );
if ( ! is_a( $order->get_data_store()->get_current_class_name(), OrdersTableDataStore::class, true ) ) {
throw new \Exception( __( 'Order is not an HPOS order.', 'woocommerce' ) );
}
$post = get_post( $order->get_id() );
if ( ! $post || $this->data_synchronizer::PLACEHOLDER_ORDER_POST_TYPE === $post->post_type ) {
return true;
}
$order_modified_gmt = $order->get_date_modified() ?? $order->get_date_created();
$order_modified_gmt = $order_modified_gmt ? $order_modified_gmt->getTimestamp() : 0;
$post_modified_gmt = $post->post_modified_gmt ?? $post->post_date_gmt;
$post_modified_gmt = ( $post_modified_gmt && '0000-00-00 00:00:00' !== $post_modified_gmt ) ? wc_string_to_timestamp( $post_modified_gmt ) : 0;
return $order_modified_gmt >= $post_modified_gmt;
}
}

View File

@@ -26,7 +26,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
/**
* 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.
* @var array
*/
private static $reading_order_ids = array();
@@ -609,6 +609,15 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
}
}
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $order->get_id() ) );
/**
* Fired when the backing post record for an HPOS order is backfilled after an order update.
*
* @since 8.5.0
*
* @param \WC_Order $order The order object.
*/
do_action( 'woocommerce_hpos_post_record_backfilled', $order );
}
/**
@@ -969,10 +978,24 @@ WHERE
/**
* Get unpaid orders last updated before the specified date.
*
* @param int $date Timestamp.
* @return array
* @param int $date This timestamp is expected in the timezone in WordPress settings for legacy reason, even though it's not a good practice.
*
* @return array Array of order IDs.
*/
public function get_unpaid_orders( $date ) {
$timezone_offset = wc_timezone_offset();
$gmt_timestamp = $date - $timezone_offset;
return $this->get_unpaid_orders_gmt( absint( $gmt_timestamp ) );
}
/**
* Get unpaid orders last updated before the specified GMT date.
*
* @param int $gmt_timestamp GMT timestamp.
*
* @return array Array of order IDs.
*/
public function get_unpaid_orders_gmt( $gmt_timestamp ) {
global $wpdb;
$orders_table = self::get_orders_table_name();
@@ -986,7 +1009,7 @@ WHERE
AND {$orders_table}.status = %s
AND {$orders_table}.date_updated_gmt < %s",
'wc-pending',
gmdate( 'Y-m-d H:i:s', absint( $date ) )
gmdate( 'Y-m-d H:i:s', absint( $gmt_timestamp ) )
)
);
// phpcs:enable
@@ -1414,17 +1437,6 @@ WHERE
return $diff;
}
/**
* Log difference between post and COT data for an order.
*
* @param array $diff Difference between post and COT data.
*
* @return void
*/
private function log_diff( array $diff ): void {
$this->error_logger->notice( 'Diff found: ' . wp_json_encode( $diff, JSON_PRETTY_PRINT ) );
}
/**
* Migrate post record from a given order object.
*
@@ -1434,12 +1446,22 @@ WHERE
* @return void
*/
private function migrate_post_record( \WC_Abstract_Order &$order, \WC_Abstract_Order $post_order ): void {
$this->migrate_meta_data_from_post_order( $order, $post_order );
$diff = $this->migrate_meta_data_from_post_order( $order, $post_order );
$post_order_base_data = $post_order->get_base_data();
foreach ( $post_order_base_data as $key => $value ) {
$this->set_order_prop( $order, $key, $value );
}
$this->persist_updates( $order, false );
/**
* Fired when an HPOS order is updated from its corresponding post record on read due to a difference in the data.
*
* @since 8.5.0
*
* @param \WC_Order $order The order object.
* @param array $diff Difference between HPOS data and post data.
*/
do_action( 'woocommerce_hpos_post_record_migrated_on_read', $order, $diff );
}
/**