8889841cPK ;[/<~E ~E Orders/DataSynchronizer.phpnu [ 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' );
}
/**
* Does the custom orders tables exist in the database?
*
* @return bool True if the custom orders tables exist in the database.
*/
public function check_orders_table_exists(): bool {
$missing_tables = $this->database_util->get_missing_tables( $this->data_store->get_database_schema() );
return count( $missing_tables ) === 0;
}
/**
* Create the custom orders database tables.
*/
public function create_database_tables() {
$this->database_util->dbdelta( $this->data_store->get_database_schema() );
}
/**
* Delete the custom orders database tables.
*/
public function delete_database_tables() {
$table_names = $this->data_store->get_all_table_names();
foreach ( $table_names as $table_name ) {
$this->database_util->drop_database_table( $table_name );
}
}
/**
* 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 );
}
/**
* 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_total_pending_count(),
);
}
/**
* Get the total number of orders pending synchronization.
*
* @return int
*/
public function get_current_orders_pending_sync_count_cached() : int {
return $this->get_current_orders_pending_sync_count( true );
}
/**
* Calculate how many orders need to be synchronized currently.
* A database query is performed to get how many orders match one of the following:
*
* - Existing in the authoritative table but not in the backup table.
* - Existing in both tables, but they have a different update date.
*
* @param bool $use_cache Whether to use the cached value instead of fetching from database.
*/
public function get_current_orders_pending_sync_count( $use_cache = false ): int {
global $wpdb;
if ( $use_cache ) {
$pending_count = wp_cache_get( 'woocommerce_hpos_pending_sync_count' );
if ( false !== $pending_count ) {
return (int) $pending_count;
}
}
$orders_table = $this->data_store::get_orders_table_name();
$order_post_types = wc_get_order_types( 'cot-migration' );
if ( empty( $order_post_types ) ) {
$this->error_logger->debug(
sprintf(
/* translators: 1: method name. */
esc_html__( '%1$s was called but no order types were registered: it may have been called too early.', 'woocommerce' ),
__METHOD__
)
);
return 0;
}
$order_post_type_placeholder = implode( ', ', array_fill( 0, count( $order_post_types ), '%s' ) );
if ( $this->custom_orders_table_is_authoritative() ) {
$missing_orders_count_sql = "
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' )
";
$operator = '>';
} else {
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_post_type_placeholder is prepared.
$missing_orders_count_sql = $wpdb->prepare(
"
SELECT COUNT(1) FROM $wpdb->posts posts
LEFT JOIN $orders_table orders ON posts.id=orders.id
WHERE
posts.post_type in ($order_post_type_placeholder)
AND posts.post_status != 'auto-draft'
AND orders.id IS NULL",
$order_post_types
);
// phpcs:enable
$operator = '<';
}
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $missing_orders_count_sql is prepared.
$sql = $wpdb->prepare(
"
SELECT(
($missing_orders_count_sql)
+
(SELECT COUNT(1) FROM (
SELECT orders.id FROM $orders_table orders
JOIN $wpdb->posts posts on posts.ID = orders.id
WHERE
posts.post_type IN ($order_post_type_placeholder)
AND orders.date_updated_gmt $operator posts.post_modified_gmt
) x)
) count",
$order_post_types
);
// phpcs:enable
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$pending_count = (int) $wpdb->get_var( $sql );
wp_cache_set( 'woocommerce_hpos_pending_sync_count', $pending_count );
return $pending_count;
}
/**
* Is the custom orders table the authoritative data source for orders currently?
*
* @return bool Whether the custom orders table the authoritative data source for orders currently.
*/
public function custom_orders_table_is_authoritative(): bool {
return wc_string_to_bool( get_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ) );
}
/**
* Get a list of ids of orders than are out of sync.
*
* Valid values for $type are:
*
* ID_TYPE_MISSING_IN_ORDERS_TABLE: orders that exist in posts table but not in orders table.
* ID_TYPE_MISSING_IN_POSTS_TABLE: orders that exist in orders table but not in posts table (the corresponding post entries are placeholders).
* ID_TYPE_DIFFERENT_UPDATE_DATE: orders that exist in both tables but have different last update dates.
*
* @param int $type One of ID_TYPE_MISSING_IN_ORDERS_TABLE, ID_TYPE_MISSING_IN_POSTS_TABLE, ID_TYPE_DIFFERENT_UPDATE_DATE.
* @param int $limit Maximum number of ids to return.
* @return array An array of order ids.
* @throws \Exception Invalid parameter.
*/
public function get_ids_of_orders_pending_sync( int $type, int $limit ) {
global $wpdb;
if ( $limit < 1 ) {
throw new \Exception( '$limit must be at least 1' );
}
$orders_table = $this->data_store::get_orders_table_name();
$order_post_types = wc_get_order_types( 'cot-migration' );
$order_post_type_placeholders = implode( ', ', array_fill( 0, count( $order_post_types ), '%s' ) );
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
switch ( $type ) {
case self::ID_TYPE_MISSING_IN_ORDERS_TABLE:
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_post_type_placeholders is prepared.
$sql = $wpdb->prepare(
"
SELECT posts.ID FROM $wpdb->posts posts
LEFT JOIN $orders_table orders ON posts.ID = orders.id
WHERE
posts.post_type IN ($order_post_type_placeholders)
AND posts.post_status != 'auto-draft'
AND orders.id IS NULL",
$order_post_types
);
// phpcs:enable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
break;
case self::ID_TYPE_MISSING_IN_POSTS_TABLE:
$sql = "
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' )
";
break;
case self::ID_TYPE_DIFFERENT_UPDATE_DATE:
$operator = $this->custom_orders_table_is_authoritative() ? '>' : '<';
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_post_type_placeholders is prepared.
$sql = $wpdb->prepare(
"
SELECT orders.id FROM $orders_table orders
JOIN $wpdb->posts posts on posts.ID = orders.id
WHERE
posts.post_type IN ($order_post_type_placeholders)
AND orders.date_updated_gmt $operator posts.post_modified_gmt
",
$order_post_types
);
// phpcs:enable
break;
default:
throw new \Exception( 'Invalid $type, must be one of the ID_TYPE_... constants.' );
}
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// phpcs:ignore WordPress.DB
return array_map( 'intval', $wpdb->get_col( $sql . " LIMIT $limit" ) );
}
/**
* Cleanup all the synchronization status information,
* because the process has been disabled by the user via settings,
* or because there's nothing left to synchronize.
*/
public function cleanup_synchronization_state() {
delete_option( self::INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION );
}
/**
* Process data for current batch.
*
* @param array $batch Batch details.
*/
public function process_batch( array $batch ) : void {
if ( $this->custom_orders_table_is_authoritative() ) {
foreach ( $batch as $id ) {
$order = wc_get_order( $id );
if ( ! $order ) {
$this->error_logger->error( "Order $id not found during batch process, skipping." );
continue;
}
$data_store = $order->get_data_store();
$data_store->backfill_post_record( $order );
}
} else {
$this->posts_to_cot_migrator->migrate_orders( $batch );
}
if ( 0 === $this->get_total_pending_count() ) {
$this->cleanup_synchronization_state();
}
}
/**
* Get total number of pending records that require update.
*
* @return int Number of pending records.
*/
public function get_total_pending_count(): int {
return $this->get_current_orders_pending_sync_count();
}
/**
* Returns the batch with records that needs to be processed for a given size.
*
* @param int $size Size of the batch.
*
* @return array Batch of records.
*/
public function get_next_batch_to_process( int $size ): array {
if ( $this->custom_orders_table_is_authoritative() ) {
$order_ids = $this->get_ids_of_orders_pending_sync( self::ID_TYPE_MISSING_IN_POSTS_TABLE, $size );
} else {
$order_ids = $this->get_ids_of_orders_pending_sync( self::ID_TYPE_MISSING_IN_ORDERS_TABLE, $size );
}
if ( count( $order_ids ) >= $size ) {
return $order_ids;
}
$order_ids = $order_ids + $this->get_ids_of_orders_pending_sync( self::ID_TYPE_DIFFERENT_UPDATE_DATE, $size - count( $order_ids ) );
return $order_ids;
}
/**
* Default batch size to use.
*
* @return int Default batch size.
*/
public function get_default_batch_size(): int {
$batch_size = self::ORDERS_SYNC_BATCH_SIZE;
if ( $this->custom_orders_table_is_authoritative() ) {
// Back-filling is slower than migration.
$batch_size = absint( self::ORDERS_SYNC_BATCH_SIZE / 10 ) + 1;
}
/**
* Filter to customize the count of orders that will be synchronized in each step of the custom orders table to/from posts table synchronization process.
*
* @since 6.6.0
*
* @param int Default value for the count.
*/
return apply_filters( 'woocommerce_orders_cot_and_posts_sync_step_size', $batch_size );
}
/**
* A user friendly name for this process.
*
* @return string Name of the process.
*/
public function get_name(): string {
return 'Order synchronizer';
}
/**
* A user friendly description for this process.
*
* @return string Description.
*/
public function get_description(): string {
return 'Synchronizes orders between posts and custom order tables.';
}
/**
* Handle the 'deleted_post' action.
*
* When posts is authoritative and sync is enabled, deleting a post also deletes COT data.
*
* @param int $postid The post id.
* @param WP_Post $post The deleted post.
*/
private function handle_deleted_post( $postid, $post ): void {
if ( 'shop_order' === $post->post_type && $this->data_sync_is_enabled() ) {
$this->data_store->delete_order_data_from_custom_order_tables( $postid );
}
}
/**
* Handle the 'woocommerce_update_order' action.
*
* When posts is authoritative and sync is enabled, updating a post triggers a corresponding change in the COT table.
*
* @param int $order_id The order id.
*/
private function handle_updated_order( $order_id ): void {
if ( ! $this->custom_orders_table_is_authoritative() && $this->data_sync_is_enabled() ) {
$this->posts_to_cot_migrator->migrate_orders( array( $order_id ) );
}
}
/**
* Handle the 'woocommerce_feature_description_tip' filter.
*
* When the COT feature is enabled and there are orders pending sync (in either direction),
* show a "you should ync before disabling" warning under the feature in the features page.
* Skip this if the UI prevents changing the feature enable status.
*
* @param string $desc_tip The original description tip for the feature.
* @param string $feature_id The feature id.
* @param bool $ui_disabled True if the UI doesn't allow to enable or disable the feature.
* @return string The new description tip for the feature.
*/
private function handle_feature_description_tip( $desc_tip, $feature_id, $ui_disabled ): string {
if ( 'custom_order_tables' !== $feature_id || $ui_disabled ) {
return $desc_tip;
}
$features_controller = wc_get_container()->get( FeaturesController::class );
$feature_is_enabled = $features_controller->feature_is_enabled( 'custom_order_tables' );
if ( ! $feature_is_enabled ) {
return $desc_tip;
}
$pending_sync_count = $this->get_current_orders_pending_sync_count();
if ( ! $pending_sync_count ) {
return $desc_tip;
}
if ( $this->custom_orders_table_is_authoritative() ) {
$extra_tip = sprintf(
_n(
"⚠ There's one order pending sync from the orders table to the posts table. The feature shouldn't be disabled until this order is synchronized.",
"⚠ There are %1\$d orders pending sync from the orders table to the posts table. The feature shouldn't be disabled until these orders are synchronized.",
$pending_sync_count,
'woocommerce'
),
$pending_sync_count
);
} else {
$extra_tip = sprintf(
_n(
"⚠ There's one order pending sync from the posts table to the orders table. The feature shouldn't be disabled until this order is synchronized.",
"⚠ There are %1\$d orders pending sync from the posts table to the orders table. The feature shouldn't be disabled until these orders are synchronized.",
$pending_sync_count,
'woocommerce'
),
$pending_sync_count
);
}
$cot_settings_url = add_query_arg(
array(
'page' => 'wc-settings',
'tab' => 'advanced',
'section' => 'custom_data_stores',
),
admin_url( 'admin.php' )
);
/* translators: %s = URL of the custom data stores settings page */
$manage_cot_settings_link = sprintf( __( "Manage orders synchronization", 'woocommerce' ), $cot_settings_url );
return $desc_tip ? "{$desc_tip}
{$extra_tip} {$manage_cot_settings_link}" : "{$extra_tip} {$manage_cot_settings_link}";
}
}
PK ;[ ] # Orders/OrdersTableDataStoreMeta.phpnu [ array( 'type' => 'int' ),
'order_id' => array( 'type' => 'int' ),
'woocommerce_version' => array(
'type' => 'string',
'name' => 'version',
),
'prices_include_tax' => array(
'type' => 'bool',
'name' => 'prices_include_tax',
),
'coupon_usages_are_counted' => array(
'type' => 'bool',
'name' => 'recorded_coupon_usage_counts',
),
'shipping_tax_amount' => array(
'type' => 'decimal',
'name' => 'shipping_tax',
),
'shipping_total_amount' => array(
'type' => 'decimal',
'name' => 'shipping_total',
),
'discount_tax_amount' => array(
'type' => 'decimal',
'name' => 'discount_tax',
),
'discount_total_amount' => array(
'type' => 'decimal',
'name' => 'discount_total',
),
);
/**
* Delete a refund order from database.
*
* @param \WC_Order $refund Refund object to delete.
* @param array $args Array of args to pass to the delete method.
*
* @return void
*/
public function delete( &$refund, $args = array() ) {
$refund_id = $refund->get_id();
if ( ! $refund_id ) {
return;
}
$this->delete_order_data_from_custom_order_tables( $refund_id );
$refund->set_id( 0 );
// If this datastore method is called while the posts table is authoritative, refrain from deleting post data.
if ( ! is_a( $refund->get_data_store(), self::class ) ) {
return;
}
// Delete the associated post, which in turn deletes order items, etc. through {@see WC_Post_Data}.
// Once we stop creating posts for orders, we should do the cleanup here instead.
wp_delete_post( $refund_id );
}
/**
* Read a refund object from custom tables.
*
* @param \WC_Abstract_Order $refund Refund object.
*
* @return void
*/
public function read( &$refund ) {
parent::read( $refund );
$this->set_refund_props( $refund );
}
/**
* Read multiple refund objects from custom tables.
*
* @param \WC_Order $refunds Refund objects.
*/
public function read_multiple( &$refunds ) {
parent::read_multiple( $refunds );
foreach ( $refunds as $refund ) {
$this->set_refund_props( $refund );
}
}
/**
* Helper method to set refund props.
*
* @param \WC_Order $refund Refund object.
*/
private function set_refund_props( $refund ) {
$refund->set_props(
array(
'amount' => $refund->get_meta( '_refund_amount', true ),
'refunded_by' => $refund->get_meta( '_refunded_by', true ),
'refunded_payment' => wc_string_to_bool( $refund->get_meta( '_refunded_payment', true ) ),
'reason' => $refund->get_meta( '_refund_reason', true ),
)
);
}
/**
* Method to create a refund in the database.
*
* @param \WC_Abstract_Order $refund Refund object.
*/
public function create( &$refund ) {
$refund->set_status( 'completed' ); // Refund are always marked completed.
$this->persist_save( $refund );
}
/**
* Update refund in database.
*
* @param \WC_Order $refund Refund object.
*/
public function update( &$refund ) {
$this->persist_updates( $refund );
}
/**
* Helper method that updates post meta based on an refund object.
* Mostly used for backwards compatibility purposes in this datastore.
*
* @param \WC_Order $refund Refund object.
*/
public function update_order_meta( &$refund ) {
parent::update_order_meta( $refund );
// Update additional props.
$updated_props = array();
$meta_key_to_props = array(
'_refund_amount' => 'amount',
'_refunded_by' => 'refunded_by',
'_refunded_payment' => 'refunded_payment',
'_refund_reason' => 'reason',
);
$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 );
$updated_props[] = $prop;
}
/**
* Fires after updating meta for a order refund.
*
* @since 2.7.0
*/
do_action( 'woocommerce_order_refund_object_updated_props', $refund, $updated_props );
}
/**
* Get a title for the new post type.
*
* @return string
*/
protected function get_post_title() {
return sprintf(
/* translators: %s: Order date */
__( 'Refund – %s', 'woocommerce' ),
( new \DateTime( 'now' ) )->format( _x( 'M d, Y @ h:i A', 'Order date parsed by DateTime::format', 'woocommerce' ) ) // phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment, WordPress.WP.I18n.UnorderedPlaceholdersText
);
}
/**
* Returns data store object to use backfilling.
*
* @return \WC_Order_Refund_Data_Store_CPT
*/
protected function get_post_data_store_for_backfill() {
return new \WC_Order_Refund_Data_Store_CPT();
}
}
PK ;[s<( <( Orders/OrdersTableDataStore.phpnu [ data_store_meta = $data_store_meta;
$this->database_util = $database_util;
$this->error_logger = $legacy_proxy->call_function( 'wc_get_logger' );
$this->internal_meta_keys = $this->get_internal_meta_keys();
}
/**
* Get the custom orders table name.
*
* @return string The custom orders table name.
*/
public static function get_orders_table_name() {
global $wpdb;
return $wpdb->prefix . 'wc_orders';
}
/**
* Get the order addresses table name.
*
* @return string The order addresses table name.
*/
public static function get_addresses_table_name() {
global $wpdb;
return $wpdb->prefix . 'wc_order_addresses';
}
/**
* Get the orders operational data table name.
*
* @return string The orders operational data table name.
*/
public static function get_operational_data_table_name() {
global $wpdb;
return $wpdb->prefix . 'wc_order_operational_data';
}
/**
* Get the orders meta data table name.
*
* @return string Name of order meta data table.
*/
public static function get_meta_table_name() {
global $wpdb;
return $wpdb->prefix . 'wc_orders_meta';
}
/**
* Get the names of all the tables involved in the custom orders table feature.
*
* @return string[]
*/
public function get_all_table_names() {
return array(
$this->get_orders_table_name(),
$this->get_addresses_table_name(),
$this->get_operational_data_table_name(),
$this->get_meta_table_name(),
);
}
/**
* Table column to WC_Order mapping for wc_orders table.
*
* @var \string[][]
*/
protected $order_column_mapping = array(
'id' => array(
'type' => 'int',
'name' => 'id',
),
'status' => array(
'type' => 'string',
'name' => 'status',
),
'type' => array(
'type' => 'string',
'name' => 'type',
),
'currency' => array(
'type' => 'string',
'name' => 'currency',
),
'tax_amount' => array(
'type' => 'decimal',
'name' => 'cart_tax',
),
'total_amount' => array(
'type' => 'decimal',
'name' => 'total',
),
'customer_id' => array(
'type' => 'int',
'name' => 'customer_id',
),
'billing_email' => array(
'type' => 'string',
'name' => 'billing_email',
),
'date_created_gmt' => array(
'type' => 'date',
'name' => 'date_created',
),
'date_updated_gmt' => array(
'type' => 'date',
'name' => 'date_modified',
),
'parent_order_id' => array(
'type' => 'int',
'name' => 'parent_id',
),
'payment_method' => array(
'type' => 'string',
'name' => 'payment_method',
),
'payment_method_title' => array(
'type' => 'string',
'name' => 'payment_method_title',
),
'ip_address' => array(
'type' => 'string',
'name' => 'customer_ip_address',
),
'transaction_id' => array(
'type' => 'string',
'name' => 'transaction_id',
),
'user_agent' => array(
'type' => 'string',
'name' => 'customer_user_agent',
),
'customer_note' => array(
'type' => 'string',
'name' => 'customer_note',
),
);
/**
* Table column to WC_Order mapping for billing addresses in wc_address table.
*
* @var \string[][]
*/
protected $billing_address_column_mapping = array(
'id' => array( 'type' => 'int' ),
'order_id' => array( 'type' => 'int' ),
'address_type' => array( 'type' => 'string' ),
'first_name' => array(
'type' => 'string',
'name' => 'billing_first_name',
),
'last_name' => array(
'type' => 'string',
'name' => 'billing_last_name',
),
'company' => array(
'type' => 'string',
'name' => 'billing_company',
),
'address_1' => array(
'type' => 'string',
'name' => 'billing_address_1',
),
'address_2' => array(
'type' => 'string',
'name' => 'billing_address_2',
),
'city' => array(
'type' => 'string',
'name' => 'billing_city',
),
'state' => array(
'type' => 'string',
'name' => 'billing_state',
),
'postcode' => array(
'type' => 'string',
'name' => 'billing_postcode',
),
'country' => array(
'type' => 'string',
'name' => 'billing_country',
),
'email' => array(
'type' => 'string',
'name' => 'billing_email',
),
'phone' => array(
'type' => 'string',
'name' => 'billing_phone',
),
);
/**
* Table column to WC_Order mapping for shipping addresses in wc_address table.
*
* @var \string[][]
*/
protected $shipping_address_column_mapping = array(
'id' => array( 'type' => 'int' ),
'order_id' => array( 'type' => 'int' ),
'address_type' => array( 'type' => 'string' ),
'first_name' => array(
'type' => 'string',
'name' => 'shipping_first_name',
),
'last_name' => array(
'type' => 'string',
'name' => 'shipping_last_name',
),
'company' => array(
'type' => 'string',
'name' => 'shipping_company',
),
'address_1' => array(
'type' => 'string',
'name' => 'shipping_address_1',
),
'address_2' => array(
'type' => 'string',
'name' => 'shipping_address_2',
),
'city' => array(
'type' => 'string',
'name' => 'shipping_city',
),
'state' => array(
'type' => 'string',
'name' => 'shipping_state',
),
'postcode' => array(
'type' => 'string',
'name' => 'shipping_postcode',
),
'country' => array(
'type' => 'string',
'name' => 'shipping_country',
),
'email' => array( 'type' => 'string' ),
'phone' => array(
'type' => 'string',
'name' => 'shipping_phone',
),
);
/**
* Table column to WC_Order mapping for wc_operational_data table.
*
* @var \string[][]
*/
protected $operational_data_column_mapping = array(
'id' => array( 'type' => 'int' ),
'order_id' => array( 'type' => 'int' ),
'created_via' => array(
'type' => 'string',
'name' => 'created_via',
),
'woocommerce_version' => array(
'type' => 'string',
'name' => 'version',
),
'prices_include_tax' => array(
'type' => 'bool',
'name' => 'prices_include_tax',
),
'coupon_usages_are_counted' => array(
'type' => 'bool',
'name' => 'recorded_coupon_usage_counts',
),
'download_permission_granted' => array(
'type' => 'bool',
'name' => 'download_permissions_granted',
),
'cart_hash' => array(
'type' => 'string',
'name' => 'cart_hash',
),
'new_order_email_sent' => array(
'type' => 'bool',
'name' => 'new_order_email_sent',
),
'order_key' => array(
'type' => 'string',
'name' => 'order_key',
),
'order_stock_reduced' => array(
'type' => 'bool',
'name' => 'order_stock_reduced',
),
'date_paid_gmt' => array(
'type' => 'date',
'name' => 'date_paid',
),
'date_completed_gmt' => array(
'type' => 'date',
'name' => 'date_completed',
),
'shipping_tax_amount' => array(
'type' => 'decimal',
'name' => 'shipping_tax',
),
'shipping_total_amount' => array(
'type' => 'decimal',
'name' => 'shipping_total',
),
'discount_tax_amount' => array(
'type' => 'decimal',
'name' => 'discount_tax',
),
'discount_total_amount' => array(
'type' => 'decimal',
'name' => 'discount_total',
),
'recorded_sales' => array(
'type' => 'bool',
'name' => 'recorded_sales',
),
);
/**
* Cache variable to store combined mapping.
*
* @var array[][][]
*/
private $all_order_column_mapping;
/**
* Return combined mappings for all order tables.
*
* @return array|\array[][][] Return combined mapping.
*/
public function get_all_order_column_mappings() {
if ( ! isset( $this->all_order_column_mapping ) ) {
$this->all_order_column_mapping = array(
'orders' => $this->order_column_mapping,
'billing_address' => $this->billing_address_column_mapping,
'shipping_address' => $this->shipping_address_column_mapping,
'operational_data' => $this->operational_data_column_mapping,
);
}
return $this->all_order_column_mapping;
}
/**
* 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';
}
/**
* Helper function to get alias for address table, this is used in select query.
*
* @param string $type Address type.
*
* @return string Alias.
*/
private function get_address_table_alias( string $type ) : string {
return "address_$type";
}
/**
* Helper method to get a CPT data store instance to use.
*
* @return \WC_Order_Data_Store_CPT Data store instance.
*/
public function get_cpt_data_store_instance() {
if ( ! isset( $this->cpt_data_store ) ) {
$this->cpt_data_store = $this->get_post_data_store_for_backfill();
}
return $this->cpt_data_store;
}
/**
* Returns data store object to use backfilling.
*
* @return \Abstract_WC_Order_Data_Store_CPT
*/
protected function get_post_data_store_for_backfill() {
return new \WC_Order_Data_Store_CPT();
}
/**
* Backfills order details in to WP_Post DB. Uses WC_Order_Data_store_CPT.
*
* @param \WC_Abstract_Order $order Order object to backfill.
*/
public function backfill_post_record( $order ) {
$cpt_data_store = $this->get_post_data_store_for_backfill();
if ( is_null( $cpt_data_store ) || ! method_exists( $cpt_data_store, 'update_order_from_object' ) ) {
return;
}
$cpt_data_store->update_order_from_object( $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" ) ) &&
is_callable( array( $this, "get_$getter_name" ) )
) {
call_user_func_array(
array(
$cpt_data_store,
"set_$getter_name",
),
array(
$order,
$this->{"get_$getter_name"}( $order ),
)
);
}
}
}
/**
* Get information about whether permissions are granted yet.
*
* @param \WC_Order $order Order object.
*
* @return bool Whether permissions are granted.
*/
public function get_download_permissions_granted( $order ) {
$order = is_int( $order ) ? wc_get_order( $order ) : $order;
return $order->get_download_permissions_granted();
}
/**
* Stores information about whether permissions were generated yet.
*
* @param \WC_Order $order Order ID or order object.
* @param bool $set True or false.
*/
public function set_download_permissions_granted( $order, $set ) {
if ( is_int( $order ) ) {
$order = wc_get_order( $order );
}
$order->set_download_permissions_granted( $set );
$order->save();
}
/**
* Gets information about whether sales were recorded.
*
* @param \WC_Order $order Order object.
*
* @return bool Whether sales are recorded.
*/
public function get_recorded_sales( $order ) {
$order = is_int( $order ) ? wc_get_order( $order ) : $order;
return $order->get_recorded_sales();
}
/**
* Stores information about whether sales were recorded.
*
* @param \WC_Order $order Order object.
* @param bool $set True or false.
*/
public function set_recorded_sales( $order, $set ) {
if ( is_int( $order ) ) {
$order = wc_get_order( $order );
}
$order->set_recorded_sales( $set );
$order->save();
}
/**
* Gets information about whether coupon counts were updated.
*
* @param \WC_Order $order Order object.
*
* @return bool Whether coupon counts were updated.
*/
public function get_recorded_coupon_usage_counts( $order ) {
$order = is_int( $order ) ? wc_get_order( $order ) : $order;
return $order->get_recorded_coupon_usage_counts();
}
/**
* Stores information about whether coupon counts were updated.
*
* @param \WC_Order $order Order object.
* @param bool $set True or false.
*/
public function set_recorded_coupon_usage_counts( $order, $set ) {
if ( is_int( $order ) ) {
$order = wc_get_order( $order );
}
$order->set_recorded_coupon_usage_counts( $set );
$order->save();
}
/**
* Whether email have been sent for this order.
*
* @param \WC_Order|int $order Order object.
*
* @return bool Whether email is sent.
*/
public function get_email_sent( $order ) {
$order = is_int( $order ) ? wc_get_order( $order ) : $order;
return $order->get_new_order_email_sent();
}
/**
* Stores information about whether email was sent.
*
* @param \WC_Order $order Order object.
* @param bool $set True or false.
*/
public function set_email_sent( $order, $set ) {
if ( is_int( $order ) ) {
$order = wc_get_order( $order );
}
$order->set_new_order_email_sent( $set );
$order->save();
}
/**
* Helper setter for email_sent.
*
* @param \WC_Order $order Order object.
*
* @return bool Whether email was sent.
*/
public function get_new_order_email_sent( $order ) {
$order = is_int( $order ) ? wc_get_order( $order ) : $order;
return $order->get_new_order_email_sent();
}
/**
* Helper setter for new order email sent.
*
* @param \WC_Order $order Order object.
* @param bool $set True or false.
*/
public function set_new_order_email_sent( $order, $set ) {
if ( is_int( $order ) ) {
$order = wc_get_order( $order );
}
$order->set_new_order_email_sent( $set );
$order->save();
}
/**
* Gets information about whether stock was reduced.
*
* @param \WC_Order $order Order object.
*
* @return bool Whether stock was reduced.
*/
public function get_stock_reduced( $order ) {
$order = is_int( $order ) ? wc_get_order( $order ) : $order;
return $order->get_order_stock_reduced();
}
/**
* Stores information about whether stock was reduced.
*
* @param \WC_Order $order Order ID or order object.
* @param bool $set True or false.
*/
public function set_stock_reduced( $order, $set ) {
if ( is_int( $order ) ) {
$order = wc_get_order( $order );
}
$order->set_order_stock_reduced( $set );
$order->save();
}
/**
* Helper getter for `order_stock_reduced`.
*
* @param \WC_Order $order Order object.
* @return bool Whether stock was reduced.
*/
public function get_order_stock_reduced( $order ) {
return $this->get_stock_reduced( $order );
}
/**
* Helper setter for `order_stock_reduced`.
*
* @param \WC_Order $order Order ID or order object.
* @param bool $set Whether stock was reduced.
*/
public function set_order_stock_reduced( $order, $set ) {
$this->set_stock_reduced( $order, $set );
}
/**
* Get amount already refunded.
*
* @param \WC_Order $order Order object.
*
* @return float Refunded amount.
*/
public function get_total_refunded( $order ) {
global $wpdb;
$order_table = self::get_orders_table_name();
$total = $wpdb->get_var(
$wpdb->prepare(
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $order_table is hardcoded.
"
SELECT SUM( total_amount ) FROM $order_table
WHERE
type = %s AND
parent_order_id = %d
;
",
// phpcs:enable
'shop_order_refund',
$order->get_id()
)
);
return -1 * ( isset( $total ) ? $total : 0 );
}
/**
* Get the total tax refunded.
*
* @param WC_Order $order Order object.
* @return float
*/
public function get_total_tax_refunded( $order ) {
global $wpdb;
$order_table = self::get_orders_table_name();
$total = $wpdb->get_var(
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $order_table is hardcoded.
$wpdb->prepare(
"SELECT SUM( order_itemmeta.meta_value )
FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta
INNER JOIN $order_table AS orders ON ( orders.type = 'shop_order_refund' AND orders.parent_order_id = %d )
INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = orders.id AND order_items.order_item_type = 'tax' )
WHERE order_itemmeta.order_item_id = order_items.order_item_id
AND order_itemmeta.meta_key IN ('tax_amount', 'shipping_tax_amount')",
$order->get_id()
)
) ?? 0;
// phpcs:enable
return abs( $total );
}
/**
* Get the total shipping refunded.
*
* @param WC_Order $order Order object.
* @return float
*/
public function get_total_shipping_refunded( $order ) {
global $wpdb;
$order_table = self::get_orders_table_name();
$total = $wpdb->get_var(
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $order_table is hardcoded.
$wpdb->prepare(
"SELECT SUM( order_itemmeta.meta_value )
FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta
INNER JOIN $order_table AS orders ON ( orders.type = 'shop_order_refund' AND orders.parent_order_id = %d )
INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = orders.id AND order_items.order_item_type = 'shipping' )
WHERE order_itemmeta.order_item_id = order_items.order_item_id
AND order_itemmeta.meta_key IN ('cost')",
$order->get_id()
)
) ?? 0;
// phpcs:enable
return abs( $total );
}
/**
* Finds an Order ID based on an order key.
*
* @param string $order_key An order key has generated by.
* @return int The ID of an order, or 0 if the order could not be found
*/
public function get_order_id_by_order_key( $order_key ) {
global $wpdb;
$orders_table = self::get_orders_table_name();
$op_table = self::get_operational_data_table_name();
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT {$orders_table}.id FROM {$orders_table}
INNER JOIN {$op_table} ON {$op_table}.order_id = {$orders_table}.id
WHERE {$op_table}.order_key = %s",
$order_key
)
);
// phpcs:enable
}
/**
* Return count of orders with a specific status.
*
* @param string $status Order status. Function wc_get_order_statuses() returns a list of valid statuses.
* @return int
*/
public function get_order_count( $status ) {
global $wpdb;
$orders_table = self::get_orders_table_name();
return absint( $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$orders_table} WHERE type = %s AND status = %s", 'shop_order', $status ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
/**
* Get all orders matching the passed in args.
*
* @deprecated 3.1.0 - Use {@see wc_get_orders} instead.
* @param array $args List of args passed to wc_get_orders().
* @return array|object
*/
public function get_orders( $args = array() ) {
wc_deprecated_function( __METHOD__, '3.1.0', 'Use wc_get_orders instead.' );
return wc_get_orders( $args );
}
/**
* Get unpaid orders last updated before the specified date.
*
* @param int $date Timestamp.
* @return array
*/
public function get_unpaid_orders( $date ) {
global $wpdb;
$orders_table = self::get_orders_table_name();
$order_types_sql = "('" . implode( "','", wc_get_order_types() ) . "')";
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return $wpdb->get_col(
$wpdb->prepare(
"SELECT id FROM {$orders_table} WHERE
{$orders_table}.type IN {$order_types_sql}
AND {$orders_table}.status = %s
AND {$orders_table}.date_updated_gmt < %s",
'wc-pending',
gmdate( 'Y-m-d H:i:s', absint( $date ) )
)
);
// phpcs:enable
}
/**
* Search order data for a term and return matching order IDs.
*
* @param string $term Search term.
*
* @return int[] Array of order IDs.
*/
public function search_orders( $term ) {
$order_ids = wc_get_orders(
array(
's' => $term,
'return' => 'ids',
)
);
/**
* Provides an opportunity to modify the list of order IDs obtained during an order search.
*
* This hook is used for Custom Order Table queries. For Custom Post Type order searches, the corresponding hook
* is `woocommerce_shop_order_search_results`.
*
* @since 7.0.0
*
* @param int[] $order_ids Search results as an array of order IDs.
* @param string $term The search term.
*/
return array_map( 'intval', (array) apply_filters( 'woocommerce_cot_shop_order_search_results', $order_ids, $term ) );
}
/**
* Fetch order type for orders in bulk.
*
* @param array $order_ids Order IDs.
*
* @return array array( $order_id1 => $type1, ... ) Array for all orders.
*/
public function get_orders_type( $order_ids ) {
global $wpdb;
if ( empty( $order_ids ) ) {
return array();
}
$orders_table = self::get_orders_table_name();
$order_ids_placeholder = implode( ', ', array_fill( 0, count( $order_ids ), '%d' ) );
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT id, type FROM {$orders_table} WHERE id IN ( $order_ids_placeholder )",
$order_ids
)
);
// phpcs:enable
$order_types = array();
foreach ( $results as $row ) {
$order_types[ $row->id ] = $row->type;
}
return $order_types;
}
/**
* Get order type from DB.
*
* @param int $order_id Order ID.
*
* @return string Order type.
*/
public function get_order_type( $order_id ) {
$type = $this->get_orders_type( array( $order_id ) );
return $type[ $order_id ] ?? '';
}
/**
* Method to read an order from custom tables.
*
* @param \WC_Order $order Order object.
*
* @throws \Exception If passed order is invalid.
*/
public function read( &$order ) {
$orders_array = array( $order->get_id() => $order );
$this->read_multiple( $orders_array );
}
/**
* Reads multiple orders from custom tables in one pass.
*
* @since 6.9.0
* @param array[\WC_Order] $orders Order objects.
* @throws \Exception If passed an invalid order.
*/
public function read_multiple( &$orders ) {
$order_ids = array_keys( $orders );
$data = $this->get_order_data_for_ids( $order_ids );
if ( count( $data ) !== count( $order_ids ) ) {
throw new \Exception( __( 'Invalid order IDs in call to read_multiple()', 'woocommerce' ) );
}
$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
if ( ! $data_synchronizer instanceof DataSynchronizer ) {
return;
}
$data_sync_enabled = $data_synchronizer->data_sync_is_enabled() && 0 === $data_synchronizer->get_current_orders_pending_sync_count_cached();
$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();
foreach ( $data as $order_data ) {
$order_id = absint( $order_data->id );
$order = $orders[ $order_id ];
$this->init_order_record( $order, $order_id, $order_data );
if ( $data_sync_enabled && $this->should_sync_order( $order ) && isset( $post_orders[ $order_id ] ) ) {
self::$reading_order_ids[] = $order_id;
$this->maybe_sync_order( $order, $post_orders[ $order->get_id() ] );
}
}
}
/**
* Helper method to check whether to sync the order.
*
* @param \WC_Abstract_Order $order Order object.
*
* @return bool Whether the order should be synced.
*/
private function should_sync_order( \WC_Abstract_Order $order ) : bool {
$draft_order = in_array( $order->get_status(), array( 'draft', 'auto-draft' ), true );
$already_synced = in_array( $order->get_id(), self::$reading_order_ids, true );
return ! $draft_order && ! $already_synced;
}
/**
* Helper method to initialize order object from DB data.
*
* @param \WC_Abstract_Order $order Order object.
* @param int $order_id Order ID.
* @param \stdClass $order_data Order data fetched from DB.
*
* @return void
*/
protected function init_order_record( \WC_Abstract_Order &$order, int $order_id, \stdClass $order_data ) {
$order->set_defaults();
$order->set_id( $order_id );
$filtered_meta_data = $this->filter_raw_meta_data( $order, $order_data->meta_data );
$order->init_meta_data( $filtered_meta_data );
$this->set_order_props_from_data( $order, $order_data );
$order->set_object_read( true );
}
/**
* For post based data stores, this was used to filter internal meta data. For custom tables, technically there is no internal meta data,
* (i.e. we store all core data as properties for the order, and not in meta data). So this method is a no-op.
*
* Except that some meta such as billing_address_index and shipping_address_index are infact stored in meta data, so we need to filter those out.
*
* However, declaring $internal_meta_keys is still required so that our backfill and other comparison checks works as expected.
*
* @param \WC_Data $object Object to filter meta data for.
* @param array $raw_meta_data Raw meta data.
*
* @return array Filtered meta data.
*/
public function filter_raw_meta_data( &$object, $raw_meta_data ) {
$filtered_meta_data = parent::filter_raw_meta_data( $object, $raw_meta_data );
$allowed_keys = array(
'_billing_address_index',
'_shipping_address_index',
);
$allowed_meta = array_filter(
$raw_meta_data,
function( $meta ) use ( $allowed_keys ) {
return in_array( $meta->meta_key, $allowed_keys, true );
}
);
return array_merge( $allowed_meta, $filtered_meta_data );
}
/**
* Sync order to/from posts tables if we are able to detect difference between order and posts but the sync is enabled.
*
* @param \WC_Abstract_Order $order Order object.
* @param \WC_Abstract_Order $post_order Order object initialized from post.
*
* @return void
* @throws \Exception If passed an invalid order.
*/
private function maybe_sync_order( \WC_Abstract_Order &$order, \WC_Abstract_Order $post_order ) {
if ( ! $this->is_post_different_from_order( $order, $post_order ) ) {
return;
}
// Modified dates can be empty when the order is created but never updated again. Fallback to created date in those cases.
$order_modified_date = $order->get_date_modified() ?? $order->get_date_created();
$order_modified_date = is_null( $order_modified_date ) ? 0 : $order_modified_date->getTimestamp();
$post_order_modified_date = $post_order->get_date_modified() ?? $post_order->get_date_created();
$post_order_modified_date = is_null( $post_order_modified_date ) ? 0 : $post_order_modified_date->getTimestamp();
/**
* We are here because there was difference in posts and order data, although the sync is enabled.
* When order modified date is more recent than post modified date, it can only mean that COT definitely has more updated version of the order.
*
* In a case where post meta was updated (without updating post_modified date), post_modified would be equal to order_modified date.
*
* So we write back to the order table when order modified date is more recent than post modified date. Otherwise, we write to the post table.
*/
if ( $post_order_modified_date >= $order_modified_date ) {
$this->migrate_post_record( $order, $post_order );
}
}
/**
* Get the post type order representation.
*
* @param \WP_Post $post Post object.
*
* @return \WC_Order Order object.
*/
private function get_cpt_order( $post ) {
$cpt_order = new \WC_Order();
$cpt_order->set_id( $post->ID );
$cpt_data_store = $this->get_cpt_data_store_instance();
$cpt_data_store->read( $cpt_order );
return $cpt_order;
}
/**
* Helper function to get posts data for an order in bullk. We use to this to compute posts object in bulk so that we can compare it with COT data.
*
* @param array $orders List of orders mapped by $order_id.
*
* @return array List of posts.
*/
private function get_post_orders_for_ids( array $orders ): array {
$order_ids = array_keys( $orders );
// We have to bust meta cache, otherwise we will just get the meta cached by OrderTableDataStore.
foreach ( $order_ids as $order_id ) {
wp_cache_delete( WC_Order::generate_meta_cache_key( $order_id, 'orders' ), 'orders' );
}
$cpt_stores = array();
$cpt_store_orders = array();
foreach ( $orders as $order_id => $order ) {
$table_data_store = $order->get_data_store();
$cpt_data_store = $table_data_store->get_cpt_data_store_instance();
$cpt_store_class_name = get_class( $cpt_data_store );
if ( ! isset( $cpt_stores[ $cpt_store_class_name ] ) ) {
$cpt_stores[ $cpt_store_class_name ] = $cpt_data_store;
$cpt_store_orders[ $cpt_store_class_name ] = array();
}
$cpt_store_orders[ $cpt_store_class_name ][ $order_id ] = $order;
}
$cpt_orders = array();
foreach ( $cpt_stores as $cpt_store_name => $cpt_store ) {
// Prime caches if we can.
if ( method_exists( $cpt_store, 'prime_caches_for_orders' ) ) {
$cpt_store->prime_caches_for_orders( array_keys( $cpt_store_orders[ $cpt_store_name ] ), array() );
}
foreach ( $cpt_store_orders[ $cpt_store_name ] as $order_id => $order ) {
$cpt_order_class_name = wc_get_order_type( $order->get_type() )['class_name'];
$cpt_order = new $cpt_order_class_name();
try {
$cpt_order->set_id( $order_id );
$cpt_store->read( $cpt_order );
$cpt_orders[ $order_id ] = $cpt_order;
} catch ( Exception $e ) {
// If the post record has been deleted (for instance, by direct query) then an exception may be thrown.
$this->error_logger->warning(
sprintf(
/* translators: %1$d order ID. */
__( 'Unable to load the post record for order %1$d', 'woocommerce' ),
$order_id
),
array(
'exception_code' => $e->getCode(),
'exception_msg' => $e->getMessage(),
'origin' => __METHOD__,
)
);
}
}
}
return $cpt_orders;
}
/**
* Computes whether post has been updated after last order. Tries to do it as efficiently as possible.
*
* @param \WC_Abstract_Order $order Order object.
* @param \WC_Abstract_Order $post_order Order object read from posts table.
*
* @return bool True if post is different than order.
*/
private function is_post_different_from_order( $order, $post_order ): bool {
if ( ArrayUtil::deep_compare_array_diff( $order->get_base_data(), $post_order->get_base_data(), false ) ) {
return true;
}
$meta_diff = $this->get_diff_meta_data_between_orders( $order, $post_order );
if ( ! empty( $meta_diff ) ) {
return true;
}
return false;
}
/**
* Migrate meta data from post to order.
*
* @param \WC_Abstract_Order $order Order object.
* @param \WC_Abstract_Order $post_order Order object read from posts table.
*
* @return array List of meta data that was migrated.
*/
private function migrate_meta_data_from_post_order( \WC_Abstract_Order &$order, \WC_Abstract_Order $post_order ) {
$diff = $this->get_diff_meta_data_between_orders( $order, $post_order, true );
$order->save_meta_data();
return $diff;
}
/**
* Helper function to compute diff between metadata of post and cot data for an order.
*
* Also provides an option to sync the metadata as well, since we are already computing the diff.
*
* @param \WC_Abstract_Order $order1 Order object read from posts.
* @param \WC_Abstract_Order $order2 Order object read from COT.
* @param bool $sync Whether to also sync the meta data.
*
* @return array Difference between post and COT meta data.
*/
private function get_diff_meta_data_between_orders( \WC_Abstract_Order &$order1, \WC_Abstract_Order $order2, $sync = false ): array {
$order1_meta = ArrayUtil::select( $order1->get_meta_data(), 'get_data', ArrayUtil::SELECT_BY_OBJECT_METHOD );
$order2_meta = ArrayUtil::select( $order2->get_meta_data(), 'get_data', ArrayUtil::SELECT_BY_OBJECT_METHOD );
$order1_meta_by_key = ArrayUtil::select_as_assoc( $order1_meta, 'key', ArrayUtil::SELECT_BY_ARRAY_KEY );
$order2_meta_by_key = ArrayUtil::select_as_assoc( $order2_meta, 'key', ArrayUtil::SELECT_BY_ARRAY_KEY );
$diff = array();
foreach ( $order1_meta_by_key as $key => $value ) {
if ( in_array( $key, $this->internal_meta_keys, true ) ) {
// These should have already been verified in the base data comparison.
continue;
}
$order1_values = ArrayUtil::select( $value, 'value', ArrayUtil::SELECT_BY_ARRAY_KEY );
if ( ! array_key_exists( $key, $order2_meta_by_key ) ) {
$sync && $order1->delete_meta_data( $key );
$diff[ $key ] = $order1_values;
unset( $order2_meta_by_key[ $key ] );
continue;
}
$order2_values = ArrayUtil::select( $order2_meta_by_key[ $key ], 'value', ArrayUtil::SELECT_BY_ARRAY_KEY );
$new_diff = ArrayUtil::deep_assoc_array_diff( $order1_values, $order2_values );
if ( ! empty( $new_diff ) && $sync ) {
if ( count( $order2_values ) > 1 ) {
$sync && $order1->delete_meta_data( $key );
foreach ( $order2_values as $post_order_value ) {
$sync && $order1->add_meta_data( $key, $post_order_value, false );
}
} else {
$sync && $order1->update_meta_data( $key, $order2_values[0] );
}
$diff[ $key ] = $new_diff;
unset( $order2_meta_by_key[ $key ] );
}
}
foreach ( $order2_meta_by_key as $key => $value ) {
if ( array_key_exists( $key, $order1_meta_by_key ) || in_array( $key, $this->internal_meta_keys, true ) ) {
continue;
}
$order2_values = ArrayUtil::select( $value, 'value', ArrayUtil::SELECT_BY_ARRAY_KEY );
foreach ( $order2_values as $meta_value ) {
$sync && $order1->add_meta_data( $key, $meta_value );
}
$diff[ $key ] = $order2_values;
}
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 {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- This is a log function.
$this->error_logger->notice( 'Diff found: ' . print_r( $diff, true ) );
}
/**
* Migrate post record from a given order object.
*
* @param \WC_Abstract_Order $order Order object.
* @param \WC_Abstract_Order $post_order Order object read from posts.
*
* @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 );
$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 );
}
/**
* Sets order properties based on a row from the database.
*
* @param \WC_Abstract_Order $order The order object.
* @param object $order_data A row of order data from the database.
*/
private function set_order_props_from_data( &$order, $order_data ) {
foreach ( $this->get_all_order_column_mappings() as $table_name => $column_mapping ) {
foreach ( $column_mapping as $column_name => $prop_details ) {
if ( ! isset( $prop_details['name'] ) ) {
continue;
}
$prop_value = $order_data->{$prop_details['name']};
if ( is_null( $prop_value ) ) {
continue;
}
if ( 'date' === $prop_details['type'] ) {
$prop_value = $this->string_to_timestamp( $prop_value );
}
$this->set_order_prop( $order, $prop_details['name'], $prop_value );
}
}
}
/**
* Set order prop if a setter exists in either the order object or in the data store.
*
* @param \WC_Abstract_Order $order Order object.
* @param string $prop_name Property name.
* @param mixed $prop_value Property value.
*
* @return bool True if the property was set, false otherwise.
*/
private function set_order_prop( \WC_Abstract_Order $order, string $prop_name, $prop_value ) {
$prop_setter_function_name = "set_{$prop_name}";
if ( is_callable( array( $order, $prop_setter_function_name ) ) ) {
return $order->{$prop_setter_function_name}( $prop_value );
} elseif ( is_callable( array( $this, $prop_setter_function_name ) ) ) {
return $this->{$prop_setter_function_name}( $order, $prop_value, false );
}
return false;
}
/**
* Return order data for a single order ID.
*
* @param int $id Order ID.
*
* @return object|\WP_Error DB order object or WP_Error.
*/
private function get_order_data_for_id( $id ) {
$results = $this->get_order_data_for_ids( array( $id ) );
return is_array( $results ) && count( $results ) > 0 ? $results[ $id ] : $results;
}
/**
* Return order data for multiple IDs.
*
* @param array $ids List of order IDs.
*
* @return \stdClass[]|object|null DB Order objects or error.
*/
protected function get_order_data_for_ids( $ids ) {
if ( ! $ids ) {
return array();
}
global $wpdb;
if ( empty( $ids ) ) {
return array();
}
$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();
// 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 )",
$ids
)
);
// phpcs:enable
$meta_data_query = $this->get_order_meta_select_statement();
$order_data = array();
$meta_data = $wpdb->get_results(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $meta_data_query and $order_meta_table is autogenerated and should already be prepared. $id_placeholder is already prepared.
"$meta_data_query WHERE $order_meta_table.order_id in ( $id_placeholder )",
$ids
)
);
foreach ( $table_data as $table_datum ) {
$order_data[ $table_datum->id ] = $table_datum;
$order_data[ $table_datum->id ]->meta_data = array();
}
foreach ( $meta_data as $meta_datum ) {
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Not a meta query.
$order_data[ $meta_datum->order_id ]->meta_data[] = (object) array(
'meta_id' => $meta_datum->id,
'meta_key' => $meta_datum->meta_key,
'meta_value' => $meta_datum->meta_value,
);
// phpcs:enable
}
return $order_data;
}
/**
* Helper method to generate combined select statement.
*
* @return string Select SQL statement to fetch order.
*/
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 );
$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();
$billing_address_clauses = $this->join_billing_address_table_to_order_query( $order_table_alias, $billing_address_table_alias );
$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 );
return "
SELECT $select_clause, {$billing_address_clauses['select']}, {$shipping_address_clauses['select']}, {$operational_data_clauses['select']}
FROM $order_table $order_table_alias
LEFT JOIN {$billing_address_clauses['join']}
LEFT JOIN {$shipping_address_clauses['join']}
LEFT JOIN {$operational_data_clauses['join']}
";
}
/**
* Helper function to generate select statement for fetching metadata in bulk.
*
* @return string Select SQL statement to fetch order metadata.
*/
private function get_order_meta_select_statement() {
$order_meta_table = self::get_meta_table_name();
return "
SELECT $order_meta_table.id, $order_meta_table.order_id, $order_meta_table.meta_key, $order_meta_table.meta_value
FROM $order_meta_table
";
}
/**
* Helper method to generate join query for billing addresses in wc_address table.
*
* @param string $order_table_alias Alias for order table to use in join.
* @param string $address_table_alias Alias for address table to use in join.
*
* @return array Select and join statements for billing address table.
*/
private function join_billing_address_table_to_order_query( $order_table_alias, $address_table_alias ) {
return $this->join_address_table_order_query( 'billing', $order_table_alias, $address_table_alias );
}
/**
* Helper method to generate join query for shipping addresses in wc_address table.
*
* @param string $order_table_alias Alias for order table to use in join.
* @param string $address_table_alias Alias for address table to use in join.
*
* @return array Select and join statements for shipping address table.
*/
private function join_shipping_address_table_to_order_query( $order_table_alias, $address_table_alias ) {
return $this->join_address_table_order_query( 'shipping', $order_table_alias, $address_table_alias );
}
/**
* 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 $order_table_alias Alias of order table to use.
* @param string $address_table_alias Alias for address table to use.
*
* @return array Select and join statements for address table.
*/
private function join_address_table_order_query( $address_type, $order_table_alias, $address_table_alias ) {
global $wpdb;
$address_table = $this::get_addresses_table_name();
$column_props_map = 'billing' === $address_type ? $this->billing_address_column_mapping : $this->shipping_address_column_mapping;
$clauses = $this->generate_select_and_join_clauses( $order_table_alias, $address_table, $address_table_alias, $column_props_map );
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $clauses['join'] and $address_table_alias are hardcoded.
$clauses['join'] = $wpdb->prepare(
"{$clauses['join']} AND $address_table_alias.address_type = %s",
$address_type
);
// phpcs:enable
return array(
'select' => $clauses['select'],
'join' => $clauses['join'],
);
}
/**
* Helper method to join order operational data table.
*
* @param string $order_table_alias Alias to use for order table.
* @param string $operational_table_alias Alias to use for operational data table.
*
* @return array Select and join queries for operational data table.
*/
private function join_operational_data_table_to_order_query( $order_table_alias, $operational_table_alias ) {
$operational_data_table = $this::get_operational_data_table_name();
return $this->generate_select_and_join_clauses(
$order_table_alias,
$operational_data_table,
$operational_table_alias,
$this->operational_data_column_mapping
);
}
/**
* Helper method to generate join and select clauses.
*
* @param string $order_table_alias Alias for order table.
* @param string $table Table to join.
* @param string $table_alias Alias for table to join.
* @param array[] $column_props_map Column to prop map for table to join.
*
* @return array Select and join queries.
*/
private function generate_select_and_join_clauses( $order_table_alias, $table, $table_alias, $column_props_map ) {
// Add aliases to column names so they will be unique when fetching.
$select_clause = $this->generate_select_clause_for_props( $table_alias, $column_props_map );
$join_clause = "$table $table_alias ON $table_alias.order_id = $order_table_alias.id";
return array(
'select' => $select_clause,
'join' => $join_clause,
);
}
/**
* Helper method to generate select clause for props.
*
* @param string $table_alias Alias for table.
* @param array[] $props Props to column mapping for table.
*
* @return string Select clause.
*/
private function generate_select_clause_for_props( $table_alias, $props ) {
$select_clauses = array();
foreach ( $props as $column_name => $prop_details ) {
$select_clauses[] = isset( $prop_details['name'] ) ? "$table_alias.$column_name as {$prop_details['name']}" : "$table_alias.$column_name as {$table_alias}_$column_name";
}
return implode( ', ', $select_clauses );
}
/**
* Persists order changes to the database.
*
* @param \WC_Abstract_Order $order The order.
* @param bool $force_all_fields Force saving all fields to DB and just changed.
*
* @throws \Exception If order data is not valid.
*
* @since 6.8.0
*/
protected function persist_order_to_db( &$order, bool $force_all_fields = false ) {
$context = ( 0 === absint( $order->get_id() ) ) ? 'create' : 'update';
$data_sync = wc_get_container()->get( DataSynchronizer::class );
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',
)
);
if ( ! $post_id ) {
throw new \Exception( __( 'Could not create order in posts table.', 'woocommerce' ) );
}
$order->set_id( $post_id );
}
$only_changes = ! $force_all_fields && 'update' === $context;
// Figure out what needs to be updated in the database.
$db_updates = $this->get_db_rows_for_order( $order, $context, $only_changes );
// Persist changes.
foreach ( $db_updates as $update ) {
// Make sure 'data' and 'format' entries match before passing to $wpdb.
ksort( $update['data'] );
ksort( $update['format'] );
$result = $this->database_util->insert_on_duplicate_key_update(
$update['table'],
$update['data'],
array_values( $update['format'] )
);
if ( false === $result ) {
// translators: %s is a table name.
throw new \Exception( sprintf( __( 'Could not persist order to database table "%s".', 'woocommerce' ), $update['table'] ) );
}
}
$changes = $order->get_changes();
$this->update_address_index_meta( $order, $changes );
}
/**
* Generates an array of rows with all the details required to insert or update an order in the database.
*
* @param \WC_Abstract_Order $order The order.
* @param string $context The context: 'create' or 'update'.
* @param boolean $only_changes Whether to consider only changes in the order for generating the rows.
*
* @return array
* @throws \Exception When invalid data is found for the given context.
*
* @since 6.8.0
*/
protected function get_db_rows_for_order( \WC_Abstract_Order $order, string $context = 'create', bool $only_changes = false ): array {
$result = array();
$row = $this->get_db_row_from_order( $order, $this->order_column_mapping, $only_changes );
if ( 'create' === $context && ! $row ) {
throw new \Exception( 'No data for new record.' ); // This shouldn't occur.
}
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' ) ),
);
}
// wc_order_operational_data.
$row = $this->get_db_row_from_order( $order, $this->operational_data_column_mapping, $only_changes );
if ( $row ) {
$result[] = array(
'table' => self::get_operational_data_table_name(),
'data' => array_merge( $row['data'], array( 'order_id' => $order->get_id() ) ),
'format' => array_merge( $row['format'], array( 'order_id' => '%d' ) ),
);
}
// wc_order_addresses.
foreach ( array( 'billing', 'shipping' ) as $address_type ) {
$row = $this->get_db_row_from_order( $order, $this->{$address_type . '_address_column_mapping'}, $only_changes );
if ( $row ) {
$result[] = array(
'table' => self::get_addresses_table_name(),
'data' => array_merge(
$row['data'],
array(
'order_id' => $order->get_id(),
'address_type' => $address_type,
)
),
'format' => array_merge(
$row['format'],
array(
'order_id' => '%d',
'address_type' => '%s',
)
),
);
}
}
/**
* Allow third parties to include rows that need to be inserted/updated in custom tables when persisting an order.
*
* @since 6.8.0
*
* @param array Array of rows to be inserted/updated when persisting an order. Each entry should be an array with
* keys 'table', 'data' (the row), 'format' (row format), 'where' and 'where_format'.
* @param \WC_Order The order object.
* @param string The context of the operation: 'create' or 'update'.
*/
$ext_rows = apply_filters( 'woocommerce_orders_table_datastore_extra_db_rows_for_order', array(), $order, $context );
return array_merge( $result, $ext_rows );
}
/**
* Produces an array with keys 'row' and 'format' that can be passed to `$wpdb->update()` as the `$data` and
* `$format` parameters. Values are taken from the order changes array and properly formatted for inclusion in the
* database.
*
* @param \WC_Abstract_Order $order Order.
* @param array $column_mapping Table column mapping.
* @param bool $only_changes Whether to consider only changes in the order object or all fields.
* @return array
*
* @since 6.8.0
*/
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 correct.
if ( array_key_exists( 'status', $column_mapping ) ) {
$changes['status'] = $this->get_post_status( $order );
}
$row = array();
$row_format = array();
foreach ( $column_mapping as $column => $details ) {
if ( ! isset( $details['name'] ) || ! array_key_exists( $details['name'], $changes ) ) {
continue;
}
$row[ $column ] = $this->database_util->format_object_value_for_db( $changes[ $details['name'] ], $details['type'] );
$row_format[ $column ] = $this->database_util->get_wpdb_format_for_type( $details['type'] );
}
if ( ! $row ) {
return false;
}
return array(
'data' => $row,
'format' => $row_format,
);
}
/**
* Method to delete an order from the database.
*
* @param \WC_Abstract_Order $order Order object.
* @param array $args Array of args to pass to the delete method.
*
* @return void
*/
public function delete( &$order, $args = array() ) {
$order_id = $order->get_id();
if ( ! $order_id ) {
return;
}
if ( ! empty( $args['force_delete'] ) ) {
/**
* Fires immediately before an order is deleted from the database.
*
* @since 7.1.0
*
* @param int $order_id ID of the order about to be deleted.
* @param WC_Order $order Instance of the order that is about to be deleted.
*/
do_action( 'woocommerce_before_delete_order', $order_id, $order );
$this->upshift_child_orders( $order );
$this->delete_order_data_from_custom_order_tables( $order_id );
$order->set_id( 0 );
// If this datastore method is called while the posts table is authoritative, refrain from deleting post data.
if ( $order->get_data_store()->get_current_class_name() !== self::class ) {
return;
}
// Delete the associated post, which in turn deletes order items, etc. through {@see WC_Post_Data}.
// Once we stop creating posts for orders, we should do the cleanup here instead.
wp_delete_post( $order_id );
do_action( 'woocommerce_delete_order', $order_id ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
} else {
/**
* Fires immediately before an order is trashed.
*
* @since 7.1.0
*
* @param int $order_id ID of the order about to be deleted.
* @param WC_Order $order Instance of the order that is about to be deleted.
*/
do_action( 'woocommerce_before_trash_order', $order_id, $order );
$this->trash_order( $order );
do_action( 'woocommerce_trash_order', $order_id ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
}
}
/**
* Helper method to set child orders to the parent order's parent.
*
* @param \WC_Abstract_Order $order Order object.
*
* @return void
*/
private function upshift_child_orders( $order ) {
global $wpdb;
$order_table = self::get_orders_table_name();
$order_parent = $order->get_parent_id();
$wpdb->update(
$order_table,
array( 'parent_order_id' => $order_parent ),
array( 'parent_order_id' => $order->get_id() ),
array( '%d' ),
array( '%d' )
);
}
/**
* Trashes an order.
*
* @param WC_Order $order The order object.
*
* @return void
*/
public function trash_order( $order ) {
global $wpdb;
if ( 'trash' === $order->get_status( 'edit' ) ) {
return;
}
$trash_metadata = array(
'_wp_trash_meta_status' => 'wc-' . $order->get_status( 'edit' ),
'_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(
'status' => 'trash',
'date_updated_gmt' => current_time( 'Y-m-d H:i:s', true ),
),
array( 'id' => $order->get_id() ),
array( '%s', '%s' ),
array( '%d' )
);
$order->set_status( 'trash' );
$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
if ( $data_synchronizer->data_sync_is_enabled() ) {
wp_trash_post( $order->get_id() );
}
}
/**
* Attempts to restore the specified order back to its original status (after having been trashed).
*
* @param WC_Order $order The order to be untrashed.
*
* @return bool If the operation was successful.
*/
public function untrash_order( WC_Order $order ): bool {
$id = $order->get_id();
$status = $order->get_status();
if ( 'trash' !== $status ) {
wc_get_logger()->warning(
sprintf(
/* translators: 1: order ID, 2: order status */
__( 'Order %1$d cannot be restored from the trash: it has already been restored to status "%2$s".', 'woocommerce' ),
$id,
$status
)
);
return false;
}
$previous_status = $order->get_meta( '_wp_trash_meta_status' );
$valid_statuses = wc_get_order_statuses();
$previous_state_is_invalid = ! array_key_exists( $previous_status, $valid_statuses );
$pending_is_valid_status = array_key_exists( 'wc-pending', $valid_statuses );
if ( $previous_state_is_invalid && $pending_is_valid_status ) {
// If the previous status is no longer valid, let's try to restore it to "pending" instead.
wc_get_logger()->warning(
sprintf(
/* translators: 1: order ID, 2: order status */
__( 'The previous status of order %1$d ("%2$s") is invalid. It has been restored to "pending" status instead.', 'woocommerce' ),
$id,
$previous_status
)
);
$previous_status = 'pending';
} elseif ( $previous_state_is_invalid ) {
// If we cannot restore to pending, we should probably stand back and let the merchant intervene some other way.
wc_get_logger()->warning(
sprintf(
/* translators: 1: order ID, 2: order status */
__( 'The previous status of order %1$d ("%2$s") is invalid. It could not be restored.', 'woocommerce' ),
$id,
$previous_status
)
);
return false;
}
/**
* Fires before an order is restored from the trash.
*
* @since 7.2.0
*
* @param int $order_id Order ID.
* @param string $previous_status The status of the order before it was trashed.
*/
do_action( 'woocommerce_untrash_order', $order->get_id(), $previous_status );
$order->set_status( $previous_status );
$order->save();
// Was the status successfully restored? Let's clean up the meta and indicate success...
if ( 'wc-' . $order->get_status() === $previous_status ) {
$order->delete_meta_data( '_wp_trash_meta_status' );
$order->delete_meta_data( '_wp_trash_meta_time' );
$order->delete_meta_data( '_wp_trash_meta_comments_status' );
$order->save_meta_data();
$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
if ( $data_synchronizer->data_sync_is_enabled() ) {
// The previous $order->save() will have forced a sync to the posts table,
// this implies that the post status is not "trash" anymore, and thus
// wp_untrash_post would do nothing.
wp_update_post(
array(
'ID' => $id,
'post_status' => 'trash',
)
);
wp_untrash_post( $id );
}
return true;
}
// ...Or log a warning and bail.
wc_get_logger()->warning(
sprintf(
/* translators: 1: order ID, 2: order status */
__( 'Something went wrong when trying to restore order %d from the trash. It could not be restored.', 'woocommerce' ),
$id
)
);
return false;
}
/**
* Deletes order data from custom order tables.
*
* @param int $order_id The order ID.
* @return void
*/
public function delete_order_data_from_custom_order_tables( $order_id ) {
global $wpdb;
// Delete COT-specific data.
foreach ( $this->get_all_table_names() as $table ) {
$wpdb->delete(
$table,
( self::get_orders_table_name() === $table )
? array( 'id' => $order_id )
: array( 'order_id' => $order_id ),
array( '%d' )
);
}
}
/**
* Method to create an order in the database.
*
* @param \WC_Order $order Order object.
*/
public function create( &$order ) {
if ( '' === $order->get_order_key() ) {
$order->set_order_key( wc_generate_order_key() );
}
$this->persist_save( $order );
/**
* Fires when a new order is created.
*
* @since 2.7.0
*
* @param int Order ID.
* @param \WC_Order Order object.
*/
do_action( 'woocommerce_new_order', $order->get_id(), $order );
}
/**
* Helper method responsible for persisting new data to order table.
*
* This should not contain and specific meta or actions, so that it can be used other order types safely.
*
* @param \WC_Order $order Order object.
* @param bool $force_all_fields Force update all fields, instead of calculating and updating only changed fields.
* @param bool $backfill Whether to backfill data to post datastore.
*
* @return void
*
* @throws \Exception When unable to save data.
*/
protected function persist_save( &$order, bool $force_all_fields = false, $backfill = true ) {
$order->set_version( Constants::get_constant( 'WC_VERSION' ) );
$order->set_currency( $order->get_currency() ? $order->get_currency() : get_woocommerce_currency() );
if ( ! $order->get_date_created( 'edit' ) ) {
$order->set_date_created( time() );
}
$this->update_order_meta( $order );
$this->persist_order_to_db( $order, $force_all_fields );
$order->save_meta_data();
$order->apply_changes();
if ( $backfill ) {
$this->maybe_backfill_post_record( $order );
}
$this->clear_caches( $order );
}
/**
* Method to update an order in the database.
*
* @param \WC_Order $order Order object.
*/
public function update( &$order ) {
// Before updating, ensure date paid is set if missing.
if (
! $order->get_date_paid( 'edit' )
&& version_compare( $order->get_version( 'edit' ), '3.0', '<' )
&& $order->has_status( apply_filters( 'woocommerce_payment_complete_order_status', $order->needs_processing() ? 'processing' : 'completed', $order->get_id(), $order ) ) // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
) {
$order->set_date_paid( $order->get_date_created( 'edit' ) );
}
if ( null === $order->get_date_created( 'edit' ) ) {
$order->set_date_created( time() );
}
$order->set_version( Constants::get_constant( 'WC_VERSION' ) );
// Fetch changes.
$changes = $order->get_changes();
$this->persist_updates( $order );
// Update download permissions if necessary.
if ( array_key_exists( 'billing_email', $changes ) || array_key_exists( 'customer_id', $changes ) ) {
$data_store = \WC_Data_Store::load( 'customer-download' );
$data_store->update_user_by_order_id( $order->get_id(), $order->get_customer_id(), $order->get_billing_email() );
}
// Mark user account as active.
if ( array_key_exists( 'customer_id', $changes ) ) {
wc_update_user_last_active( $order->get_customer_id() );
}
$order->apply_changes();
$this->clear_caches( $order );
do_action( 'woocommerce_update_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
}
/**
* Proxy to udpating order meta. Here for backward compatibility reasons.
*
* @param \WC_Order $order Order object.
*
* @return void
*/
protected function update_post_meta( &$order ) {
$this->update_order_meta( $order );
}
/**
* Helper method that is responsible for persisting order updates to the database.
*
* This is expected to be reused by other order types, and should not contain any specific metadata updates or actions.
*
* @param \WC_Order $order Order object.
* @param bool $backfill Whether to backfill data to post tables.
*
* @return array $changes Array of changes.
*
* @throws \Exception When unable to persist order.
*/
protected function persist_updates( &$order, $backfill = true ) {
// Fetch changes.
$changes = $order->get_changes();
if ( ! isset( $changes['date_modified'] ) ) {
$order->set_date_modified( time() );
}
if ( $backfill ) {
$this->maybe_backfill_post_record( $order );
}
$this->persist_order_to_db( $order );
$order->save_meta_data();
return $changes;
}
/**
* Helper function to decide whether to backfill post record.
*
* @param \WC_Abstract_Order $order Order object.
*
* @return void
*/
private function maybe_backfill_post_record( $order ) {
$data_sync = wc_get_container()->get( DataSynchronizer::class );
if ( $data_sync->data_sync_is_enabled() ) {
$this->backfill_post_record( $order );
}
}
/**
* Helper method that updates post meta based on an order object.
* Mostly used for backwards compatibility purposes in this datastore.
*
* @param \WC_Order $order Order object.
*
* @since 7.0.0
*/
public function update_order_meta( &$order ) {
$changes = $order->get_changes();
$this->update_address_index_meta( $order, $changes );
}
/**
* Helper function to update billing and shipping address metadata.
*
* @param \WC_Abstract_Order $order Order Object.
* @param array $changes Array of changes.
*
* @return void
*/
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 ) ) );
}
}
}
/**
* Return array of coupon_code => meta_key for coupon which have usage limit and have tentative keys.
* Pass $coupon_id if key for only one of the coupon is needed.
*
* @param WC_Order $order Order object.
* @param int $coupon_id If passed, will return held key for that coupon.
*
* @return array|string Key value pair for coupon code and meta key name. If $coupon_id is passed, returns meta_key for only that coupon.
*/
public function get_coupon_held_keys( $order, $coupon_id = null ) {
$held_keys = $order->get_meta( '_coupon_held_keys' );
if ( $coupon_id ) {
return isset( $held_keys[ $coupon_id ] ) ? $held_keys[ $coupon_id ] : null;
}
return $held_keys;
}
/**
* Return array of coupon_code => meta_key for coupon which have usage limit per customer and have tentative keys.
*
* @param WC_Order $order Order object.
* @param int $coupon_id If passed, will return held key for that coupon.
*
* @return mixed
*/
public function get_coupon_held_keys_for_users( $order, $coupon_id = null ) {
$held_keys_for_user = $order->get_meta( '_coupon_held_keys_for_users' );
if ( $coupon_id ) {
return isset( $held_keys_for_user[ $coupon_id ] ) ? $held_keys_for_user[ $coupon_id ] : null;
}
return $held_keys_for_user;
}
/**
* Add/Update list of meta keys that are currently being used by this order to hold a coupon.
* This is used to figure out what all meta entries we should delete when order is cancelled/completed.
*
* @param WC_Order $order Order object.
* @param array $held_keys Array of coupon_code => meta_key.
* @param array $held_keys_for_user Array of coupon_code => meta_key for held coupon for user.
*
* @return mixed
*/
public function set_coupon_held_keys( $order, $held_keys, $held_keys_for_user ) {
if ( is_array( $held_keys ) && 0 < count( $held_keys ) ) {
$order->update_meta_data( '_coupon_held_keys', $held_keys );
}
if ( is_array( $held_keys_for_user ) && 0 < count( $held_keys_for_user ) ) {
$order->update_meta_data( '_coupon_held_keys_for_users', $held_keys_for_user );
}
}
/**
* Release all coupons held by this order.
*
* @param WC_Order $order Current order object.
* @param bool $save Whether to delete keys from DB right away. Could be useful to pass `false` if you are building a bulk request.
*/
public function release_held_coupons( $order, $save = true ) {
$coupon_held_keys = $this->get_coupon_held_keys( $order );
if ( is_array( $coupon_held_keys ) ) {
foreach ( $coupon_held_keys as $coupon_id => $meta_key ) {
$coupon = new \WC_Coupon( $coupon_id );
$coupon->delete_meta_data( $meta_key );
$coupon->save_meta_data();
}
}
$order->delete_meta_data( '_coupon_held_keys' );
$coupon_held_keys_for_users = $this->get_coupon_held_keys_for_users( $order );
if ( is_array( $coupon_held_keys_for_users ) ) {
foreach ( $coupon_held_keys_for_users as $coupon_id => $meta_key ) {
$coupon = new \WC_Coupon( $coupon_id );
$coupon->delete_meta_data( $meta_key );
$coupon->save_meta_data();
}
}
$order->delete_meta_data( '_coupon_held_keys_for_users' );
if ( $save ) {
$order->save_meta_data();
}
}
/**
* Performs actual query to get orders. Uses `OrdersTableQuery` to build and generate the query.
*
* @param array $query_vars Query variables.
*
* @return array|object List of orders and count of orders.
*/
public function query( $query_vars ) {
if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) {
$query_vars['no_found_rows'] = true;
}
if ( isset( $query_vars['anonymized'] ) ) {
$query_vars['meta_query'] = $query_vars['meta_query'] ?? array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
if ( $query_vars['anonymized'] ) {
$query_vars['meta_query'][] = array(
'key' => '_anonymized',
'value' => 'yes',
);
} else {
$query_vars['meta_query'][] = array(
'key' => '_anonymized',
'compare' => 'NOT EXISTS',
);
}
}
try {
$query = new OrdersTableQuery( $query_vars );
} catch ( \Exception $e ) {
$query = (object) array(
'orders' => array(),
'found_orders' => 0,
'max_num_pages' => 0,
);
}
if ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) {
$orders = $query->orders;
} else {
$orders = WC()->order_factory->get_orders( $query->orders );
}
if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) {
return (object) array(
'orders' => $orders,
'total' => $query->found_orders,
'max_num_pages' => $query->max_num_pages,
);
}
return $orders;
}
//phpcs:enable Squiz.Commenting, Generic.Commenting
/**
* Get the SQL needed to create all the tables needed for the custom orders table feature.
*
* @return string
*/
public function get_database_schema() {
global $wpdb;
$collate = $wpdb->has_cap( 'collation' ) ? $wpdb->get_charset_collate() : '';
$orders_table_name = $this->get_orders_table_name();
$addresses_table_name = $this->get_addresses_table_name();
$operational_data_table_name = $this->get_operational_data_table_name();
$meta_table = $this->get_meta_table_name();
$sql = "
CREATE TABLE $orders_table_name (
id bigint(20) unsigned,
status varchar(20) null,
currency varchar(10) null,
type varchar(20) null,
tax_amount decimal(26,8) null,
total_amount decimal(26,8) null,
customer_id bigint(20) unsigned null,
billing_email varchar(320) null,
date_created_gmt datetime null,
date_updated_gmt datetime null,
parent_order_id bigint(20) unsigned null,
payment_method varchar(100) null,
payment_method_title text null,
transaction_id varchar(100) null,
ip_address varchar(100) null,
user_agent text null,
customer_note text null,
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 parent_order_id (parent_order_id),
KEY date_updated (date_updated_gmt)
) $collate;
CREATE TABLE $addresses_table_name (
id bigint(20) unsigned auto_increment primary key,
order_id bigint(20) unsigned NOT NULL,
address_type varchar(20) null,
first_name text null,
last_name text null,
company text null,
address_1 text null,
address_2 text null,
city text null,
state text null,
postcode text null,
country text null,
email varchar(320) null,
phone varchar(100) null,
KEY order_id (order_id),
UNIQUE KEY address_type_order_id (address_type, order_id),
KEY email (email),
KEY phone (phone)
) $collate;
CREATE TABLE $operational_data_table_name (
id bigint(20) unsigned auto_increment primary key,
order_id bigint(20) unsigned NULL,
created_via varchar(100) NULL,
woocommerce_version varchar(20) NULL,
prices_include_tax tinyint(1) NULL,
coupon_usages_are_counted tinyint(1) NULL,
download_permission_granted tinyint(1) NULL,
cart_hash varchar(100) NULL,
new_order_email_sent tinyint(1) NULL,
order_key varchar(100) NULL,
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,
recorded_sales tinyint(1) NULL,
UNIQUE KEY order_id (order_id),
UNIQUE KEY order_key (order_key)
) $collate;
CREATE TABLE $meta_table (
id bigint(20) unsigned auto_increment primary key,
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))
) $collate;
";
return $sql;
}
/**
* Returns an array of meta for an object.
*
* @param WC_Data $object WC_Data object.
* @return array
*/
public function read_meta( &$object ) {
$raw_meta_data = $this->data_store_meta->read_meta( $object );
return $this->filter_raw_meta_data( $object, $raw_meta_data );
}
/**
* Deletes meta based on meta ID.
*
* @param WC_Data $object WC_Data object.
* @param stdClass $meta (containing at least ->id).
*/
public function delete_meta( &$object, $meta ) {
return $this->data_store_meta->delete_meta( $object, $meta );
}
/**
* Add new piece of meta.
*
* @param WC_Data $object WC_Data object.
* @param stdClass $meta (containing ->key and ->value).
* @return int meta ID
*/
public function add_meta( &$object, $meta ) {
return $this->data_store_meta->add_meta( $object, $meta );
}
/**
* Update meta.
*
* @param WC_Data $object WC_Data object.
* @param stdClass $meta (containing ->id, ->key and ->value).
*/
public function update_meta( &$object, $meta ) {
return $this->data_store_meta->update_meta( $object, $meta );
}
}
PK ;[b= ! Orders/OrdersTableSearchQuery.phpnu [ query = $query;
$this->search_term = "'" . esc_sql( '%' . urldecode( $query->get( 's' ) ) . '%' ) . "'";
}
/**
* Supplies an array of clauses to be used in an order query.
*
* @internal
* @throws Exception If unable to generate either the JOIN or WHERE SQL fragments.
*
* @return array {
* @type string $join JOIN clause.
* @type string $where WHERE clause.
* }
*/
public function get_sql_clauses(): array {
return array(
'join' => array( $this->generate_join() ),
'where' => array( $this->generate_where() ),
);
}
/**
* Generates the necessary JOIN clauses for the order search to be performed.
*
* @throws Exception May be triggered if a table name cannot be determined.
*
* @return string
*/
private function generate_join(): string {
$orders_table = $this->query->get_table_name( 'orders' );
$meta_table = $this->query->get_table_name( 'meta' );
$items_table = $this->query->get_table_name( 'items' );
return "
LEFT JOIN $meta_table AS search_query_meta ON search_query_meta.order_id = $orders_table.id
LEFT JOIN $items_table AS search_query_items ON search_query_items.order_id = $orders_table.id
";
}
/**
* Generates the necessary WHERE clauses for the order search to be performed.
*
* @throws Exception May be triggered if a table name cannot be determined.
*
* @return string
*/
private function generate_where(): string {
$where = '';
$meta_fields = $this->get_meta_fields_to_be_searched();
$possible_order_id = (string) absint( $this->query->get( 's' ) );
// Support the passing of an order ID as the search term.
if ( (string) $this->query->get( 's' ) === $possible_order_id ) {
$where = $this->query->get_table_name( 'orders' ) . '.id = ' . $possible_order_id . ' OR ';
}
$where .= "
(
search_query_meta.meta_key IN ( $meta_fields )
AND search_query_meta.meta_value LIKE $this->search_term
)
OR search_query_items.order_item_name LIKE $this->search_term
";
return " ( $where ) ";
}
/**
* Returns the order meta field keys to be searched.
*
* These will be returned as a single string, where the meta keys have been escaped, quoted and are
* comma-separated (ie, "'abc', 'foo'" - ready for inclusion in a SQL IN() clause).
*
* @return string
*/
private function get_meta_fields_to_be_searched(): string {
/**
* Controls the order meta keys to be included in search queries.
*
* This hook is used when Custom Order Tables are in use: the corresponding hook when CPT-orders are in use
* is 'woocommerce_shop_order_search_fields'.
*
* @since 7.0.0
*
* @param array
*/
$meta_keys = apply_filters(
'woocommerce_order_table_search_query_meta_keys',
array(
'_billing_address_index',
'_shipping_address_index',
)
);
$meta_keys = (array) array_map(
function ( string $meta_key ): string {
return "'" . esc_sql( wc_clean( $meta_key ) ) . "'";
},
$meta_keys
);
return implode( ',', $meta_keys );
}
}
PK ;[el ! ! Orders/OrdersTableFieldQuery.phpnu [ ',
'>=',
'<',
'<=',
'BETWEEN',
'NOT BETWEEN',
);
/**
* The original query object.
*
* @var OrdersTableQuery
*/
private $query = null;
/**
* Determines whether the field query should produce no results due to an invalid argument.
*
* @var boolean
*/
private $force_no_results = false;
/**
* Holds a sanitized version of the `field_query`.
*
* @var array
*/
private $queries = array();
/**
* JOIN clauses to add to the main SQL query.
*
* @var array
*/
private $join = array();
/**
* WHERE clauses to add to the main SQL query.
*
* @var array
*/
private $where = array();
/**
* Table aliases in use by the field query. Used to keep track of JOINs and optimize when possible.
*
* @var array
*/
private $table_aliases = array();
/**
* Constructor.
*
* @param OrdersTableQuery $q The main query being performed.
*/
public function __construct( OrdersTableQuery $q ) {
$field_query = $q->get( 'field_query' );
if ( ! $field_query || ! is_array( $field_query ) ) {
return;
}
$this->query = $q;
$this->queries = $this->sanitize_query( $field_query );
$this->where = ( ! $this->force_no_results ) ? $this->process( $this->queries ) : '1=0';
}
/**
* Sanitizes the field_query argument.
*
* @param array $q A field_query array.
* @return array A sanitized field query array.
* @throws \Exception When field table info is missing.
*/
private function sanitize_query( array $q ) {
$sanitized = array();
foreach ( $q as $key => $arg ) {
if ( 'relation' === $key ) {
$relation = $arg;
} elseif ( ! is_array( $arg ) ) {
continue;
} elseif ( $this->is_atomic( $arg ) ) {
if ( isset( $arg['value'] ) && array() === $arg['value'] ) {
continue;
}
// Sanitize 'compare'.
$arg['compare'] = strtoupper( $arg['compare'] ?? '=' );
$arg['compare'] = in_array( $arg['compare'], self::VALID_COMPARISON_OPERATORS, true ) ? $arg['compare'] : '=';
if ( '=' === $arg['compare'] && isset( $arg['value'] ) && is_array( $arg['value'] ) ) {
$arg['compare'] = 'IN';
}
// Sanitize 'cast'.
$arg['cast'] = $this->sanitize_cast_type( $arg['type'] ?? '' );
$field_info = $this->query->get_field_mapping_info( $arg['field'] );
if ( ! $field_info ) {
$this->force_no_results = true;
continue;
}
$arg = array_merge( $arg, $field_info );
$sanitized[ $key ] = $arg;
} else {
$sanitized_arg = $this->sanitize_query( $arg );
if ( $sanitized_arg ) {
$sanitized[ $key ] = $sanitized_arg;
}
}
}
if ( $sanitized ) {
$sanitized['relation'] = 1 === count( $sanitized ) ? 'OR' : $this->sanitize_relation( $relation ?? 'AND' );
}
return $sanitized;
}
/**
* Makes sure we use an AND or OR relation. Defaults to AND.
*
* @param string $relation An unsanitized relation prop.
* @return string
*/
private function sanitize_relation( string $relation ): string {
if ( ! empty( $relation ) && 'OR' === strtoupper( $relation ) ) {
return 'OR';
}
return 'AND';
}
/**
* Processes field_query entries and generates the necessary table aliases, JOIN statements and WHERE conditions.
*
* @param array $q A field query.
* @return string An SQL WHERE statement.
*/
private function process( array $q ) {
$where = '';
if ( empty( $q ) ) {
return $where;
}
if ( $this->is_atomic( $q ) ) {
$q['alias'] = $this->find_or_create_table_alias_for_clause( $q );
$where = $this->generate_where_for_clause( $q );
} else {
$relation = $q['relation'];
unset( $q['relation'] );
foreach ( $q as $query ) {
$chunks[] = $this->process( $query );
}
if ( 1 === count( $chunks ) ) {
$where = $chunks[0];
} else {
$where = '(' . implode( " {$relation} ", $chunks ) . ')';
}
}
return $where;
}
/**
* Checks whether a given field_query clause is atomic or not (i.e. not nested).
*
* @param array $q The field_query clause.
* @return boolean TRUE if atomic, FALSE otherwise.
*/
private function is_atomic( $q ) {
return isset( $q['field'] );
}
/**
* Finds a common table alias that the field_query clause can use, or creates one.
*
* @param array $q An atomic field_query clause.
* @return string A table alias for use in an SQL JOIN clause.
* @throws \Exception When table info for clause is missing.
*/
private function find_or_create_table_alias_for_clause( $q ) {
global $wpdb;
if ( ! empty( $q['alias'] ) ) {
return $q['alias'];
}
if ( empty( $q['table'] ) || empty( $q['column'] ) ) {
throw new \Exception( __( 'Missing table info for query arg.', 'woocommerce' ) );
}
$join = '';
if ( isset( $q['mapping_id'] ) ) {
// Re-use JOINs and aliases from OrdersTableQuery for core tables.
$alias = $this->query->get_core_mapping_alias( $q['mapping_id'] );
$join = $this->query->get_core_mapping_join( $q['mapping_id'] );
} else {
$alias = $q['table'];
$join = '';
}
if ( in_array( $alias, $this->table_aliases, true ) ) {
return $alias;
}
$this->table_aliases[] = $alias;
if ( $join ) {
$this->join[] = $join;
}
return $alias;
}
/**
* Returns the correct type for a given clause 'type'.
*
* @param string $type MySQL type.
* @return string MySQL type.
*/
private function sanitize_cast_type( $type ) {
$clause_type = strtoupper( $type );
if ( ! $clause_type || ! preg_match( '/^(?:BINARY|CHAR|DATE|DATETIME|SIGNED|UNSIGNED|TIME|NUMERIC(?:\(\d+(?:,\s?\d+)?\))?|DECIMAL(?:\(\d+(?:,\s?\d+)?\))?)$/', $clause_type ) ) {
return 'CHAR';
}
if ( 'NUMERIC' === $clause_type ) {
$clause_type = 'SIGNED';
}
return $clause_type;
}
/**
* Generates an SQL WHERE clause for a given field_query atomic clause.
*
* @param array $clause An atomic field_query clause.
* @return string An SQL WHERE clause or an empty string if $clause is invalid.
*/
private function generate_where_for_clause( $clause ): string {
global $wpdb;
$clause_value = $clause['value'] ?? '';
if ( in_array( $clause['compare'], array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ), true ) ) {
if ( ! is_array( $clause_value ) ) {
$clause_value = preg_split( '/[,\s]+/', $clause_value );
}
} elseif ( is_string( $clause_value ) ) {
$clause_value = trim( $clause_value );
}
$clause_compare = $clause['compare'];
switch ( $clause_compare ) {
case 'IN':
case 'NOT IN':
$where = $wpdb->prepare( '(' . substr( str_repeat( ',%s', count( $clause_value ) ), 1 ) . ')', $clause_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
break;
case 'BETWEEN':
case 'NOT BETWEEN':
$where = $wpdb->prepare( '%s AND %s', $clause_value[0], $clause_value[1] ?? $clause_value[0] );
break;
case 'LIKE':
case 'NOT LIKE':
$where = $wpdb->prepare( '%s', '%' . $wpdb->esc_like( $clause_value ) . '%' );
break;
case 'EXISTS':
// EXISTS with a value is interpreted as '='.
if ( $clause_value ) {
$clause_compare = '=';
$where = $wpdb->prepare( '%s', $clause_value );
} else {
$clause_compare = 'IS NOT';
$where = 'NULL';
}
break;
case 'NOT EXISTS':
// 'value' is ignored for NOT EXISTS.
$clause_compare = 'IS';
$where = 'NULL';
break;
default:
$where = $wpdb->prepare( '%s', $clause_value );
break;
}
if ( $where ) {
if ( 'CHAR' === $clause['cast'] ) {
return "`{$clause['alias']}`.`{$clause['column']}` {$clause_compare} {$where}";
} else {
return "CAST(`{$clause['alias']}`.`{$clause['column']}` AS {$clause['cast']}) {$clause_compare} {$where}";
}
}
return '';
}
/**
* Returns JOIN and WHERE clauses to be appended to the main SQL query.
*
* @return array {
* @type string $join JOIN clause.
* @type string $where WHERE clause.
* }
*/
public function get_sql_clauses() {
return array(
'join' => $this->join,
'where' => $this->where ? array( $this->where ) : array(),
);
}
}
PK ;[buJ J Orders/OrdersTableMetaQuery.phpnu [ ',
'>=',
'<',
'<=',
'BETWEEN',
'NOT BETWEEN',
);
/**
* Prefix used when generating aliases for the metadata table.
*
* @var string
*/
private const ALIAS_PREFIX = 'meta';
/**
* Name of the main orders table.
*
* @var string
*/
private $meta_table = '';
/**
* Name of the metadata table.
*
* @var string
*/
private $orders_table = '';
/**
* Sanitized `meta_query`.
*
* @var array
*/
private $queries = array();
/**
* Flat list of clauses by name.
*
* @var array
*/
private $flattened_clauses = array();
/**
* JOIN clauses to add to the main SQL query.
*
* @var array
*/
private $join = array();
/**
* WHERE clauses to add to the main SQL query.
*
* @var array
*/
private $where = array();
/**
* Table aliases in use by the meta query. Used to optimize JOINs when possible.
*
* @var array
*/
private $table_aliases = array();
/**
* Constructor.
*
* @param OrdersTableQuery $q The main query being performed.
*/
public function __construct( OrdersTableQuery $q ) {
$meta_query = $q->get( 'meta_query' );
if ( ! $meta_query ) {
return;
}
$this->queries = $this->sanitize_meta_query( $meta_query );
$this->meta_table = $q->get_table_name( 'meta' );
$this->orders_table = $q->get_table_name( 'orders' );
$this->build_query();
}
/**
* Returns JOIN and WHERE clauses to be appended to the main SQL query.
*
* @return array {
* @type string $join JOIN clause.
* @type string $where WHERE clause.
* }
*/
public function get_sql_clauses(): array {
return array(
'join' => $this->sanitize_join( $this->join ),
'where' => $this->flatten_where_clauses( $this->where ),
);
}
/**
* Returns a list of names (corresponding to meta_query clauses) that can be used as an 'orderby' arg.
*
* @since 7.4
*
* @return array
*/
public function get_orderby_keys(): array {
if ( ! $this->flattened_clauses ) {
return array();
}
$keys = array();
$keys[] = 'meta_value';
$keys[] = 'meta_value_num';
$first_clause = reset( $this->flattened_clauses );
if ( $first_clause && ! empty( $first_clause['key'] ) ) {
$keys[] = $first_clause['key'];
}
$keys = array_merge(
$keys,
array_keys( $this->flattened_clauses )
);
return $keys;
}
/**
* Returns an SQL fragment for the given meta_query key that can be used in an ORDER BY clause.
* Call {@see 'get_orderby_keys'} to obtain a list of valid keys.
*
* @since 7.4
*
* @param string $key The key name.
* @return string
*
* @throws \Exception When an invalid key is passed.
*/
public function get_orderby_clause_for_key( string $key ): string {
$clause = false;
if ( isset( $this->flattened_clauses[ $key ] ) ) {
$clause = $this->flattened_clauses[ $key ];
} else {
$first_clause = reset( $this->flattened_clauses );
if ( $first_clause && ! empty( $first_clause['key'] ) ) {
if ( 'meta_value_num' === $key ) {
return "{$first_clause['alias']}.meta_value+0";
}
if ( 'meta_value' === $key || $first_clause['key'] === $key ) {
$clause = $first_clause;
}
}
}
if ( ! $clause ) {
// translators: %s is a meta_query key.
throw new \Exception( sprintf( __( 'Invalid meta_query clause key: %s.', 'woocommerce' ), $key ) );
}
return "CAST({$clause['alias']}.meta_value AS {$clause['cast']})";
}
/**
* Checks whether a given meta_query clause is atomic or not (i.e. not nested).
*
* @param array $arg The meta_query clause.
* @return boolean TRUE if atomic, FALSE otherwise.
*/
private function is_atomic( array $arg ): bool {
return isset( $arg['key'] ) || isset( $arg['value'] );
}
/**
* Sanitizes the meta_query argument.
*
* @param array $q A meta_query array.
* @return array A sanitized meta query array.
*/
private function sanitize_meta_query( array $q ): array {
$sanitized = array();
foreach ( $q as $key => $arg ) {
if ( 'relation' === $key ) {
$relation = $arg;
} elseif ( ! is_array( $arg ) ) {
continue;
} elseif ( $this->is_atomic( $arg ) ) {
if ( isset( $arg['value'] ) && array() === $arg['value'] ) {
unset( $arg['value'] );
}
$arg['compare'] = isset( $arg['compare'] ) ? strtoupper( $arg['compare'] ) : ( isset( $arg['value'] ) && is_array( $arg['value'] ) ? 'IN' : '=' );
$arg['compare_key'] = isset( $arg['compare_key'] ) ? strtoupper( $arg['compare_key'] ) : ( isset( $arg['key'] ) && is_array( $arg['key'] ) ? 'IN' : '=' );
if ( ! in_array( $arg['compare'], self::NON_NUMERIC_OPERATORS, true ) && ! in_array( $arg['compare'], self::NUMERIC_OPERATORS, true ) ) {
$arg['compare'] = '=';
}
if ( ! in_array( $arg['compare_key'], self::NON_NUMERIC_OPERATORS, true ) ) {
$arg['compare_key'] = '=';
}
$sanitized[ $key ] = $arg;
$sanitized[ $key ]['index'] = $key;
} else {
$sanitized_arg = $this->sanitize_meta_query( $arg );
if ( $sanitized_arg ) {
$sanitized[ $key ] = $sanitized_arg;
}
}
}
if ( $sanitized ) {
$sanitized['relation'] = 1 === count( $sanitized ) ? 'OR' : $this->sanitize_relation( $relation ?? 'AND' );
}
return $sanitized;
}
/**
* Makes sure we use an AND or OR relation. Defaults to AND.
*
* @param string $relation An unsanitized relation prop.
* @return string
*/
private function sanitize_relation( string $relation ): string {
if ( ! empty( $relation ) && 'OR' === strtoupper( $relation ) ) {
return 'OR';
}
return 'AND';
}
/**
* Returns the correct type for a given meta type.
*
* @param string $type MySQL type.
* @return string MySQL type.
*/
private function sanitize_cast_type( string $type = '' ): string {
$meta_type = strtoupper( $type );
if ( ! $meta_type || ! preg_match( '/^(?:BINARY|CHAR|DATE|DATETIME|SIGNED|UNSIGNED|TIME|NUMERIC(?:\(\d+(?:,\s?\d+)?\))?|DECIMAL(?:\(\d+(?:,\s?\d+)?\))?)$/', $meta_type ) ) {
return 'CHAR';
}
if ( 'NUMERIC' === $meta_type ) {
$meta_type = 'SIGNED';
}
return $meta_type;
}
/**
* Makes sure a JOIN array does not have duplicates.
*
* @param array $join A JOIN array.
* @return array A sanitized JOIN array.
*/
private function sanitize_join( array $join ): array {
return array_filter( array_unique( array_map( 'trim', $join ) ) );
}
/**
* Flattens a nested WHERE array.
*
* @param array $where A possibly nested WHERE array with AND/OR operators.
* @return string An SQL WHERE clause.
*/
private function flatten_where_clauses( $where ): string {
if ( is_string( $where ) ) {
return trim( $where );
}
$chunks = array();
$operator = $this->sanitize_relation( $where['operator'] ?? '' );
foreach ( $where as $key => $w ) {
if ( 'operator' === $key ) {
continue;
}
$flattened = $this->flatten_where_clauses( $w );
if ( $flattened ) {
$chunks[] = $flattened;
}
}
if ( $chunks ) {
return '(' . implode( " {$operator} ", $chunks ) . ')';
} else {
return '';
}
}
/**
* Builds all the required internal bits for this meta query.
*
* @return void
*/
private function build_query(): void {
if ( ! $this->queries ) {
return;
}
$queries = $this->queries;
$sql_where = $this->process( $queries );
$this->where = $sql_where;
}
/**
* Processes meta_query entries and generates the necessary table aliases, JOIN statements and WHERE conditions.
*
* @param array $arg A meta query.
* @param null|array $parent The parent of the element being processed.
* @return array A nested array of WHERE conditions.
*/
private function process( array &$arg, &$parent = null ): array {
$where = array();
if ( $this->is_atomic( $arg ) ) {
$arg['alias'] = $this->find_or_create_table_alias_for_clause( $arg, $parent );
$arg['cast'] = $this->sanitize_cast_type( $arg['type'] ?? '' );
$where = array_filter(
array(
$this->generate_where_for_clause_key( $arg ),
$this->generate_where_for_clause_value( $arg ),
)
);
// Store clauses by their key for ORDER BY purposes.
$flat_clause_key = is_int( $arg['index'] ) ? $arg['alias'] : $arg['index'];
$unique_flat_key = $flat_clause_key;
$i = 1;
while ( isset( $this->flattened_clauses[ $unique_flat_key ] ) ) {
$unique_flat_key = $flat_clause_key . '-' . $i;
$i++;
}
$this->flattened_clauses[ $unique_flat_key ] =& $arg;
} else {
// Nested.
$relation = $arg['relation'];
unset( $arg['relation'] );
foreach ( $arg as $index => &$clause ) {
$chunks[] = $this->process( $clause, $arg );
}
// Merge chunks of the form OR(m) with the surrounding clause.
if ( 1 === count( $chunks ) ) {
$where = $chunks[0];
} else {
$where = array_merge(
array(
'operator' => $relation,
),
$chunks
);
}
}
return $where;
}
/**
* Generates a JOIN clause to handle an atomic meta_query clause.
*
* @param array $clause An atomic meta_query clause.
* @param string $alias Metadata table alias to use.
* @return string An SQL JOIN clause.
*/
private function generate_join_for_clause( array $clause, string $alias ): string {
global $wpdb;
if ( 'NOT EXISTS' === $clause['compare'] ) {
if ( 'LIKE' === $clause['compare_key'] ) {
return $wpdb->prepare(
"LEFT JOIN {$this->meta_table} AS {$alias} ON ( {$this->orders_table}.id = {$alias}.order_id AND {$alias}.meta_key LIKE %s )", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
'%' . $wpdb->esc_like( $clause['key'] ) . '%'
);
} else {
return $wpdb->prepare(
"LEFT JOIN {$this->meta_table} AS {$alias} ON ( {$this->orders_table}.id = {$alias}.order_id AND {$alias}.meta_key = %s )", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$clause['key']
);
}
}
return "INNER JOIN {$this->meta_table} AS {$alias} ON ( {$this->orders_table}.id = {$alias}.order_id )";
}
/**
* Finds a common table alias that the meta_query clause can use, or creates one.
*
* @param array $clause An atomic meta_query clause.
* @param array $parent_query The parent query this clause is in.
* @return string A table alias for use in an SQL JOIN clause.
*/
private function find_or_create_table_alias_for_clause( array $clause, array $parent_query ): string {
if ( ! empty( $clause['alias'] ) ) {
return $clause['alias'];
}
$alias = false;
$siblings = array_filter(
$parent_query,
array( __CLASS__, 'is_atomic' )
);
foreach ( $siblings as $sibling ) {
if ( empty( $sibling['alias'] ) ) {
continue;
}
if ( $this->is_operator_compatible_with_shared_join( $clause, $sibling, $parent_query['relation'] ?? 'AND' ) ) {
$alias = $sibling['alias'];
break;
}
}
if ( ! $alias ) {
$alias = self::ALIAS_PREFIX . count( $this->table_aliases );
$this->join[] = $this->generate_join_for_clause( $clause, $alias );
$this->table_aliases[] = $alias;
}
return $alias;
}
/**
* Checks whether two meta_query clauses can share a JOIN.
*
* @param array $clause An atomic meta_query clause.
* @param array $sibling An atomic meta_query clause.
* @param string $relation The relation involving both clauses.
* @return boolean TRUE if the clauses can share a table alias, FALSE otherwise.
*/
private function is_operator_compatible_with_shared_join( array $clause, array $sibling, string $relation = 'AND' ): bool {
if ( ! $this->is_atomic( $clause ) || ! $this->is_atomic( $sibling ) ) {
return false;
}
$valid_operators = array();
if ( 'OR' === $relation ) {
$valid_operators = array( '=', 'IN', 'BETWEEN', 'LIKE', 'REGEXP', 'RLIKE', '>', '>=', '<', '<=' );
} elseif ( isset( $sibling['key'] ) && isset( $clause['key'] ) && $sibling['key'] === $clause['key'] ) {
$valid_operators = array( '!=', 'NOT IN', 'NOT LIKE' );
}
return in_array( strtoupper( $clause['compare'] ), $valid_operators, true ) && in_array( strtoupper( $sibling['compare'] ), $valid_operators, true );
}
/**
* Generates an SQL WHERE clause for a given meta_query atomic clause based on its meta key.
* Adapted from WordPress' `WP_Meta_Query::get_sql_for_clause()` method.
*
* @param array $clause An atomic meta_query clause.
* @return string An SQL WHERE clause or an empty string if $clause is invalid.
*/
private function generate_where_for_clause_key( array $clause ): string {
global $wpdb;
if ( ! array_key_exists( 'key', $clause ) ) {
return '';
}
if ( 'NOT EXISTS' === $clause['compare'] ) {
return "{$clause['alias']}.order_id IS NULL";
}
$alias = $clause['alias'];
if ( in_array( $clause['compare_key'], array( '!=', 'NOT IN', 'NOT LIKE', 'NOT EXISTS', 'NOT REGEXP' ), true ) ) {
$i = count( $this->table_aliases );
$subquery_alias = self::ALIAS_PREFIX . $i;
$this->table_aliases[] = $subquery_alias;
$meta_compare_string_start = 'NOT EXISTS (';
$meta_compare_string_start .= "SELECT 1 FROM {$this->meta_table} {$subquery_alias} ";
$meta_compare_string_start .= "WHERE {$subquery_alias}.order_id = {$alias}.order_id ";
$meta_compare_string_end = 'LIMIT 1';
$meta_compare_string_end .= ')';
}
switch ( $clause['compare_key'] ) {
case '=':
case 'EXISTS':
$where = $wpdb->prepare( "$alias.meta_key = %s", trim( $clause['key'] ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
break;
case 'LIKE':
$meta_compare_value = '%' . $wpdb->esc_like( trim( $clause['key'] ) ) . '%';
$where = $wpdb->prepare( "$alias.meta_key LIKE %s", $meta_compare_value ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
break;
case 'IN':
$meta_compare_string = "$alias.meta_key IN (" . substr( str_repeat( ',%s', count( $clause['key'] ) ), 1 ) . ')';
$where = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
break;
case 'RLIKE':
case 'REGEXP':
$operator = $clause['compare_key'];
if ( isset( $clause['type_key'] ) && 'BINARY' === strtoupper( $clause['type_key'] ) ) {
$cast = 'BINARY';
} else {
$cast = '';
}
$where = $wpdb->prepare( "$alias.meta_key $operator $cast %s", trim( $clause['key'] ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
break;
case '!=':
case 'NOT EXISTS':
$meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key = %s " . $meta_compare_string_end;
$where = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
break;
case 'NOT LIKE':
$meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key LIKE %s " . $meta_compare_string_end;
$meta_compare_value = '%' . $wpdb->esc_like( trim( $clause['key'] ) ) . '%';
$where = $wpdb->prepare( $meta_compare_string, $meta_compare_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
break;
case 'NOT IN':
$array_subclause = '(' . substr( str_repeat( ',%s', count( $clause['key'] ) ), 1 ) . ') ';
$meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key IN " . $array_subclause . $meta_compare_string_end;
$where = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
break;
case 'NOT REGEXP':
$operator = $clause['compare_key'];
if ( isset( $clause['type_key'] ) && 'BINARY' === strtoupper( $clause['type_key'] ) ) {
$cast = 'BINARY';
} else {
$cast = '';
}
$meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key REGEXP $cast %s " . $meta_compare_string_end;
$where = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
break;
default:
$where = '';
break;
}
return $where;
}
/**
* Generates an SQL WHERE clause for a given meta_query atomic clause based on its meta value.
* Adapted from WordPress' `WP_Meta_Query::get_sql_for_clause()` method.
*
* @param array $clause An atomic meta_query clause.
* @return string An SQL WHERE clause or an empty string if $clause is invalid.
*/
private function generate_where_for_clause_value( $clause ): string {
global $wpdb;
if ( ! array_key_exists( 'value', $clause ) ) {
return '';
}
$meta_value = $clause['value'];
if ( in_array( $clause['compare'], array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ), true ) ) {
if ( ! is_array( $meta_value ) ) {
$meta_value = preg_split( '/[,\s]+/', $meta_value );
}
} elseif ( is_string( $meta_value ) ) {
$meta_value = trim( $meta_value );
}
$meta_compare = $clause['compare'];
switch ( $meta_compare ) {
case 'IN':
case 'NOT IN':
$where = $wpdb->prepare( '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')', $meta_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
break;
case 'BETWEEN':
case 'NOT BETWEEN':
$where = $wpdb->prepare( '%s AND %s', $meta_value[0], $meta_value[1] );
break;
case 'LIKE':
case 'NOT LIKE':
$where = $wpdb->prepare( '%s', '%' . $wpdb->esc_like( $meta_value ) . '%' );
break;
// EXISTS with a value is interpreted as '='.
case 'EXISTS':
$meta_compare = '=';
$where = $wpdb->prepare( '%s', $meta_value );
break;
// 'value' is ignored for NOT EXISTS.
case 'NOT EXISTS':
$where = '';
break;
default:
$where = $wpdb->prepare( '%s', $meta_value );
break;
}
if ( $where ) {
if ( 'CHAR' === $clause['cast'] ) {
return "{$clause['alias']}.meta_value {$meta_compare} {$where}";
} else {
return "CAST({$clause['alias']}.meta_value AS {$clause['cast']}) {$meta_compare} {$where}";
}
}
}
}
PK ;[mԋ ԋ Orders/OrdersTableQuery.phpnu [ ]*)(>=|<=|>|<|\.\.\.)([^.<>]+)/';
/**
* Highest possible unsigned bigint value (unsigned bigints being the type of the `id` column).
*
* This is deliberately held as a string, rather than a numeric type, for inclusion within queries.
*/
private const MYSQL_MAX_UNSIGNED_BIGINT = '18446744073709551615';
/**
* Names of all COT tables (orders, addresses, operational_data, meta) in the form 'table_id' => 'table name'.
*
* @var array
*/
private $tables = array();
/**
* Column mappings for all COT tables.
*
* @var array
*/
private $mappings = array();
/**
* Query vars after processing and sanitization.
*
* @var array
*/
private $args = array();
/**
* Columns to be selected in the SELECT clause.
*
* @var array
*/
private $fields = array();
/**
* Array of table aliases and conditions used to compute the JOIN clause of the query.
*
* @var array
*/
private $join = array();
/**
* Array of fields and conditions used to compute the WHERE clause of the query.
*
* @var array
*/
private $where = array();
/**
* Field to be used in the GROUP BY clause of the query.
*
* @var array
*/
private $groupby = array();
/**
* Array of fields used to compute the ORDER BY clause of the query.
*
* @var array
*/
private $orderby = array();
/**
* Limits used to compute the LIMIT clause of the query.
*
* @var array
*/
private $limits = array();
/**
* Results (order IDs) for the current query.
*
* @var array
*/
private $results = array();
/**
* Final SQL query to run after processing of args.
*
* @var string
*/
private $sql = '';
/**
* Final SQL query to count results after processing of args.
*
* @var string
*/
private $count_sql = '';
/**
* The number of pages (when pagination is enabled).
*
* @var int
*/
private $max_num_pages = 0;
/**
* The number of orders found.
*
* @var int
*/
private $found_orders = 0;
/**
* Field query parser.
*
* @var OrdersTableFieldQuery
*/
private $field_query = null;
/**
* Meta query parser.
*
* @var OrdersTableMetaQuery
*/
private $meta_query = null;
/**
* Search query parser.
*
* @var OrdersTableSearchQuery?
*/
private $search_query = null;
/**
* Date query parser.
*
* @var WP_Date_Query
*/
private $date_query = null;
/**
* Sets up and runs the query after processing arguments.
*
* @param array $args Array of query vars.
*/
public function __construct( $args = array() ) {
global $wpdb;
$datastore = wc_get_container()->get( OrdersTableDataStore::class );
// TODO: maybe OrdersTableDataStore::get_all_table_names() could return these keys/indices instead.
$this->tables = array(
'orders' => $datastore::get_orders_table_name(),
'addresses' => $datastore::get_addresses_table_name(),
'operational_data' => $datastore::get_operational_data_table_name(),
'meta' => $datastore::get_meta_table_name(),
'items' => $wpdb->prefix . 'woocommerce_order_items',
);
$this->mappings = $datastore->get_all_order_column_mappings();
$this->args = $args;
// TODO: args to be implemented.
unset( $this->args['customer_note'], $this->args['name'] );
$this->build_query();
$this->run_query();
}
/**
* Remaps some legacy and `WP_Query` specific query vars to vars available in the customer order table scheme.
*
* @return void
*/
private function maybe_remap_args(): void {
$mapping = array(
// WP_Query legacy.
'post_date' => 'date_created_gmt',
'post_date_gmt' => 'date_created_gmt',
'post_modified' => 'date_modified_gmt',
'post_modified_gmt' => 'date_updated_gmt',
'post_status' => 'status',
'_date_completed' => 'date_completed_gmt',
'_date_paid' => 'date_paid_gmt',
'paged' => 'page',
'post_parent' => 'parent_order_id',
'post_parent__in' => 'parent_order_id',
'post_parent__not_in' => 'parent_exclude',
'post__not_in' => 'exclude',
'posts_per_page' => 'limit',
'p' => 'id',
'post__in' => 'id',
'post_type' => 'type',
'fields' => 'return',
'customer_user' => 'customer_id',
'order_currency' => 'currency',
'order_version' => 'woocommerce_version',
'cart_discount' => 'discount_total_amount',
'cart_discount_tax' => 'discount_tax_amount',
'order_shipping' => 'shipping_total_amount',
'order_shipping_tax' => 'shipping_tax_amount',
'order_tax' => 'tax_amount',
// Translate from WC_Order_Query to table structure.
'version' => 'woocommerce_version',
'date_created' => 'date_created_gmt',
'date_modified' => 'date_updated_gmt',
'date_modified_gmt' => 'date_updated_gmt',
'date_completed' => 'date_completed_gmt',
'date_completed_gmt' => 'date_completed_gmt',
'date_paid' => 'date_paid_gmt',
'discount_total' => 'discount_total_amount',
'discount_tax' => 'discount_tax_amount',
'shipping_total' => 'shipping_total_amount',
'shipping_tax' => 'shipping_tax_amount',
'cart_tax' => 'tax_amount',
'total' => 'total_amount',
'customer_ip_address' => 'ip_address',
'customer_user_agent' => 'user_agent',
'parent' => 'parent_order_id',
);
foreach ( $mapping as $query_key => $table_field ) {
if ( isset( $this->args[ $query_key ] ) && '' !== $this->args[ $query_key ] ) {
$this->args[ $table_field ] = $this->args[ $query_key ];
unset( $this->args[ $query_key ] );
}
}
// meta_query.
$this->args['meta_query'] = ( $this->arg_isset( 'meta_query' ) && is_array( $this->args['meta_query'] ) ) ? $this->args['meta_query'] : array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
$shortcut_meta_query = array();
foreach ( array( 'key', 'value', 'compare', 'type', 'compare_key', 'type_key' ) as $key ) {
if ( $this->arg_isset( "meta_{$key}" ) ) {
$shortcut_meta_query[ $key ] = $this->args[ "meta_{$key}" ];
}
}
if ( ! empty( $shortcut_meta_query ) ) {
if ( ! empty( $this->args['meta_query'] ) ) {
$this->args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'relation' => 'AND',
$shortcut_meta_query,
$this->args['meta_query'],
);
} else {
$this->args['meta_query'] = array( $shortcut_meta_query ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
}
}
}
/**
* Generates a `WP_Date_Query` compatible query from a given date.
* 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.
* @return array An array with keys 'year', 'month', 'day' and possibly 'hour', 'minute' and 'second'.
*/
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( 'UTC' ) );
} elseif ( ! is_a( $date, 'WC_DateTime' ) ) {
// YYYY-MM-DD queries have 'day' precision for backwards compat.
$date = wc_string_to_datetime( $date );
$precision = 'day';
}
$result['year'] = $date->date( 'Y' );
$result['month'] = $date->date( 'm' );
$result['day'] = $date->date( 'd' );
if ( 'second' === $precision ) {
$result['hour'] = $date->date( 'H' );
$result['minute'] = $date->date( 'i' );
$result['second'] = $date->date( 's' );
}
return $result;
}
/**
* Processes date-related query args and merges the result into 'date_query'.
*
* @return void
* @throws \Exception When date args are invalid.
*/
private function process_date_args(): void {
$valid_operators = array( '>', '>=', '=', '<=', '<', '...' );
$date_queries = array();
$gmt_date_keys = array(
'date_created_gmt',
'date_updated_gmt',
'date_paid_gmt',
'date_completed_gmt',
);
foreach ( array_filter( $gmt_date_keys, array( $this, 'arg_isset' ) ) as $date_key ) {
$date_value = $this->args[ $date_key ];
$operator = '=';
$dates = array();
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] );
}
$dates[] = $this->date_to_date_query_arg( $matches[3] );
} else {
$dates[] = $this->date_to_date_query_arg( $date_value );
}
if ( empty( $dates ) || ! $operator || ( '...' === $operator && count( $dates ) < 2 ) ) {
throw new \Exception( 'Invalid date_query' );
}
$operator_to_keys = array();
if ( in_array( $operator, array( '>', '>=', '...' ), true ) ) {
$operator_to_keys[] = 'after';
}
if ( in_array( $operator, array( '<', '<=', '...' ), true ) ) {
$operator_to_keys[] = 'before';
}
$date_queries[] = array_merge(
array(
'column' => $date_key,
'inclusive' => ! in_array( $operator, array( '<', '>' ), true ),
),
'=' === $operator
? end( $dates )
: array_combine( $operator_to_keys, $dates )
);
}
// Add top-level date parameters to the date_query.
$tl_query = array();
foreach ( array( 'hour', 'minute', 'second', 'year', 'monthnum', 'week', 'day', 'year' ) as $tl_key ) {
if ( $this->arg_isset( $tl_key ) ) {
$tl_query[ $tl_key ] = $this->args[ $tl_key ];
unset( $this->args[ $tl_key ] );
}
}
if ( $tl_query ) {
$tl_query['column'] = 'date_created_gmt';
$date_queries[] = $tl_query;
}
if ( $date_queries ) {
if ( ! $this->arg_isset( 'date_query' ) ) {
$this->args['date_query'] = array();
}
$this->args['date_query'] = array_merge(
array( 'relation' => 'AND' ),
$date_queries,
$this->args['date_query']
);
}
$this->process_date_query_columns();
}
/**
* Makes sure all 'date_query' columns are correctly prefixed and their respective tables are being JOIN'ed.
*
* @return void
*/
private function process_date_query_columns() {
global $wpdb;
$legacy_columns = array(
'post_date' => 'date_created_gmt',
'post_date_gmt' => 'date_created_gmt',
'post_modified' => 'date_modified_gmt',
'post_modified_gmt' => 'date_updated_gmt',
);
$table_mapping = array(
'date_created_gmt' => $this->tables['orders'],
'date_updated_gmt' => $this->tables['orders'],
'date_paid_gmt' => $this->tables['operational_data'],
'date_completed_gmt' => $this->tables['operational_data'],
);
if ( empty( $this->args['date_query'] ) ) {
return;
}
array_walk_recursive(
$this->args['date_query'],
function( &$value, $key ) use ( $legacy_columns, $table_mapping, $wpdb ) {
if ( 'column' !== $key ) {
return;
}
// Translate legacy columns from wp_posts if necessary.
$value =
( isset( $legacy_columns[ $value ] ) || isset( $legacy_columns[ "{$wpdb->posts}.{$value}" ] ) )
? $legacy_columns[ $value ]
: $value;
$table = $table_mapping[ $value ] ?? null;
if ( ! $table ) {
return;
}
$value = "{$table}.{$value}";
if ( $table !== $this->tables['orders'] ) {
$this->join( $table, '', '', 'inner', true );
}
}
);
}
/**
* Sanitizes the 'status' query var.
*
* @return void
*/
private function sanitize_status(): void {
// Sanitize status.
$valid_statuses = array_keys( wc_get_order_statuses() );
if ( empty( $this->args['status'] ) || 'any' === $this->args['status'] ) {
$this->args['status'] = $valid_statuses;
} elseif ( 'all' === $this->args['status'] ) {
$this->args['status'] = array();
} else {
$this->args['status'] = is_array( $this->args['status'] ) ? $this->args['status'] : array( $this->args['status'] );
foreach ( $this->args['status'] as &$status ) {
$status = in_array( 'wc-' . $status, $valid_statuses, true ) ? 'wc-' . $status : $status;
}
$this->args['status'] = array_unique( array_filter( $this->args['status'] ) );
}
}
/**
* Parses and sanitizes the 'orderby' query var.
*
* @return void
*/
private function sanitize_order_orderby(): void {
// Allowed keys.
// TODO: rand, meta keys, etc.
$allowed_keys = array( 'ID', 'id', 'type', 'date', 'modified', 'parent' );
// Translate $orderby to a valid field.
$mapping = array(
'ID' => "{$this->tables['orders']}.id",
'id' => "{$this->tables['orders']}.id",
'type' => "{$this->tables['orders']}.type",
'date' => "{$this->tables['orders']}.date_created_gmt",
'date_created' => "{$this->tables['orders']}.date_created_gmt",
'modified' => "{$this->tables['orders']}.date_updated_gmt",
'date_modified' => "{$this->tables['orders']}.date_updated_gmt",
'parent' => "{$this->tables['orders']}.parent_order_id",
'total' => "{$this->tables['orders']}.total_amount",
'order_total' => "{$this->tables['orders']}.total_amount",
);
$order = $this->args['order'] ?? '';
$orderby = $this->args['orderby'] ?? '';
if ( 'none' === $orderby ) {
return;
}
// No need to sanitize, will be processed in calling function.
if ( 'include' === $orderby || 'post__in' === $orderby ) {
return;
}
if ( is_string( $orderby ) ) {
$orderby_fields = array_map( 'trim', explode( ' ', $orderby ) );
$orderby = array();
foreach ( $orderby_fields as $field ) {
$orderby[ $field ] = $order;
}
}
$allowed_orderby = array_merge(
array_keys( $mapping ),
array_values( $mapping ),
$this->meta_query ? $this->meta_query->get_orderby_keys() : array()
);
$this->args['orderby'] = array();
foreach ( $orderby as $order_key => $order ) {
if ( ! in_array( $order_key, $allowed_orderby, true ) ) {
continue;
}
if ( isset( $mapping[ $order_key ] ) ) {
$order_key = $mapping[ $order_key ];
}
$this->args['orderby'][ $order_key ] = $this->sanitize_order( $order );
}
}
/**
* Makes sure the order in an ORDER BY statement is either 'ASC' o 'DESC'.
*
* @param string $order The unsanitized order.
* @return string The sanitized order.
*/
private function sanitize_order( string $order ): string {
$order = strtoupper( $order );
return in_array( $order, array( 'ASC', 'DESC' ), true ) ? $order : 'DESC';
}
/**
* Builds the final SQL query to be run.
*
* @return void
*/
private function build_query(): void {
$this->maybe_remap_args();
// Field queries.
if ( ! empty( $this->args['field_query'] ) ) {
$this->field_query = new OrdersTableFieldQuery( $this );
$sql = $this->field_query->get_sql_clauses();
$this->join = $sql['join'] ? array_merge( $this->join, $sql['join'] ) : $this->join;
$this->where = $sql['where'] ? array_merge( $this->where, $sql['where'] ) : $this->where;
}
// Build query.
$this->process_date_args();
$this->process_orders_table_query_args();
$this->process_operational_data_table_query_args();
$this->process_addresses_table_query_args();
// Search queries.
if ( ! empty( $this->args['s'] ) ) {
$this->search_query = new OrdersTableSearchQuery( $this );
$sql = $this->search_query->get_sql_clauses();
$this->join = $sql['join'] ? array_merge( $this->join, $sql['join'] ) : $this->join;
$this->where = $sql['where'] ? array_merge( $this->where, $sql['where'] ) : $this->where;
}
// Meta queries.
if ( ! empty( $this->args['meta_query'] ) ) {
$this->meta_query = new OrdersTableMetaQuery( $this );
$sql = $this->meta_query->get_sql_clauses();
$this->join = $sql['join'] ? array_merge( $this->join, $sql['join'] ) : $this->join;
$this->where = $sql['where'] ? array_merge( $this->where, array( $sql['where'] ) ) : $this->where;
}
// Date queries.
if ( ! empty( $this->args['date_query'] ) ) {
$this->date_query = new \WP_Date_Query( $this->args['date_query'], "{$this->tables['orders']}.date_created_gmt" );
$this->where[] = substr( trim( $this->date_query->get_sql() ), 3 ); // WP_Date_Query includes "AND".
}
$this->process_orderby();
$this->process_limit();
$orders_table = $this->tables['orders'];
// Group by is a faster substitute for DISTINCT, as long as we are only selecting IDs. MySQL don't like it when we join tables and use DISTINCT.
$this->groupby[] = "{$this->tables['orders']}.id";
$this->fields = "{$orders_table}.id";
$fields = $this->fields;
// JOIN.
$join = implode( ' ', array_unique( array_filter( array_map( 'trim', $this->join ) ) ) );
// WHERE.
$where = '1=1';
foreach ( $this->where as $_where ) {
$where .= " AND ({$_where})";
}
// ORDER BY.
$orderby = $this->orderby ? ( 'ORDER BY ' . implode( ', ', $this->orderby ) ) : '';
// LIMITS.
$limits = '';
if ( ! empty( $this->limits ) && count( $this->limits ) === 2 ) {
list( $offset, $row_count ) = $this->limits;
$row_count = -1 === $row_count ? self::MYSQL_MAX_UNSIGNED_BIGINT : (int) $row_count;
$limits = 'LIMIT ' . (int) $offset . ', ' . $row_count;
}
// GROUP BY.
$groupby = $this->groupby ? 'GROUP BY ' . implode( ', ', (array) $this->groupby ) : '';
$this->sql = "SELECT $fields FROM $orders_table $join WHERE $where $groupby $orderby $limits";
$this->build_count_query( $fields, $join, $where, $groupby );
}
/**
* Build SQL query for counting total number of results.
*
* @param string $fields Prepared fields for SELECT clause.
* @param string $join Prepared JOIN clause.
* @param string $where Prepared WHERE clause.
* @param string $groupby Prepared GROUP BY clause.
*/
private function build_count_query( $fields, $join, $where, $groupby ) {
if ( ! isset( $this->sql ) || '' === $this->sql ) {
wc_doing_it_wrong( __FUNCTION__, 'Count query can only be build after main query is built.', '7.3.0' );
}
$orders_table = $this->tables['orders'];
$this->count_sql = "SELECT COUNT(DISTINCT $fields) FROM $orders_table $join WHERE $where";
}
/**
* Returns the table alias for a given table mapping.
*
* @param string $mapping_id The mapping name (e.g. 'orders' or 'operational_data').
* @return string Table alias.
*
* @since 7.0.0
*/
public function get_core_mapping_alias( string $mapping_id ): string {
return in_array( $mapping_id, array( 'billing_address', 'shipping_address' ), true )
? $mapping_id
: $this->tables[ $mapping_id ];
}
/**
* Returns an SQL JOIN clause that can be used to join the main orders table with another order table.
*
* @param string $mapping_id The mapping name (e.g. 'orders' or 'operational_data').
* @return string The JOIN clause.
*
* @since 7.0.0
*/
public function get_core_mapping_join( string $mapping_id ): string {
global $wpdb;
if ( 'orders' === $mapping_id ) {
return '';
}
$is_address_mapping = in_array( $mapping_id, array( 'billing_address', 'shipping_address' ), true );
$alias = $this->get_core_mapping_alias( $mapping_id );
$table = $is_address_mapping ? $this->tables['addresses'] : $this->tables[ $mapping_id ];
$join = '';
$join_on = '';
$join .= "INNER JOIN `{$table}`" . ( $alias !== $table ? " AS `{$alias}`" : '' );
if ( isset( $this->mappings[ $mapping_id ]['order_id'] ) ) {
$join_on .= "`{$this->tables['orders']}`.id = `{$alias}`.order_id";
}
if ( $is_address_mapping ) {
$join_on .= $wpdb->prepare( " AND `{$alias}`.address_type = %s", substr( $mapping_id, 0, -8 ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
return $join . ( $join_on ? " ON ( {$join_on} )" : '' );
}
/**
* JOINs the main orders table with another table.
*
* @param string $table Table name (including prefix).
* @param string $alias Table alias to use. Defaults to $table.
* @param string $on ON clause. Defaults to "wc_orders.id = {$alias}.order_id".
* @param string $join_type JOIN type: LEFT, RIGHT or INNER.
* @param boolean $alias_once If TRUE, table won't be JOIN'ed again if already JOIN'ed.
* @return void
* @throws \Exception When an error occurs, such as trying to re-use an alias with $alias_once = FALSE.
*/
private function join( string $table, string $alias = '', string $on = '', string $join_type = 'inner', bool $alias_once = false ) {
$alias = empty( $alias ) ? $table : $alias;
$join_type = strtoupper( trim( $join_type ) );
if ( $this->tables['orders'] === $alias ) {
// translators: %s is a table name.
throw new \Exception( sprintf( __( '%s can not be used as a table alias in OrdersTableQuery', 'woocommerce' ), $alias ) );
}
if ( empty( $on ) ) {
if ( $this->tables['orders'] === $table ) {
$on = "{$this->tables['orders']}.id = {$alias}.id";
} else {
$on = "{$this->tables['orders']}.id = {$alias}.order_id";
}
}
if ( isset( $this->join[ $alias ] ) ) {
if ( ! $alias_once ) {
// translators: %s is a table name.
throw new \Exception( sprintf( __( 'Can not re-use table alias "%s" in OrdersTableQuery.', 'woocommerce' ), $alias ) );
}
return;
}
if ( '' === $join_type || ! in_array( $join_type, array( 'LEFT', 'RIGHT', 'INNER' ), true ) ) {
$join_type = 'INNER';
}
$sql_join = '';
$sql_join .= "{$join_type} JOIN {$table} ";
$sql_join .= ( $alias !== $table ) ? "AS {$alias} " : '';
$sql_join .= "ON ( {$on} )";
$this->join[ $alias ] = $sql_join;
}
/**
* Generates a properly escaped and sanitized WHERE condition for a given field.
*
* @param string $table The table the field belongs to.
* @param string $field The field or column name.
* @param string $operator The operator to use in the condition. Defaults to '=' or 'IN' depending on $value.
* @param mixed $value The value.
* @param string $type The column type as specified in {@see OrdersTableDataStore} column mappings.
* @return string The resulting WHERE condition.
*/
public function where( string $table, string $field, string $operator, $value, string $type ): string {
global $wpdb;
$db_util = wc_get_container()->get( DatabaseUtil::class );
$operator = strtoupper( '' !== $operator ? $operator : '=' );
try {
$format = $db_util->get_wpdb_format_for_type( $type );
} catch ( \Exception $e ) {
$format = '%s';
}
// = and != can be shorthands for IN and NOT in for array values.
if ( is_array( $value ) && '=' === $operator ) {
$operator = 'IN';
} elseif ( is_array( $value ) && '!=' === $operator ) {
$operator = 'NOT IN';
}
if ( ! in_array( $operator, array( '=', '!=', 'IN', 'NOT IN' ), true ) ) {
return false;
}
if ( is_array( $value ) ) {
$value = array_map( array( $db_util, 'format_object_value_for_db' ), $value, array_fill( 0, count( $value ), $type ) );
} else {
$value = $db_util->format_object_value_for_db( $value, $type );
}
if ( is_array( $value ) ) {
$placeholder = array_fill( 0, count( $value ), $format );
$placeholder = '(' . implode( ',', $placeholder ) . ')';
} else {
$placeholder = $format;
}
$sql = $wpdb->prepare( "{$table}.{$field} {$operator} {$placeholder}", $value ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
return $sql;
}
/**
* Processes fields related to the orders table.
*
* @return void
*/
private function process_orders_table_query_args(): void {
$this->sanitize_status();
$fields = array_filter(
array(
'id',
'status',
'type',
'currency',
'tax_amount',
'customer_id',
'billing_email',
'total_amount',
'parent_order_id',
'payment_method',
'payment_method_title',
'transaction_id',
'ip_address',
'user_agent',
),
array( $this, 'arg_isset' )
);
foreach ( $fields as $arg_key ) {
$this->where[] = $this->where( $this->tables['orders'], $arg_key, '=', $this->args[ $arg_key ], $this->mappings['orders'][ $arg_key ]['type'] );
}
if ( $this->arg_isset( 'parent_exclude' ) ) {
$this->where[] = $this->where( $this->tables['orders'], 'parent_order_id', '!=', $this->args['parent_exclude'], 'int' );
}
if ( $this->arg_isset( 'exclude' ) ) {
$this->where[] = $this->where( $this->tables['orders'], 'id', '!=', $this->args['exclude'], 'int' );
}
// 'customer' is a very special field.
if ( $this->arg_isset( 'customer' ) ) {
$customer_query = $this->generate_customer_query( $this->args['customer'] );
if ( $customer_query ) {
$this->where[] = $customer_query;
}
}
}
/**
* Generate SQL conditions for the 'customer' query.
*
* @param array $values List of customer ids or emails.
* @param string $relation 'OR' or 'AND' relation used to build the customer query.
* @return string SQL to be used in a WHERE clause.
*/
private function generate_customer_query( $values, string $relation = 'OR' ): string {
$values = is_array( $values ) ? $values : array( $values );
$ids = array();
$emails = array();
foreach ( $values as $value ) {
if ( is_array( $value ) ) {
$sql = $this->generate_customer_query( $value, 'AND' );
$pieces[] = $sql ? '(' . $sql . ')' : '';
} elseif ( is_numeric( $value ) ) {
$ids[] = absint( $value );
} elseif ( is_string( $value ) && is_email( $value ) ) {
$emails[] = sanitize_email( $value );
}
}
if ( $ids ) {
$pieces[] = $this->where( $this->tables['orders'], 'customer_id', '=', $ids, 'int' );
}
if ( $emails ) {
$pieces[] = $this->where( $this->tables['orders'], 'billing_email', '=', $emails, 'string' );
}
return $pieces ? implode( " $relation ", $pieces ) : '';
}
/**
* Processes fields related to the operational data table.
*
* @return void
*/
private function process_operational_data_table_query_args(): void {
$fields = array_filter(
array(
'created_via',
'woocommerce_version',
'prices_include_tax',
'order_key',
'discount_total_amount',
'discount_tax_amount',
'shipping_total_amount',
'shipping_tax_amount',
),
array( $this, 'arg_isset' )
);
if ( ! $fields ) {
return;
}
$this->join(
$this->tables['operational_data'],
'',
'',
'inner',
true
);
foreach ( $fields as $arg_key ) {
$this->where[] = $this->where( $this->tables['operational_data'], $arg_key, '=', $this->args[ $arg_key ], $this->mappings['operational_data'][ $arg_key ]['type'] );
}
}
/**
* Processes fields related to the addresses table.
*
* @return void
*/
private function process_addresses_table_query_args(): void {
global $wpdb;
foreach ( array( 'billing', 'shipping' ) as $address_type ) {
$fields = array_filter(
array(
$address_type . '_first_name',
$address_type . '_last_name',
$address_type . '_company',
$address_type . '_address_1',
$address_type . '_address_2',
$address_type . '_city',
$address_type . '_state',
$address_type . '_postcode',
$address_type . '_country',
$address_type . '_phone',
),
array( $this, 'arg_isset' )
);
if ( ! $fields ) {
continue;
}
$this->join(
$this->tables['addresses'],
$address_type,
$wpdb->prepare( "{$this->tables['orders']}.id = {$address_type}.order_id AND {$address_type}.address_type = %s", $address_type ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
'inner',
false
);
foreach ( $fields as $arg_key ) {
$column_name = str_replace( "{$address_type}_", '', $arg_key );
$this->where[] = $this->where(
$address_type,
$column_name,
'=',
$this->args[ $arg_key ],
$this->mappings[ "{$address_type}_address" ][ $column_name ]['type']
);
}
}
}
/**
* Generates the ORDER BY clause.
*
* @return void
*/
private function process_orderby(): void {
// 'order' and 'orderby' vars.
$this->args['order'] = $this->sanitize_order( $this->args['order'] ?? '' );
$this->sanitize_order_orderby();
$orderby = $this->args['orderby'];
if ( 'none' === $orderby ) {
$this->orderby = '';
return;
}
if ( 'include' === $orderby || 'post__in' === $orderby ) {
$ids = $this->args['id'] ?? $this->args['includes'];
if ( empty( $ids ) ) {
return;
}
$ids = array_map( 'absint', $ids );
$this->orderby = array( "FIELD( {$this->tables['orders']}.id, " . implode( ',', $ids ) . ' )' );
return;
}
$meta_orderby_keys = $this->meta_query ? $this->meta_query->get_orderby_keys() : array();
$orderby_array = array();
foreach ( $this->args['orderby'] as $_orderby => $order ) {
if ( in_array( $_orderby, $meta_orderby_keys, true ) ) {
$_orderby = $this->meta_query->get_orderby_clause_for_key( $_orderby );
}
$orderby_array[] = "{$_orderby} {$order}";
}
$this->orderby = $orderby_array;
}
/**
* Generates the limits to be used in the LIMIT clause.
*
* @return void
*/
private function process_limit(): void {
$row_count = ( $this->arg_isset( 'limit' ) ? (int) $this->args['limit'] : false );
$page = ( $this->arg_isset( 'page' ) ? absint( $this->args['page'] ) : 1 );
$offset = ( $this->arg_isset( 'offset' ) ? absint( $this->args['offset'] ) : false );
// Bool false indicates no limit was specified; less than -1 means an invalid value was passed (such as -3).
if ( false === $row_count || $row_count < -1 ) {
return;
}
if ( false === $offset && $row_count > -1 ) {
$offset = (int) ( ( $page - 1 ) * $row_count );
}
$this->limits = array( $offset, $row_count );
}
/**
* Checks if a query var is set (i.e. not one of the "skipped values").
*
* @param string $arg_key Query var.
* @return bool TRUE if query var is set.
*/
public function arg_isset( string $arg_key ): bool {
return ( isset( $this->args[ $arg_key ] ) && ! in_array( $this->args[ $arg_key ], self::SKIPPED_VALUES, true ) );
}
/**
* Runs the SQL query.
*
* @return void
*/
private function run_query(): void {
global $wpdb;
// Run query.
$this->orders = array_map( 'absint', $wpdb->get_col( $this->sql ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
// Set max_num_pages and found_orders if necessary.
if ( ( $this->arg_isset( 'no_found_rows' ) && ! $this->args['no_found_rows'] ) || empty( $this->orders ) ) {
return;
}
if ( $this->limits ) {
$this->found_orders = absint( $wpdb->get_var( $this->count_sql ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$this->max_num_pages = (int) ceil( $this->found_orders / $this->args['limit'] );
} else {
$this->found_orders = count( $this->orders );
}
}
/**
* Make some private available for backwards compatibility.
*
* @param string $name Property to get.
* @return mixed
*/
public function __get( string $name ) {
switch ( $name ) {
case 'found_orders':
case 'found_posts':
return $this->found_orders;
case 'max_num_pages':
return $this->max_num_pages;
case 'posts':
case 'orders':
return $this->results;
case 'request':
return $this->sql;
default:
break;
}
}
/**
* Returns the value of one of the query arguments.
*
* @param string $arg_name Query var.
* @return mixed
*/
public function get( string $arg_name ) {
return $this->args[ $arg_name ] ?? null;
}
/**
* Returns the name of one of the OrdersTableDatastore tables.
*
* @param string $table_id Table identifier. One of 'orders', 'operational_data', 'addresses', 'meta'.
* @return string The prefixed table name.
* @throws \Exception When table ID is not found.
*/
public function get_table_name( string $table_id = '' ): string {
if ( ! isset( $this->tables[ $table_id ] ) ) {
// Translators: %s is a table identifier.
throw new \Exception( sprintf( __( 'Invalid table id: %s.', 'woocommerce' ), $table_id ) );
}
return $this->tables[ $table_id ];
}
/**
* Finds table and mapping information about a field or column.
*
* @param string $field Field to look for in `.` format or just ``.
* @return false|array {
* @type string $table Full table name where the field is located.
* @type string $mapping_id Unprefixed table or mapping name.
* @type string $field_name Name of the corresponding order field.
* @type string $column Column in $table that corresponds to the field.
* @type string $type Field type.
* }
*/
public function get_field_mapping_info( $field ) {
global $wpdb;
$result = array(
'table' => '',
'mapping_id' => '',
'field_name' => '',
'column' => '',
'column_type' => '',
);
$mappings_to_search = array();
if ( false !== strstr( $field, '.' ) ) {
list( $mapping_or_table, $field_name_or_col ) = explode( '.', $field );
$mapping_or_table = substr( $mapping_or_table, 0, strlen( $wpdb->prefix ) ) === $wpdb->prefix ? substr( $mapping_or_table, strlen( $wpdb->prefix ) ) : $mapping_or_table;
$mapping_or_table = 'wc_' === substr( $mapping_or_table, 0, 3 ) ? substr( $mapping_or_table, 3 ) : $mapping_or_table;
if ( isset( $this->mappings[ $mapping_or_table ] ) ) {
if ( isset( $this->mappings[ $mapping_or_table ][ $field_name_or_col ] ) ) {
$result['mapping_id'] = $mapping_or_table;
$result['column'] = $field_name_or_col;
} else {
$mappings_to_search = array( $mapping_or_table );
}
}
} else {
$field_name_or_col = $field;
$mappings_to_search = array_keys( $this->mappings );
}
foreach ( $mappings_to_search as $mapping_id ) {
foreach ( $this->mappings[ $mapping_id ] as $column_name => $column_data ) {
if ( isset( $column_data['name'] ) && $column_data['name'] === $field_name_or_col ) {
$result['mapping_id'] = $mapping_id;
$result['column'] = $column_name;
break 2;
}
}
}
if ( ! $result['mapping_id'] || ! $result['column'] ) {
return false;
}
$field_info = $this->mappings[ $result['mapping_id'] ][ $result['column'] ];
$result['field_name'] = $field_info['name'];
$result['column_type'] = $field_info['type'];
$result['table'] = ( in_array( $result['mapping_id'], array( 'billing_address', 'shipping_address' ), true ) )
? $this->tables['addresses']
: $this->tables[ $result['mapping_id'] ];
return $result;
}
}
PK ;[pƳU U &