PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.9.1
WooCommerce v10.9.1
10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / src / Database / Migrations / CustomOrderTable / CLIRunner.php
woocommerce / src / Database / Migrations / CustomOrderTable Last commit date
CLIRunner.php 4 months ago PostMetaToOrderMetaMigrator.php 2 years ago PostToOrderAddressTableMigrator.php 2 years ago PostToOrderOpTableMigrator.php 2 years ago PostToOrderTableMigrator.php 3 years ago PostsToOrdersMigrationController.php 1 year ago
CLIRunner.php
1381 lines
1 <?php
2
3 namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;
4
5 use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
6 use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
7 use Automattic\WooCommerce\Internal\DataStores\Orders\LegacyDataHandler;
8 use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
9 use Automattic\WooCommerce\Internal\Features\FeaturesController;
10 use Automattic\WooCommerce\Utilities\PluginUtil;
11 use WP_CLI;
12
13 /**
14 * CLI tool for migrating order data to/from custom table.
15 *
16 * Credits https://github.com/liquidweb/woocommerce-custom-orders-table/blob/develop/includes/class-woocommerce-custom-orders-table-cli.php.
17 *
18 * Class CLIRunner
19 */
20 class CLIRunner {
21
22 /**
23 * CustomOrdersTableController instance.
24 *
25 * @var CustomOrdersTableController
26 */
27 private $controller;
28
29 /**
30 * DataSynchronizer instance.
31 *
32 * @var DataSynchronizer;
33 */
34 private $synchronizer;
35
36 /**
37 * PostsToOrdersMigrationController instance.
38 *
39 * @var PostsToOrdersMigrationController
40 */
41 private $post_to_cot_migrator;
42
43 /**
44 * Init method, invoked by DI container.
45 *
46 * @param CustomOrdersTableController $controller Instance.
47 * @param DataSynchronizer $synchronizer Instance.
48 * @param PostsToOrdersMigrationController $posts_to_orders_migration_controller Instance.
49 *
50 * @internal
51 */
52 final public function init( CustomOrdersTableController $controller, DataSynchronizer $synchronizer, PostsToOrdersMigrationController $posts_to_orders_migration_controller ) {
53 $this->controller = $controller;
54 $this->synchronizer = $synchronizer;
55 $this->post_to_cot_migrator = $posts_to_orders_migration_controller;
56 }
57
58 /**
59 * Registers commands for CLI.
60 */
61 public function register_commands() {
62 $legacy_commands = array( 'count_unmigrated', 'sync', 'verify_cot_data', 'enable', 'disable' );
63 foreach ( $legacy_commands as $cmd ) {
64 $new_cmd_name = 'verify_cot_data' === $cmd ? 'verify_data' : $cmd;
65
66 WP_CLI::add_command( "wc hpos {$new_cmd_name}", array( $this, $cmd ) );
67 WP_CLI::add_command(
68 "wc cot {$cmd}",
69 function ( array $args = array(), array $assoc_args = array() ) use ( $cmd, $new_cmd_name ) {
70 WP_CLI::warning( "Command `wc cot {$cmd}` is deprecated since 8.9.0. Please use `wc hpos {$new_cmd_name}` instead." );
71 return call_user_func( array( $this, $cmd ), $args, $assoc_args );
72 }
73 );
74 }
75
76 WP_CLI::add_command( 'wc hpos cleanup', array( $this, 'cleanup_post_data' ) );
77 WP_CLI::add_command( 'wc hpos status', array( $this, 'status' ) );
78 WP_CLI::add_command( 'wc hpos diff', array( $this, 'diff' ) );
79 WP_CLI::add_command( 'wc hpos backfill', array( $this, 'backfill' ) );
80 WP_CLI::add_command( 'wc hpos compatibility-info', array( $this, 'compatibility_info' ) );
81 WP_CLI::add_command( 'wc hpos compatibility-mode enable', array( $this, 'enable_compat_mode' ) );
82 WP_CLI::add_command( 'wc hpos compatibility-mode disable', array( $this, 'disable_compat_mode' ) );
83
84 WP_CLI::add_command( 'wc cot migrate', array( $this, 'migrate' ) ); // Fully deprecated. No longer works.
85 }
86
87 /**
88 * Check if the COT feature is enabled.
89 *
90 * @param bool $log Optionally log a error message.
91 *
92 * @return bool Whether the COT feature is enabled.
93 */
94 private function is_enabled( $log = true ): bool {
95 if ( ! $this->controller->custom_orders_table_usage_is_enabled() ) {
96 if ( $log ) {
97 WP_CLI::log(
98 sprintf(
99 // translators: %s - link to testing instructions webpage.
100 __( 'Custom order table usage is not enabled. If you are testing, you can enable it by following the testing instructions in %s', 'woocommerce' ),
101 'https://developer.woocommerce.com/docs/features/high-performance-order-storage/recipe-book/'
102 )
103 );
104 }
105 }
106
107 return $this->controller->custom_orders_table_usage_is_enabled();
108 }
109
110 /**
111 * Free some in-memory usage.
112 */
113 private function free_in_memory_usage() {
114 $GLOBALS['wpdb']->flush();
115 $GLOBALS['wpdb']->queries = array(); // Query log.
116
117 if ( function_exists( 'wp_cache_supports' ) && wp_cache_supports( 'flush_runtime' ) ) {
118 wp_cache_flush_runtime();
119 }
120 }
121
122 /**
123 * Count how many orders have yet to be migrated into the custom orders table.
124 *
125 * ## EXAMPLES
126 *
127 * wp wc hpos count_unmigrated
128 *
129 * @param array $args Positional arguments passed to the command.
130 *
131 * @param array $assoc_args Associative arguments (options) passed to the command.
132 *
133 * @return int The number of orders to be migrated.*
134 */
135 public function count_unmigrated( $args = array(), $assoc_args = array() ): int {
136 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
137 $order_count = $this->synchronizer->get_current_orders_pending_sync_count();
138
139 $assoc_args = wp_parse_args(
140 $assoc_args,
141 array(
142 'log' => true,
143 )
144 );
145 if ( isset( $assoc_args['log'] ) && $assoc_args['log'] ) {
146 WP_CLI::log(
147 sprintf(
148 /* Translators: %1$d is the number of orders to be synced. */
149 _n(
150 'There is %1$d order to be synced.',
151 'There are %1$d orders to be synced.',
152 $order_count,
153 'woocommerce'
154 ),
155 $order_count
156 )
157 );
158 }
159
160 return (int) $order_count;
161 }
162
163 /**
164 * Sync order data between the custom order tables and the core WordPress post tables.
165 *
166 * ## OPTIONS
167 *
168 * [--batch-size=<batch-size>]
169 * : The number of orders to process in each batch.
170 * ---
171 * default: 500
172 * ---
173 *
174 * ## EXAMPLES
175 *
176 * wp wc hpos sync --batch-size=500
177 *
178 * @param array $args Positional arguments passed to the command.
179 * @param array $assoc_args Associative arguments (options) passed to the command.
180 */
181 public function sync( $args = array(), $assoc_args = array() ) {
182 if ( ! $this->synchronizer->check_orders_table_exists() ) {
183 WP_CLI::warning( __( 'Custom order tables does not exist, creating...', 'woocommerce' ) );
184 $this->synchronizer->create_database_tables();
185 if ( $this->synchronizer->check_orders_table_exists() ) {
186 WP_CLI::success( __( 'Custom order tables were created successfully.', 'woocommerce' ) );
187 } else {
188 WP_CLI::error( __( 'Custom order tables could not be created.', 'woocommerce' ) );
189 }
190 }
191
192 $order_count = $this->count_unmigrated();
193
194 // Abort if there are no orders to migrate.
195 if ( ! $order_count ) {
196 return WP_CLI::warning( __( 'There are no orders to sync, aborting.', 'woocommerce' ) );
197 }
198
199 $assoc_args = wp_parse_args(
200 $assoc_args,
201 array(
202 'batch-size' => 500,
203 )
204 );
205 $batch_size = ( (int) $assoc_args['batch-size'] ) === 0 ? 500 : (int) $assoc_args['batch-size'];
206 $progress = WP_CLI\Utils\make_progress_bar( 'Order Data Sync', $order_count / $batch_size );
207 $processed = 0;
208 $batch_count = 1;
209 $total_time = 0;
210 $orders_remaining = true;
211
212 while ( $order_count > 0 || $orders_remaining ) {
213 $remaining_count = $order_count;
214
215 WP_CLI::debug(
216 sprintf(
217 /* Translators: %1$d is the batch number and %2$d is the batch size. */
218 __( 'Beginning batch #%1$d (%2$d orders/batch).', 'woocommerce' ),
219 $batch_count,
220 $batch_size
221 )
222 );
223 $batch_start_time = microtime( true );
224 $order_ids = $this->synchronizer->get_next_batch_to_process( $batch_size );
225 if ( count( $order_ids ) ) {
226 $this->synchronizer->process_batch( $order_ids );
227 }
228 $processed += count( $order_ids );
229 $batch_total_time = microtime( true ) - $batch_start_time;
230
231 WP_CLI::debug(
232 sprintf(
233 // Translators: %1$d is the batch number, %2$d is the number of processed orders and %3$d is the execution time in seconds.
234 __( 'Batch %1$d (%2$d orders) completed in %3$d seconds', 'woocommerce' ),
235 $batch_count,
236 count( $order_ids ),
237 $batch_total_time
238 )
239 );
240
241 ++$batch_count;
242 $total_time += $batch_total_time;
243
244 $progress->tick();
245
246 $orders_remaining = count( $this->synchronizer->get_next_batch_to_process( 1 ) ) > 0;
247 $order_count = $remaining_count - $batch_size;
248
249 $this->free_in_memory_usage();
250 }
251
252 $progress->finish();
253
254 // Issue a warning if no orders were migrated.
255 if ( ! $processed ) {
256 return WP_CLI::warning( __( 'No orders were synced.', 'woocommerce' ) );
257 }
258
259 WP_CLI::log( __( 'Sync completed.', 'woocommerce' ) );
260
261 return WP_CLI::success(
262 sprintf(
263 /* Translators: %1$d is the number of migrated orders and %2$d is the execution time in seconds. */
264 _n(
265 '%1$d order was synced in %2$d seconds.',
266 '%1$d orders were synced in %2$d seconds.',
267 $processed,
268 'woocommerce'
269 ),
270 $processed,
271 $total_time
272 )
273 );
274 }
275
276 /**
277 * [Deprecated] Use `wp wc hpos sync` instead.
278 * Copy order data into the postmeta table.
279 *
280 * Note that this could dramatically increase the size of your postmeta table, but is recommended
281 * if you wish to stop using the custom orders table plugin.
282 *
283 * ## OPTIONS
284 *
285 * [--batch-size=<batch-size>]
286 * : The number of orders to process in each batch. Passing a value of 0 will disable batching.
287 * ---
288 * default: 500
289 * ---
290 *
291 * ## EXAMPLES
292 *
293 * # Copy all order data into the post meta table, 500 posts at a time.
294 * wp wc cot migrate --batch-size=500
295 *
296 * @param array $args Positional arguments passed to the command.
297 * @param array $assoc_args Associative arguments (options) passed to the command.
298 */
299 public function migrate( array $args = array(), array $assoc_args = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- for backwards compat.
300 WP_CLI::log( __( 'Migrate command is deprecated. Please use `sync` instead.', 'woocommerce' ) );
301 }
302
303 /**
304 * Verify migrated order data with original posts data.
305 *
306 * ## OPTIONS
307 *
308 * [--batch-size=<batch-size>]
309 * : The number of orders to verify in each batch.
310 * ---
311 * default: 500
312 * ---
313 *
314 * [--start-from=<order_id>]
315 * : Order ID to start from.
316 * ---
317 * default: 0
318 * ---
319 *
320 * [--end-at=<order_id>]
321 * : Order ID to end at.
322 * ---
323 * default: -1
324 * ---
325 *
326 * [--verbose]
327 * : Whether to output errors as they happen in batch, or output them all together at the end.
328 * ---
329 * default: false
330 * ---
331 *
332 * [--order-types]
333 * : Comma-separated list of order types that needs to be verified. For example, --order-types=shop_order,shop_order_refund
334 * ---
335 * default: Output of function `wc_get_order_types( 'cot-migration' )`
336 *
337 * [--re-migrate]
338 * : Attempt to re-migrate orders that failed verification. You should only use this option when you have never run the site with HPOS as authoritative source of order data yet, or you have manually checked the reported errors, otherwise, you risk stale data overwriting the more recent data.
339 * default: false
340 *
341 * ## EXAMPLES
342 *
343 * # Verify migrated order data, 500 orders at a time.
344 * wp wc hpos verify_cot_data --batch-size=500 --start-from=0 --end-at=10000
345 *
346 * @param array $args Positional arguments passed to the command.
347 * @param array $assoc_args Associative arguments (options) passed to the command.
348 */
349 public function verify_cot_data( $args = array(), $assoc_args = array() ) {
350 global $wpdb;
351
352 if ( ! $this->synchronizer->check_orders_table_exists() ) {
353 WP_CLI::error( __( 'Orders table does not exist.', 'woocommerce' ) );
354 return;
355 }
356
357 $assoc_args = wp_parse_args(
358 $assoc_args,
359 array(
360 'batch-size' => 500,
361 'start-from' => 0,
362 'end-at' => - 1,
363 'verbose' => false,
364 'order-types' => '',
365 're-migrate' => false,
366 )
367 );
368
369 $batch_count = 1;
370 $total_time = 0;
371 $failed_ids = array();
372 $processed = 0;
373 $order_id_start = (int) $assoc_args['start-from'];
374 $order_id_end = (int) $assoc_args['end-at'];
375 $order_id_end = -1 === $order_id_end ? PHP_INT_MAX : $order_id_end;
376 $batch_size = ( (int) $assoc_args['batch-size'] ) === 0 ? 500 : (int) $assoc_args['batch-size'];
377 $verbose = (bool) $assoc_args['verbose'];
378 $order_types = wc_get_order_types( 'cot-migration' );
379 $remigrate = (bool) $assoc_args['re-migrate'];
380 if ( ! empty( $assoc_args['order-types'] ) ) {
381 $passed_order_types = array_map( 'trim', explode( ',', $assoc_args['order-types'] ) );
382 $order_types = array_intersect( $order_types, $passed_order_types );
383 }
384
385 if ( 0 === count( $order_types ) ) {
386 return WP_CLI::error(
387 sprintf(
388 /* Translators: %s is the comma-separated list of order types. */
389 __( 'Passed order type does not match any registered order types. Following order types are registered: %s', 'woocommerce' ),
390 implode( ',', wc_get_order_types( 'cot-migration' ) )
391 )
392 );
393 }
394
395 $order_types_pl = implode( ',', array_fill( 0, count( $order_types ), '%s' ) );
396
397 $order_count = $this->get_verify_order_count( $order_id_start, $order_id_end, $order_types, false );
398
399 $progress = WP_CLI\Utils\make_progress_bar( 'Order Data Verification', $order_count / $batch_size );
400
401 $error_processing = false;
402
403 if ( ! $order_count ) {
404 return WP_CLI::warning( __( 'There are no orders to verify, aborting.', 'woocommerce' ) );
405 }
406
407 while ( $order_count > 0 ) {
408 WP_CLI::debug(
409 sprintf(
410 /* Translators: %1$d is the batch number, %2$d is the batch size. */
411 __( 'Beginning verification for batch #%1$d (%2$d orders/batch).', 'woocommerce' ),
412 $batch_count,
413 $batch_size
414 )
415 );
416
417 // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Inputs are prepared.
418 $order_ids = $wpdb->get_col(
419 $wpdb->prepare(
420 "SELECT ID FROM $wpdb->posts WHERE post_type in ( $order_types_pl ) AND ID >= %d AND ID <= %d ORDER BY ID ASC LIMIT %d",
421 array_merge(
422 $order_types,
423 array(
424 $order_id_start,
425 $order_id_end,
426 $batch_size,
427 )
428 )
429 )
430 );
431 // phpcs:enable
432 $batch_start_time = microtime( true );
433 $failed_ids_in_current_batch = $this->post_to_cot_migrator->verify_migrated_orders( $order_ids );
434 $failed_ids_in_current_batch = $this->verify_meta_data( $order_ids, $failed_ids_in_current_batch );
435 $failed_ids = $verbose ? array() : $failed_ids + $failed_ids_in_current_batch;
436 $error_processing = $error_processing || ! empty( $failed_ids_in_current_batch );
437 $processed += count( $order_ids );
438 $batch_total_time = microtime( true ) - $batch_start_time;
439 ++$batch_count;
440 $total_time += $batch_total_time;
441
442 if ( count( $failed_ids_in_current_batch ) > 0 ) {
443 if ( $verbose ) {
444 $errors = wp_json_encode( $failed_ids_in_current_batch, JSON_PRETTY_PRINT );
445 WP_CLI::warning(
446 sprintf(
447 /* Translators: %1$d is number of errors and %2$s is the formatted array of order IDs. */
448 _n(
449 '%1$d error found: %2$s. Please review the error above.',
450 '%1$d errors found: %2$s. Please review the errors above.',
451 count( $failed_ids_in_current_batch ),
452 'woocommerce'
453 ),
454 count( $failed_ids_in_current_batch ),
455 $errors
456 )
457 );
458 }
459
460 if ( $remigrate ) {
461 $failed_ids = $failed_ids ? array_diff_key( $failed_ids, $failed_ids_in_current_batch ) : array();
462 $error_processing = ( ! $verbose ) && $failed_ids;
463
464 $verbose && WP_CLI::warning( sprintf( __( 'Attempting to remigrate...', 'woocommerce' ) ) );
465
466 $failed_ids_in_current_batch_keys = array_keys( $failed_ids_in_current_batch );
467 $this->synchronizer->process_batch( $failed_ids_in_current_batch_keys );
468 $errors_in_remigrate_batch = $this->post_to_cot_migrator->verify_migrated_orders( $failed_ids_in_current_batch_keys );
469 $errors_in_remigrate_batch = $this->verify_meta_data( $failed_ids_in_current_batch_keys, $errors_in_remigrate_batch );
470
471 if ( count( $errors_in_remigrate_batch ) > 0 ) {
472 $error_processing = true;
473 $formatted_errors = wp_json_encode( $errors_in_remigrate_batch, JSON_PRETTY_PRINT );
474
475 if ( $verbose ) {
476 WP_CLI::warning(
477 sprintf(
478 /* Translators: %1$d is number of errors and %2$s is the formatted array of order IDs. */
479 _n(
480 '%1$d error found: %2$s when re-migrating order. Please review the error above.',
481 '%1$d errors found: %2$s when re-migrating orders. Please review the errors above.',
482 count( $errors_in_remigrate_batch ),
483 'woocommerce'
484 ),
485 count( $errors_in_remigrate_batch ),
486 $formatted_errors
487 )
488 );
489 } else {
490 array_walk(
491 $errors_in_remigrate_batch,
492 function ( &$errors_for_order ) {
493 $errors_for_order[] = array( 'remigrate_failed' => true );
494 }
495 );
496 $failed_ids = $failed_ids + $errors_in_remigrate_batch;
497 }
498 } else {
499 $verbose && WP_CLI::warning( 'Re-migration successful.', 'woocommerce' );
500 }
501 }
502 }
503
504 $progress->tick();
505
506 WP_CLI::debug(
507 sprintf(
508 /* Translators: %1$d is the batch number, %2$d is time taken to process batch. */
509 __( 'Batch %1$d (%2$d orders) completed in %3$d seconds.', 'woocommerce' ),
510 $batch_count,
511 count( $order_ids ),
512 $batch_total_time
513 )
514 );
515
516 $order_id_start = max( $order_ids ) + 1;
517 $remaining_count = $this->get_verify_order_count( $order_id_start, $order_id_end, $order_types, false );
518 if ( $remaining_count === $order_count ) {
519 return WP_CLI::error( __( 'Infinite loop detected, aborting. No errors found.', 'woocommerce' ) );
520 }
521 $order_count = $remaining_count;
522 }
523
524 $progress->finish();
525 WP_CLI::log( __( 'Verification completed.', 'woocommerce' ) );
526
527 if ( ! $error_processing ) {
528 return WP_CLI::success(
529 sprintf(
530 /* Translators: %1$d is the number of migrated orders and %2$d is time taken. */
531 _n(
532 '%1$d order was verified in %2$d seconds.',
533 '%1$d orders were verified in %2$d seconds.',
534 $processed,
535 'woocommerce'
536 ),
537 $processed,
538 $total_time
539 )
540 );
541 } else {
542 return WP_CLI::error(
543 sprintf(
544 '%1$s %2$s',
545 sprintf(
546 /* Translators: %1$d is the number of migrated orders and %2$d is the execution time in seconds. */
547 _n(
548 '%1$d order was verified in %2$d seconds.',
549 '%1$d orders were verified in %2$d seconds.',
550 $processed,
551 'woocommerce'
552 ),
553 $processed,
554 $total_time
555 ),
556 $failed_ids
557 ? sprintf(
558 /* Translators: %1$d is number of errors and %2$s is the formatted array of order IDs. */
559 _n(
560 '%1$d error found: %2$s. Please review the error above.',
561 '%1$d errors found: %2$s. Please review the errors above.',
562 count( $failed_ids ),
563 'woocommerce'
564 ),
565 count( $failed_ids ),
566 wp_json_encode( $failed_ids, JSON_PRETTY_PRINT )
567 )
568 : __( 'Please review the errors above.', 'woocommerce' )
569 )
570 );
571 }
572 }
573
574 /**
575 * Helper method to get count for orders needing verification.
576 *
577 * @param int $order_id_start Order ID to start from.
578 * @param int $order_id_end Order ID to end at.
579 * @param array $order_types List of order types to verify.
580 * @param bool $log Whether to also log an error message.
581 *
582 * @return int Order count.
583 */
584 private function get_verify_order_count( int $order_id_start, int $order_id_end, array $order_types, bool $log = true ): int {
585 global $wpdb;
586
587 $order_types_placeholder = implode( ',', array_fill( 0, count( $order_types ), '%s' ) );
588
589 // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Inputs are prepared.
590 $order_count = (int) $wpdb->get_var(
591 $wpdb->prepare(
592 "SELECT COUNT(*) FROM $wpdb->posts WHERE post_type in ($order_types_placeholder) AND ID >= %d AND ID <= %d",
593 array_merge(
594 $order_types,
595 array(
596 $order_id_start,
597 $order_id_end,
598 )
599 )
600 )
601 );
602 // phpcs:enable
603
604 if ( $log ) {
605 WP_CLI::log(
606 sprintf(
607 /* Translators: %1$d is the number of orders to be verified. */
608 _n(
609 'There is %1$d order to be verified.',
610 'There are %1$d orders to be verified.',
611 $order_count,
612 'woocommerce'
613 ),
614 $order_count
615 )
616 );
617 }
618
619 return $order_count;
620 }
621
622 /**
623 * Verify meta data as part of verifying the order object.
624 *
625 * @param array $order_ids Order IDs.
626 * @param array $failed_ids Array for storing failed IDs.
627 *
628 * @return array Failed IDs with meta details.
629 */
630 private function verify_meta_data( array $order_ids, array $failed_ids ): array {
631 $meta_keys_to_ignore = $this->synchronizer->get_ignored_order_props();
632
633 global $wpdb;
634 if ( ! count( $order_ids ) ) {
635 return array();
636 }
637 $excluded_columns = array_merge(
638 $this->post_to_cot_migrator->get_migrated_meta_keys(),
639 $meta_keys_to_ignore
640 );
641 $excluded_columns_placeholder = implode( ', ', array_fill( 0, count( $excluded_columns ), '%s' ) );
642 $order_ids_placeholder = implode( ', ', array_fill( 0, count( $order_ids ), '%d' ) );
643 $meta_table = OrdersTableDataStore::get_meta_table_name();
644
645 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- table names are hardcoded, orders_ids and excluded_columns are prepared.
646 $query = $wpdb->prepare(
647 "
648 SELECT {$wpdb->postmeta}.post_id as entity_id, {$wpdb->postmeta}.meta_key, {$wpdb->postmeta}.meta_value
649 FROM $wpdb->postmeta
650 WHERE
651 {$wpdb->postmeta}.post_id in ( $order_ids_placeholder ) AND
652 {$wpdb->postmeta}.meta_key not in ( $excluded_columns_placeholder )
653 ORDER BY {$wpdb->postmeta}.post_id ASC, {$wpdb->postmeta}.meta_key ASC;
654 ",
655 array_merge(
656 $order_ids,
657 $excluded_columns
658 )
659 );
660 $source_data = $wpdb->get_results( $query, ARRAY_A );
661 // phpcs:enable
662
663 $normalized_source_data = $this->normalize_raw_meta_data( $source_data );
664
665 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- table names are hardcoded, orders_ids and excluded_columns are prepared.
666 $migrated_query = $wpdb->prepare(
667 "
668 SELECT $meta_table.order_id as entity_id, $meta_table.meta_key, $meta_table.meta_value
669 FROM $meta_table
670 WHERE
671 $meta_table.order_id in ( $order_ids_placeholder )
672 ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
673 ",
674 $order_ids
675 );
676 $migrated_data = $wpdb->get_results( $migrated_query, ARRAY_A );
677 // phpcs:enable
678
679 $normalized_migrated_meta_data = $this->normalize_raw_meta_data( $migrated_data );
680
681 foreach ( $normalized_source_data as $order_id => $meta ) {
682 foreach ( $meta as $meta_key => $values ) {
683 $migrated_meta_values = isset( $normalized_migrated_meta_data[ $order_id ][ $meta_key ] ) ? $normalized_migrated_meta_data[ $order_id ][ $meta_key ] : array();
684 $diff = array_diff( $values, $migrated_meta_values );
685
686 if ( count( $diff ) ) {
687 if ( ! isset( $failed_ids[ $order_id ] ) ) {
688 $failed_ids[ $order_id ] = array();
689 }
690 $failed_ids[ $order_id ][] = array(
691 'order_id' => $order_id,
692 'meta_key' => $meta_key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Not a meta query.
693 'orig_meta_values' => $values,
694 'new_meta_values' => $migrated_meta_values,
695 );
696 }
697 }
698 }
699
700 return $failed_ids;
701 }
702
703 /**
704 * Helper method to normalize response from meta queries into order_id > meta_key > meta_values.
705 *
706 * @param array $data Data fetched from meta queries.
707 *
708 * @return array Normalized data.
709 */
710 private function normalize_raw_meta_data( array $data ): array {
711 $clubbed_data = array();
712 foreach ( $data as $row ) {
713 if ( ! isset( $clubbed_data[ $row['entity_id'] ] ) ) {
714 $clubbed_data[ $row['entity_id'] ] = array();
715 }
716 if ( ! isset( $clubbed_data[ $row['entity_id'] ][ $row['meta_key'] ] ) ) {
717 $clubbed_data[ $row['entity_id'] ][ $row['meta_key'] ] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Not a meta query.
718 }
719 $clubbed_data[ $row['entity_id'] ][ $row['meta_key'] ][] = $row['meta_value'];
720 }
721 return $clubbed_data;
722 }
723
724 /**
725 * Set custom order tables (HPOS) to authoritative if: 1). HPOS and posts tables are in sync, or, 2). This is a new shop (in this case also create tables). Additionally, all installed WC plugins should be compatible.
726 *
727 * ## OPTIONS
728 *
729 * [--for-new-shop]
730 * : Enable only if this is a new shop, irrespective of whether tables are in sync.
731 * ---
732 * default: false
733 * ---
734 *
735 * [--with-sync]
736 * : Also enables sync (if it's currently not enabled).
737 * ---
738 * default: false
739 * ---
740 *
741 * [--ignore-plugin-compatibility]
742 * : Enable even if there are active plugins that are incompatible with HPOS.
743 *
744 * ### EXAMPLES
745 *
746 * # Enable HPOS on new shops.
747 * wp wc hpos enable --for-new-shop
748 *
749 * @param array $args Positional arguments passed to the command.
750 * @param array $assoc_args Associative arguments (options) passed to the command.
751 *
752 * @return void
753 */
754 public function enable( array $args = array(), array $assoc_args = array() ) {
755 $assoc_args = wp_parse_args(
756 $assoc_args,
757 array(
758 'for-new-shop' => false,
759 'with-sync' => false,
760 'ignore-plugin-compatibility' => false,
761 )
762 );
763
764 $enable_hpos = true;
765 WP_CLI::log( __( 'Running pre-enable checks...', 'woocommerce' ) );
766
767 $is_new_shop = \WC_Install::is_new_install();
768 if ( $assoc_args['for-new-shop'] && ! $is_new_shop ) {
769 WP_CLI::error( __( '[Failed] This is not a new shop, but --for-new-shop flag was passed.', 'woocommerce' ) );
770 }
771
772 $container = wc_get_container();
773 /** Feature controller instance @var FeaturesController $feature_controller */
774 $feature_controller = $container->get( FeaturesController::class );
775 if ( ! $assoc_args['ignore-plugin-compatibility'] ) {
776 $compatibility_info = $feature_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
777 /** Plugin util instance @var PluginUtil $plugin_util */
778 $plugin_util = $container->get( PluginUtil::class );
779 $incompatibles = $plugin_util->get_items_considered_incompatible( 'custom_order_tables', $compatibility_info );
780 if ( count( $incompatibles ) > 0 ) {
781 WP_CLI::warning( __( '[Failed] Some installed plugins are incompatible. Please review the plugins by going to WooCommerce > Settings > Advanced > Features and see the "Order data storage" section.', 'woocommerce' ) );
782 $enable_hpos = false;
783 }
784 }
785
786 /** DataSynchronizer instance @var DataSynchronizer $data_synchronizer */
787 $data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
788 $pending_orders = $data_synchronizer->get_total_pending_count();
789 $table_exists = $data_synchronizer->check_orders_table_exists();
790
791 if ( ! $table_exists ) {
792 WP_CLI::warning( __( 'Orders table does not exist. Creating...', 'woocommerce' ) );
793 if ( $is_new_shop || 0 === $pending_orders ) {
794 $data_synchronizer->create_database_tables();
795 if ( $data_synchronizer->check_orders_table_exists() ) {
796 WP_CLI::log( __( 'Orders table created.', 'woocommerce' ) );
797 $table_exists = true;
798 } else {
799 WP_CLI::warning( __( '[Failed] Orders table could not be created.', 'woocommerce' ) );
800 $enable_hpos = false;
801 }
802 } else {
803 WP_CLI::warning( __( '[Failed] The orders table does not exist and this is not a new shop. Please create the table by going to WooCommerce > Settings > Advanced > Features and enabling sync.', 'woocommerce' ) );
804 $enable_hpos = false;
805 }
806 }
807
808 if ( $pending_orders > 0 ) {
809 WP_CLI::warning(
810 sprintf(
811 // translators: %s is the command to run (wp wc cot sync).
812 __( '[Failed] There are orders pending sync. Please run `%s` to sync pending orders.', 'woocommerce' ),
813 'wp wc hpos sync',
814 )
815 );
816 $enable_hpos = false;
817 }
818
819 if ( $assoc_args['with-sync'] && $table_exists ) {
820 $this->toggle_compat_mode( true );
821 }
822
823 if ( ! $enable_hpos ) {
824 WP_CLI::error( __( 'HPOS pre-checks failed, please see the errors above', 'woocommerce' ) );
825 return;
826 }
827
828 /** CustomOrdersTableController instance @var CustomOrdersTableController $cot_status */
829 $cot_status = wc_get_container()->get( CustomOrdersTableController::class );
830 if ( $cot_status->custom_orders_table_usage_is_enabled() ) {
831 WP_CLI::warning( __( 'HPOS is already enabled.', 'woocommerce' ) );
832 } else {
833 $feature_controller->change_feature_enable( 'custom_order_tables', true );
834 if ( $cot_status->custom_orders_table_usage_is_enabled() ) {
835 WP_CLI::success( __( 'HPOS enabled.', 'woocommerce' ) );
836 } else {
837 WP_CLI::error( __( 'HPOS could not be enabled.', 'woocommerce' ) );
838 }
839 }
840 }
841
842 /**
843 * Disables custom order tables (HPOS) and posts to authoritative if HPOS and post tables are in sync.
844 *
845 * ## OPTIONS
846 *
847 * [--with-sync]
848 * : Also disables sync (if it's currently enabled).
849 * ---
850 * default: false
851 * ---
852 *
853 * ### EXAMPLES
854 *
855 * # Disable HPOS.
856 * wp wc hpos disable
857 *
858 * @param array $args Positional arguments passed to the command.
859 * @param array $assoc_args Associative arguments (options) passed to the command.
860 */
861 public function disable( $args, $assoc_args ) {
862 $assoc_args = wp_parse_args(
863 $assoc_args,
864 array(
865 'with-sync' => false,
866 )
867 );
868
869 WP_CLI::log( __( 'Running pre-disable checks...', 'woocommerce' ) );
870
871 /** DataSynchronizer instance @var DataSynchronizer $data_synchronizer */
872 $data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
873 $pending_orders = $data_synchronizer->get_total_pending_count();
874 if ( $pending_orders > 0 ) {
875 return WP_CLI::error(
876 sprintf(
877 // translators: %s is the command to run (wp wc cot sync).
878 __( '[Failed] There are orders pending sync. Please run `%s` to sync pending orders.', 'woocommerce' ),
879 'wp wc hpos sync',
880 )
881 );
882 }
883
884 /** FeaturesController instance @var FeaturesController $feature_controller */
885 $feature_controller = wc_get_container()->get( FeaturesController::class );
886
887 /** CustomOrdersTableController instance @var CustomOrdersTableController $cot_status */
888 $cot_status = wc_get_container()->get( CustomOrdersTableController::class );
889 if ( ! $cot_status->custom_orders_table_usage_is_enabled() ) {
890 WP_CLI::warning( __( 'HPOS is already disabled.', 'woocommerce' ) );
891 } else {
892 $feature_controller->change_feature_enable( 'custom_order_tables', false );
893 if ( $cot_status->custom_orders_table_usage_is_enabled() ) {
894 return WP_CLI::warning( __( 'HPOS could not be disabled.', 'woocommerce' ) );
895 } else {
896 WP_CLI::success( __( 'HPOS disabled.', 'woocommerce' ) );
897 }
898 }
899
900 if ( $assoc_args['with-sync'] ) {
901 $this->toggle_compat_mode( false );
902 }
903 }
904
905 /**
906 * When HPOS is enabled, this command lets you remove redundant data from the postmeta table for migrated orders.
907 *
908 * ## OPTIONS
909 *
910 * <all|id|range>...
911 * : ID or range of orders to clean up.
912 *
913 * [--batch-size=<batch-size>]
914 * : Number of orders to process per batch. Applies only to cleaning up of 'all' orders.
915 * ---
916 * default: 500
917 * ---
918 *
919 * [--force]
920 * : When true, post meta will be cleaned up even if the post appears to have been updated more recently than the order.
921 * ---
922 * default: false
923 * ---
924 *
925 * ## EXAMPLES
926 *
927 * # Cleanup post data for order 314.
928 * $ wp wc hpos cleanup 314
929 *
930 * # Cleanup postmeta for orders with IDs between 10 and 100 and order 314.
931 * $ wp wc hpos cleanup 10-100 314
932 *
933 * # Cleanup postmeta for all orders.
934 * wp wc hpos cleanup all
935 *
936 * # Cleanup postmeta for all orders with a batch size of 200 (instead of the default 500).
937 * wp wc hpos cleanup all --batch-size=200
938 *
939 * @param array $args Positional arguments passed to the command.
940 * @param array $assoc_args Associative arguments (options) passed to the command.
941 * @return void
942 */
943 public function cleanup_post_data( array $args = array(), array $assoc_args = array() ) {
944 if ( ! $this->synchronizer->custom_orders_table_is_authoritative() || $this->synchronizer->data_sync_is_enabled() ) {
945 WP_CLI::error( __( 'Cleanup can only be performed when HPOS is active and compatibility mode is disabled.', 'woocommerce' ) );
946 }
947 $handler = wc_get_container()->get( LegacyDataHandler::class );
948
949 $all_orders = 'all' === $args[0];
950 $force = (bool) ( $assoc_args['force'] ?? false );
951 $q_order_ids = $all_orders ? array() : $args;
952 $q_limit = $all_orders ? absint( $assoc_args['batch-size'] ?? 500 ) : 0; // Limit per batch.
953
954 $order_count = $handler->count_orders_for_cleanup( $q_order_ids );
955 if ( ! $order_count ) {
956 WP_CLI::warning( __( 'No orders to cleanup.', 'woocommerce' ) );
957 return;
958 }
959
960 $progress = WP_CLI\Utils\make_progress_bar( __( 'HPOS cleanup', 'woocommerce' ), $order_count );
961 $count = 0;
962 $failed_ids = array();
963
964 // translators: %d is the number of orders to clean up.
965 WP_CLI::log( sprintf( _n( 'Starting cleanup for %d order...', 'Starting cleanup for %d orders...', $order_count, 'woocommerce' ), $order_count ) );
966
967 do {
968 $failed_ids_in_batch = array();
969 $order_ids = $handler->get_orders_for_cleanup( $q_order_ids, $q_limit );
970
971 if ( $failed_ids && empty( array_diff( $order_ids, $failed_ids ) ) ) {
972 break;
973 }
974
975 $order_ids = array_diff( $order_ids, $failed_ids ); // Do not reattempt IDs that have already failed.
976
977 foreach ( $order_ids as $order_id ) {
978 try {
979 $handler->cleanup_post_data( $order_id, $force );
980 ++$count;
981
982 // translators: %d is an order ID.
983 WP_CLI::debug( sprintf( __( 'Cleanup completed for order %d.', 'woocommerce' ), $order_id ) );
984 } catch ( \Exception $e ) {
985 // translators: %1$d is an order ID, %2$s is an error message.
986 WP_CLI::warning( sprintf( __( 'An error occurred while cleaning up order %1$d: %2$s', 'woocommerce' ), $order_id, $e->getMessage() ) );
987 $failed_ids_in_batch[] = $order_id;
988 }
989
990 $progress->tick();
991 }
992
993 $failed_ids = array_merge( $failed_ids, $failed_ids_in_batch );
994
995 if ( ! $all_orders ) {
996 break;
997 }
998
999 if ( $failed_ids_in_batch && ! array_diff( $order_ids, $failed_ids_in_batch ) ) {
1000 WP_CLI::warning( __( 'Failed to clean up all orders in a batch. Aborting.', 'woocommerce' ) );
1001 break;
1002 }
1003
1004 $this->free_in_memory_usage();
1005 } while ( $order_ids );
1006
1007 $progress->finish();
1008
1009 if ( $failed_ids ) {
1010 return WP_CLI::error(
1011 sprintf(
1012 // translators: %d is the number of orders that were cleaned up.
1013 _n( 'Cleanup completed for %d order. Review errors above.', 'Cleanup completed for %d orders. Review errors above.', $count, 'woocommerce' ),
1014 $count
1015 )
1016 );
1017 }
1018
1019 WP_CLI::success(
1020 sprintf(
1021 // translators: %d is the number of orders that were cleaned up.
1022 _n( 'Cleanup completed for %d order.', 'Cleanup completed for %d orders.', $count, 'woocommerce' ),
1023 $count
1024 )
1025 );
1026 }
1027
1028 /**
1029 * Displays a summary of HPOS situation on this site.
1030 *
1031 * @since 8.6.0
1032 *
1033 * @param array $args Positional arguments passed to the command.
1034 * @param array $assoc_args Associative arguments (options) passed to the command.
1035 */
1036 public function status( array $args = array(), array $assoc_args = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- for backwards compat.
1037 $legacy_handler = wc_get_container()->get( LegacyDataHandler::class );
1038
1039 // translators: %s is either 'yes' or 'no'.
1040 WP_CLI::log( sprintf( __( 'HPOS enabled?: %s', 'woocommerce' ), wc_bool_to_string( $this->controller->custom_orders_table_usage_is_enabled() ) ) );
1041
1042 // translators: %s is either 'yes' or 'no'.
1043 WP_CLI::log( sprintf( __( 'Compatibility mode enabled?: %s', 'woocommerce' ), wc_bool_to_string( $this->synchronizer->data_sync_is_enabled() ) ) );
1044
1045 // translators: %d is an order count.
1046 WP_CLI::log( sprintf( __( 'Unsynced orders: %d', 'woocommerce' ), $this->synchronizer->get_current_orders_pending_sync_count() ) );
1047
1048 WP_CLI::log(
1049 sprintf(
1050 /* translators: %d is an order count. */
1051 __( 'Orders subject to cleanup: %d', 'woocommerce' ),
1052 ( $this->synchronizer->custom_orders_table_is_authoritative() && ! $this->synchronizer->data_sync_is_enabled() )
1053 ? $legacy_handler->count_orders_for_cleanup()
1054 : 0
1055 )
1056 );
1057 }
1058
1059 /**
1060 * Displays differences for an order between the HPOS and post datastore.
1061 *
1062 * ## OPTIONS
1063 *
1064 * <order_id>
1065 * :The ID of the order.
1066 *
1067 * [--format=<format>]
1068 * : Render output in a particular format.
1069 * ---
1070 * default: table
1071 * options:
1072 * - table
1073 * - csv
1074 * - json
1075 * - yaml
1076 * ---
1077 *
1078 * ## EXAMPLES
1079 *
1080 * # Find differences between datastores for order 123.
1081 * $ wp wc hpos diff 123
1082 *
1083 * # Find differences for order 123 and display as CSV.
1084 * $ wp wc hpos diff 123 --format=csv
1085 *
1086 * @since 8.6.0
1087 *
1088 * @param array $args Positional arguments passed to the command.
1089 * @param array $assoc_args Associative arguments (options) passed to the command.
1090 */
1091 public function diff( array $args = array(), array $assoc_args = array() ) {
1092 $id = absint( $args[0] );
1093
1094 try {
1095 $diff = wc_get_container()->get( LegacyDataHandler::class )->get_diff_for_order( $id );
1096 } catch ( \Exception $e ) {
1097 // translators: %1$d is an order ID, %2$s is an error message.
1098 WP_CLI::error( sprintf( __( 'An error occurred while computing a diff for order %1$d: %2$s', 'woocommerce' ), $id, $e->getMessage() ) );
1099 }
1100
1101 if ( ! $diff ) {
1102 WP_CLI::success( __( 'No differences found.', 'woocommerce' ) );
1103 return;
1104 }
1105
1106 // Format the diff array.
1107 $diff = array_map(
1108 function ( $key, $hpos_value, $cpt_value ) {
1109 // Format for dates.
1110 $hpos_value = is_a( $hpos_value, \WC_DateTime::class ) ? $hpos_value->format( DATE_ATOM ) : $hpos_value;
1111 $cpt_value = is_a( $cpt_value, \WC_DateTime::class ) ? $cpt_value->format( DATE_ATOM ) : $cpt_value;
1112
1113 // Format for NULL.
1114 $hpos_value = is_null( $hpos_value ) ? '' : $hpos_value;
1115 $cpt_value = is_null( $cpt_value ) ? '' : $cpt_value;
1116
1117 return array(
1118 'property' => $key,
1119 'hpos' => $hpos_value,
1120 'post' => $cpt_value,
1121 );
1122 },
1123 array_keys( $diff ),
1124 array_column( $diff, 0 ),
1125 array_column( $diff, 1 ),
1126 );
1127
1128 WP_CLI::warning(
1129 // translators: %d is an order ID.
1130 sprintf( __( 'Differences found for order %d:', 'woocommerce' ), $id )
1131 );
1132 WP_CLI\Utils\format_items(
1133 $assoc_args['format'] ?? 'table',
1134 $diff,
1135 array( 'property', 'hpos', 'post' )
1136 );
1137 }
1138
1139 /**
1140 * Backfills an order from either the HPOS or the posts datastore.
1141 *
1142 * ## OPTIONS
1143 *
1144 * <order_id>
1145 * : The ID of the order.
1146 *
1147 * --from=<datastore>
1148 * : Source datastore. Either 'hpos' or 'posts'.
1149 * ---
1150 * options:
1151 * - hpos
1152 * - posts
1153 * ---
1154 *
1155 * --to=<datastore>
1156 * : Destination datastore. Either 'hpos' or 'posts'.
1157 * ---
1158 * options:
1159 * - hpos
1160 * - posts
1161 * ---
1162 *
1163 * [--meta_keys=<meta_keys>]
1164 * : Comma-separated list of meta keys to backfill.
1165 *
1166 * [--props=<props>]
1167 * : Comma-separated list of order properties to backfill.
1168 *
1169 * @since 8.6.0
1170 *
1171 * @param array $args Positional arguments passed to the command.
1172 * @param array $assoc_args Associative arguments (options) passed to the command.
1173 */
1174 public function backfill( array $args = array(), array $assoc_args = array() ) {
1175 $legacy_handler = wc_get_container()->get( LegacyDataHandler::class );
1176
1177 $from = $assoc_args['from'] ?? '';
1178 $to = $assoc_args['to'] ?? '';
1179 $order_id = absint( $args[0] );
1180
1181 if ( ! $order_id ) {
1182 WP_CLI::error( __( 'Please provide a valid order ID.', 'woocommerce' ) );
1183 }
1184
1185 foreach ( array( 'from', 'to' ) as $datastore ) {
1186 if ( ! in_array( ${"$datastore"}, array( 'posts', 'hpos' ), true ) ) {
1187 // translators: %s is a shell argument representing a datastore name.
1188 WP_CLI::error( sprintf( __( '\'%s\' is not a valid datastore.', 'woocommerce' ), ${"$datastore"} ) );
1189 }
1190 }
1191
1192 if ( $from === $to ) {
1193 WP_CLI::error( __( 'Please use different source (--from) and destination (--to) datastores.', 'woocommerce' ) );
1194 }
1195
1196 $fields = array_intersect_key( $assoc_args, array_flip( array( 'meta_keys', 'props' ) ) );
1197 foreach ( $fields as &$field_names ) {
1198 $field_names = is_string( $field_names ) ? array_map( 'trim', explode( ',', $field_names ) ) : $field_names;
1199 $field_names = array_unique( array_filter( array_filter( $field_names, 'is_string' ) ) );
1200 }
1201
1202 try {
1203 $legacy_handler->backfill_order_to_datastore( $order_id, $from, $to, $fields );
1204 } catch ( \Exception $e ) {
1205 WP_CLI::error(
1206 sprintf(
1207 // translators: %1$d is an order ID, %2$s and %3$s are datastore names, %4$s is an error message.
1208 __( 'An error occurred while backfilling order %1$d from %2$s to %3$s: %4$s', 'woocommerce' ),
1209 $order_id,
1210 $from,
1211 $to,
1212 $e->getMessage()
1213 )
1214 );
1215 }
1216
1217 WP_CLI::success(
1218 sprintf(
1219 // translators: %1$d is an order ID, %2$s and %3$s are datastore names ("hpos" or "posts" for example).
1220 __( 'Order %1$d backfilled from %2$s to %3$s.', 'woocommerce' ),
1221 $order_id,
1222 $from,
1223 $to
1224 )
1225 );
1226 }
1227
1228 /**
1229 * Show the list of WooCommerce-aware plugins known to be compatible, incompatible or without compatibility declaration for HPOS. Note that inactive plugins will always be listed in the "uncertain" list.
1230 *
1231 * [--include-inactive]
1232 * : Include inactive plugins in the list.
1233 *
1234 * [--display-filenames]
1235 * : Print plugin file names instead of plugin names.
1236 *
1237 * @since 9.1.0
1238 *
1239 * @param array $args Positional arguments passed to the command.
1240 * @param array $assoc_args Associative arguments (options) passed to the command.
1241 */
1242 public function compatibility_info( array $args = array(), array $assoc_args = array() ): void {
1243 $container = wc_get_container();
1244 $feature_controller = $container->get( FeaturesController::class );
1245 $plugin_info = $feature_controller->get_compatible_plugins_for_feature( 'custom_order_tables', ! ( (bool) ( $assoc_args['include-inactive'] ?? null ) ) );
1246 $display_filenames = (bool) ( $assoc_args['display-filenames'] ?? null );
1247
1248 $compatibles = $this->get_printable_plugin_names( $plugin_info['compatible'], $display_filenames );
1249 $compatibles_count = count( $compatibles );
1250 $this->log(
1251 sprintf(
1252 // translators: $1$d = plugins count, %2$s = colon (if list follows) or empty.
1253 _n( "\n%%C%1\$d%%n compatible plugin found%2\$s", "\n%%C%1\$d%%n compatible plugins found%2\$s", $compatibles_count, 'woocommerce' ),
1254 $compatibles_count,
1255 $compatibles_count > 0 ? ":\n" : ''
1256 )
1257 );
1258 $this->print_plugin_names( $compatibles );
1259
1260 $incompatibles = $this->get_printable_plugin_names( $plugin_info['incompatible'], $display_filenames );
1261 $incompatibles_count = count( $incompatibles );
1262 $this->log(
1263 sprintf(
1264 // translators: $1$d = plugins count, %2$s = colon (if list follows) or empty.
1265 _n( "\n%%C%1\$d%%n incompatible plugin found%2\$s", "\n%%C%1\$d%%n incompatible plugins found%2\$s", $incompatibles_count, 'woocommerce' ),
1266 $incompatibles_count,
1267 $incompatibles_count > 0 ? ":\n" : ''
1268 )
1269 );
1270 $this->print_plugin_names( $incompatibles );
1271
1272 $uncertain = $this->get_printable_plugin_names( $plugin_info['uncertain'], $display_filenames );
1273 $uncertain_count = count( $uncertain );
1274 $this->log(
1275 sprintf(
1276 // translators: $1$d = plugins count, %2$s = colon (if list follows) or empty.
1277 _n( "\n%%C%1\$d%%n uncertain plugin found%2\$s", "\n%%C%1\$d%%n uncertain plugins found%2\$s", $uncertain_count, 'woocommerce' ),
1278 $uncertain_count,
1279 $uncertain_count > 0 ? ":\n" : ''
1280 )
1281 );
1282 $this->print_plugin_names( $uncertain );
1283 }
1284
1285 /**
1286 * Get the printable names for a set of plugins given their file names.
1287 *
1288 * @param array $plugins The plugin file names.
1289 * @param bool $display_filenames True to simply return the sorted list of plugin file names.
1290 * @return array A sorted array of plugin names or file names.
1291 */
1292 private function get_printable_plugin_names( array $plugins, bool $display_filenames ): array {
1293 if ( $display_filenames ) {
1294 sort( $plugins );
1295 return $plugins;
1296 }
1297
1298 $plugin_names = array_map(
1299 fn( $plugin_file ) => get_plugin_data( WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $plugin_file, false )['Name'] ?? $plugin_file,
1300 $plugins
1301 );
1302 sort( $plugin_names );
1303 return $plugin_names;
1304 }
1305
1306 /**
1307 * Print a list of plugin names.
1308 *
1309 * @param array $plugins The names to print.
1310 */
1311 private function print_plugin_names( array $plugins ): void {
1312 foreach ( $plugins as $plugin_file ) {
1313 $this->log( ' ' . $plugin_file );
1314 }
1315 }
1316
1317 /**
1318 * Show a log message using the WP_CLI text colorization feature.
1319 *
1320 * @param string $text Text to show.
1321 */
1322 private function log( string $text ) {
1323 WP_CLI::log( WP_CLI::colorize( $text ) );
1324 }
1325
1326 /**
1327 * Enables compatibility mode, which keeps the HPOS and posts datastore in sync.
1328 *
1329 * @since 9.1.0
1330 */
1331 public function enable_compat_mode(): void {
1332 $this->toggle_compat_mode( true );
1333 }
1334
1335 /**
1336 * Disables compatibility mode, which keeps the HPOS and posts datastore in sync.
1337 *
1338 * @since 9.1.0
1339 */
1340 public function disable_compat_mode(): void {
1341 $this->toggle_compat_mode( false );
1342 }
1343
1344 /**
1345 * Toggles compatibility mode on or off.
1346 *
1347 * @since 9.1.0
1348 *
1349 * @param bool $enabled TRUE to enable compatibility mode, FALSE to disable.
1350 */
1351 private function toggle_compat_mode( bool $enabled ): void {
1352 if ( ! $this->synchronizer->check_orders_table_exists() ) {
1353 if ( $enabled ) {
1354 $this->synchronizer->create_database_tables();
1355 } else {
1356 WP_CLI::error( __( 'HPOS tables do not exist.', 'woocommerce' ) );
1357 }
1358 }
1359
1360 $currently_enabled = $this->synchronizer->data_sync_is_enabled();
1361
1362 if ( $currently_enabled === $enabled ) {
1363 if ( $enabled ) {
1364 WP_CLI::warning( __( 'Compatibility mode is already enabled.', 'woocommerce' ) );
1365 } else {
1366 WP_CLI::warning( __( 'Compatibility mode is already disabled.', 'woocommerce' ) );
1367 }
1368
1369 return;
1370 }
1371
1372 update_option( $this->synchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, wc_bool_to_string( $enabled ) );
1373
1374 if ( $enabled ) {
1375 WP_CLI::success( __( 'Compatibility mode enabled.', 'woocommerce' ) );
1376 } else {
1377 WP_CLI::success( __( 'Compatibility mode disabled.', 'woocommerce' ) );
1378 }
1379 }
1380 }
1381