PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 11.0.0
WooCommerce v11.0.0
11.1.0-rc.2 11.1.0-rc.1 11.1.0-beta.2 11.1.0-beta.1 11.0.1 11.0.0 11.0.0-rc.3 11.0.0-rc.2 11.0.0-rc.1 11.0.0-beta.2 11.0.0-beta.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 1 month ago PostMetaToOrderMetaMigrator.php 3 years ago PostToOrderAddressTableMigrator.php 2 years ago PostToOrderOpTableMigrator.php 3 years ago PostToOrderTableMigrator.php 3 years ago PostsToOrdersMigrationController.php 1 year ago
CLIRunner.php
1385 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 'wc'
223 );
224 $batch_start_time = microtime( true );
225 $order_ids = $this->synchronizer->get_next_batch_to_process( $batch_size );
226 if ( count( $order_ids ) ) {
227 $this->synchronizer->process_batch( $order_ids );
228 }
229 $processed += count( $order_ids );
230 $batch_total_time = microtime( true ) - $batch_start_time;
231
232 WP_CLI::debug(
233 sprintf(
234 // Translators: %1$d is the batch number, %2$d is the number of processed orders and %3$d is the execution time in seconds.
235 __( 'Batch %1$d (%2$d orders) completed in %3$d seconds', 'woocommerce' ),
236 $batch_count,
237 count( $order_ids ),
238 $batch_total_time
239 ),
240 'wc'
241 );
242
243 ++$batch_count;
244 $total_time += $batch_total_time;
245
246 $progress->tick();
247
248 $orders_remaining = count( $this->synchronizer->get_next_batch_to_process( 1 ) ) > 0;
249 $order_count = $remaining_count - $batch_size;
250
251 $this->free_in_memory_usage();
252 }
253
254 $progress->finish();
255
256 // Issue a warning if no orders were migrated.
257 if ( ! $processed ) {
258 return WP_CLI::warning( __( 'No orders were synced.', 'woocommerce' ) );
259 }
260
261 WP_CLI::log( __( 'Sync completed.', 'woocommerce' ) );
262
263 return WP_CLI::success(
264 sprintf(
265 /* Translators: %1$d is the number of migrated orders and %2$d is the execution time in seconds. */
266 _n(
267 '%1$d order was synced in %2$d seconds.',
268 '%1$d orders were synced in %2$d seconds.',
269 $processed,
270 'woocommerce'
271 ),
272 $processed,
273 $total_time
274 )
275 );
276 }
277
278 /**
279 * [Deprecated] Use `wp wc hpos sync` instead.
280 * Copy order data into the postmeta table.
281 *
282 * Note that this could dramatically increase the size of your postmeta table, but is recommended
283 * if you wish to stop using the custom orders table plugin.
284 *
285 * ## OPTIONS
286 *
287 * [--batch-size=<batch-size>]
288 * : The number of orders to process in each batch. Passing a value of 0 will disable batching.
289 * ---
290 * default: 500
291 * ---
292 *
293 * ## EXAMPLES
294 *
295 * # Copy all order data into the post meta table, 500 posts at a time.
296 * wp wc cot migrate --batch-size=500
297 *
298 * @param array $args Positional arguments passed to the command.
299 * @param array $assoc_args Associative arguments (options) passed to the command.
300 */
301 public function migrate( array $args = array(), array $assoc_args = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- for backwards compat.
302 WP_CLI::log( __( 'Migrate command is deprecated. Please use `sync` instead.', 'woocommerce' ) );
303 }
304
305 /**
306 * Verify migrated order data with original posts data.
307 *
308 * ## OPTIONS
309 *
310 * [--batch-size=<batch-size>]
311 * : The number of orders to verify in each batch.
312 * ---
313 * default: 500
314 * ---
315 *
316 * [--start-from=<order_id>]
317 * : Order ID to start from.
318 * ---
319 * default: 0
320 * ---
321 *
322 * [--end-at=<order_id>]
323 * : Order ID to end at.
324 * ---
325 * default: -1
326 * ---
327 *
328 * [--verbose]
329 * : Whether to output errors as they happen in batch, or output them all together at the end.
330 * ---
331 * default: false
332 * ---
333 *
334 * [--order-types]
335 * : Comma-separated list of order types that needs to be verified. For example, --order-types=shop_order,shop_order_refund
336 * ---
337 * default: Output of function `wc_get_order_types( 'cot-migration' )`
338 *
339 * [--re-migrate]
340 * : 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.
341 * default: false
342 *
343 * ## EXAMPLES
344 *
345 * # Verify migrated order data, 500 orders at a time.
346 * wp wc hpos verify_cot_data --batch-size=500 --start-from=0 --end-at=10000
347 *
348 * @param array $args Positional arguments passed to the command.
349 * @param array $assoc_args Associative arguments (options) passed to the command.
350 */
351 public function verify_cot_data( $args = array(), $assoc_args = array() ) {
352 global $wpdb;
353
354 if ( ! $this->synchronizer->check_orders_table_exists() ) {
355 WP_CLI::error( __( 'Orders table does not exist.', 'woocommerce' ) );
356 return;
357 }
358
359 $assoc_args = wp_parse_args(
360 $assoc_args,
361 array(
362 'batch-size' => 500,
363 'start-from' => 0,
364 'end-at' => - 1,
365 'verbose' => false,
366 'order-types' => '',
367 're-migrate' => false,
368 )
369 );
370
371 $batch_count = 1;
372 $total_time = 0;
373 $failed_ids = array();
374 $processed = 0;
375 $order_id_start = (int) $assoc_args['start-from'];
376 $order_id_end = (int) $assoc_args['end-at'];
377 $order_id_end = -1 === $order_id_end ? PHP_INT_MAX : $order_id_end;
378 $batch_size = ( (int) $assoc_args['batch-size'] ) === 0 ? 500 : (int) $assoc_args['batch-size'];
379 $verbose = (bool) $assoc_args['verbose'];
380 $order_types = wc_get_order_types( 'cot-migration' );
381 $remigrate = (bool) $assoc_args['re-migrate'];
382 if ( ! empty( $assoc_args['order-types'] ) ) {
383 $passed_order_types = array_map( 'trim', explode( ',', $assoc_args['order-types'] ) );
384 $order_types = array_intersect( $order_types, $passed_order_types );
385 }
386
387 if ( 0 === count( $order_types ) ) {
388 return WP_CLI::error(
389 sprintf(
390 /* Translators: %s is the comma-separated list of order types. */
391 __( 'Passed order type does not match any registered order types. Following order types are registered: %s', 'woocommerce' ),
392 implode( ',', wc_get_order_types( 'cot-migration' ) )
393 )
394 );
395 }
396
397 $order_types_pl = implode( ',', array_fill( 0, count( $order_types ), '%s' ) );
398
399 $order_count = $this->get_verify_order_count( $order_id_start, $order_id_end, $order_types, false );
400
401 $progress = WP_CLI\Utils\make_progress_bar( 'Order Data Verification', $order_count / $batch_size );
402
403 $error_processing = false;
404
405 if ( ! $order_count ) {
406 return WP_CLI::warning( __( 'There are no orders to verify, aborting.', 'woocommerce' ) );
407 }
408
409 while ( $order_count > 0 ) {
410 WP_CLI::debug(
411 sprintf(
412 /* Translators: %1$d is the batch number, %2$d is the batch size. */
413 __( 'Beginning verification for batch #%1$d (%2$d orders/batch).', 'woocommerce' ),
414 $batch_count,
415 $batch_size
416 ),
417 'wc'
418 );
419
420 // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Inputs are prepared.
421 $order_ids = $wpdb->get_col(
422 $wpdb->prepare(
423 "SELECT ID FROM $wpdb->posts WHERE post_type in ( $order_types_pl ) AND ID >= %d AND ID <= %d ORDER BY ID ASC LIMIT %d",
424 array_merge(
425 $order_types,
426 array(
427 $order_id_start,
428 $order_id_end,
429 $batch_size,
430 )
431 )
432 )
433 );
434 // phpcs:enable
435 $batch_start_time = microtime( true );
436 $failed_ids_in_current_batch = $this->post_to_cot_migrator->verify_migrated_orders( $order_ids );
437 $failed_ids_in_current_batch = $this->verify_meta_data( $order_ids, $failed_ids_in_current_batch );
438 $failed_ids = $verbose ? array() : $failed_ids + $failed_ids_in_current_batch;
439 $error_processing = $error_processing || ! empty( $failed_ids_in_current_batch );
440 $processed += count( $order_ids );
441 $batch_total_time = microtime( true ) - $batch_start_time;
442 ++$batch_count;
443 $total_time += $batch_total_time;
444
445 if ( count( $failed_ids_in_current_batch ) > 0 ) {
446 if ( $verbose ) {
447 $errors = wp_json_encode( $failed_ids_in_current_batch, JSON_PRETTY_PRINT );
448 WP_CLI::warning(
449 sprintf(
450 /* Translators: %1$d is number of errors and %2$s is the formatted array of order IDs. */
451 _n(
452 '%1$d error found: %2$s. Please review the error above.',
453 '%1$d errors found: %2$s. Please review the errors above.',
454 count( $failed_ids_in_current_batch ),
455 'woocommerce'
456 ),
457 count( $failed_ids_in_current_batch ),
458 $errors
459 )
460 );
461 }
462
463 if ( $remigrate ) {
464 $failed_ids = $failed_ids ? array_diff_key( $failed_ids, $failed_ids_in_current_batch ) : array();
465 $error_processing = ( ! $verbose ) && $failed_ids;
466
467 $verbose && WP_CLI::warning( sprintf( __( 'Attempting to remigrate...', 'woocommerce' ) ) );
468
469 $failed_ids_in_current_batch_keys = array_keys( $failed_ids_in_current_batch );
470 $this->synchronizer->process_batch( $failed_ids_in_current_batch_keys );
471 $errors_in_remigrate_batch = $this->post_to_cot_migrator->verify_migrated_orders( $failed_ids_in_current_batch_keys );
472 $errors_in_remigrate_batch = $this->verify_meta_data( $failed_ids_in_current_batch_keys, $errors_in_remigrate_batch );
473
474 if ( count( $errors_in_remigrate_batch ) > 0 ) {
475 $error_processing = true;
476 $formatted_errors = wp_json_encode( $errors_in_remigrate_batch, JSON_PRETTY_PRINT );
477
478 if ( $verbose ) {
479 WP_CLI::warning(
480 sprintf(
481 /* Translators: %1$d is number of errors and %2$s is the formatted array of order IDs. */
482 _n(
483 '%1$d error found: %2$s when re-migrating order. Please review the error above.',
484 '%1$d errors found: %2$s when re-migrating orders. Please review the errors above.',
485 count( $errors_in_remigrate_batch ),
486 'woocommerce'
487 ),
488 count( $errors_in_remigrate_batch ),
489 $formatted_errors
490 )
491 );
492 } else {
493 array_walk(
494 $errors_in_remigrate_batch,
495 function ( &$errors_for_order ) {
496 $errors_for_order[] = array( 'remigrate_failed' => true );
497 }
498 );
499 $failed_ids = $failed_ids + $errors_in_remigrate_batch;
500 }
501 } else {
502 $verbose && WP_CLI::warning( 'Re-migration successful.', 'woocommerce' );
503 }
504 }
505 }
506
507 $progress->tick();
508
509 WP_CLI::debug(
510 sprintf(
511 /* Translators: %1$d is the batch number, %2$d is time taken to process batch. */
512 __( 'Batch %1$d (%2$d orders) completed in %3$d seconds.', 'woocommerce' ),
513 $batch_count,
514 count( $order_ids ),
515 $batch_total_time
516 ),
517 'wc'
518 );
519
520 $order_id_start = max( $order_ids ) + 1;
521 $remaining_count = $this->get_verify_order_count( $order_id_start, $order_id_end, $order_types, false );
522 if ( $remaining_count === $order_count ) {
523 return WP_CLI::error( __( 'Infinite loop detected, aborting. No errors found.', 'woocommerce' ) );
524 }
525 $order_count = $remaining_count;
526 }
527
528 $progress->finish();
529 WP_CLI::log( __( 'Verification completed.', 'woocommerce' ) );
530
531 if ( ! $error_processing ) {
532 return WP_CLI::success(
533 sprintf(
534 /* Translators: %1$d is the number of migrated orders and %2$d is time taken. */
535 _n(
536 '%1$d order was verified in %2$d seconds.',
537 '%1$d orders were verified in %2$d seconds.',
538 $processed,
539 'woocommerce'
540 ),
541 $processed,
542 $total_time
543 )
544 );
545 } else {
546 return WP_CLI::error(
547 sprintf(
548 '%1$s %2$s',
549 sprintf(
550 /* Translators: %1$d is the number of migrated orders and %2$d is the execution time in seconds. */
551 _n(
552 '%1$d order was verified in %2$d seconds.',
553 '%1$d orders were verified in %2$d seconds.',
554 $processed,
555 'woocommerce'
556 ),
557 $processed,
558 $total_time
559 ),
560 $failed_ids
561 ? sprintf(
562 /* Translators: %1$d is number of errors and %2$s is the formatted array of order IDs. */
563 _n(
564 '%1$d error found: %2$s. Please review the error above.',
565 '%1$d errors found: %2$s. Please review the errors above.',
566 count( $failed_ids ),
567 'woocommerce'
568 ),
569 count( $failed_ids ),
570 wp_json_encode( $failed_ids, JSON_PRETTY_PRINT )
571 )
572 : __( 'Please review the errors above.', 'woocommerce' )
573 )
574 );
575 }
576 }
577
578 /**
579 * Helper method to get count for orders needing verification.
580 *
581 * @param int $order_id_start Order ID to start from.
582 * @param int $order_id_end Order ID to end at.
583 * @param array $order_types List of order types to verify.
584 * @param bool $log Whether to also log an error message.
585 *
586 * @return int Order count.
587 */
588 private function get_verify_order_count( int $order_id_start, int $order_id_end, array $order_types, bool $log = true ): int {
589 global $wpdb;
590
591 $order_types_placeholder = implode( ',', array_fill( 0, count( $order_types ), '%s' ) );
592
593 // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Inputs are prepared.
594 $order_count = (int) $wpdb->get_var(
595 $wpdb->prepare(
596 "SELECT COUNT(*) FROM $wpdb->posts WHERE post_type in ($order_types_placeholder) AND ID >= %d AND ID <= %d",
597 array_merge(
598 $order_types,
599 array(
600 $order_id_start,
601 $order_id_end,
602 )
603 )
604 )
605 );
606 // phpcs:enable
607
608 if ( $log ) {
609 WP_CLI::log(
610 sprintf(
611 /* Translators: %1$d is the number of orders to be verified. */
612 _n(
613 'There is %1$d order to be verified.',
614 'There are %1$d orders to be verified.',
615 $order_count,
616 'woocommerce'
617 ),
618 $order_count
619 )
620 );
621 }
622
623 return $order_count;
624 }
625
626 /**
627 * Verify meta data as part of verifying the order object.
628 *
629 * @param array $order_ids Order IDs.
630 * @param array $failed_ids Array for storing failed IDs.
631 *
632 * @return array Failed IDs with meta details.
633 */
634 private function verify_meta_data( array $order_ids, array $failed_ids ): array {
635 $meta_keys_to_ignore = $this->synchronizer->get_ignored_order_props();
636
637 global $wpdb;
638 if ( ! count( $order_ids ) ) {
639 return array();
640 }
641 $excluded_columns = array_merge(
642 $this->post_to_cot_migrator->get_migrated_meta_keys(),
643 $meta_keys_to_ignore
644 );
645 $excluded_columns_placeholder = implode( ', ', array_fill( 0, count( $excluded_columns ), '%s' ) );
646 $order_ids_placeholder = implode( ', ', array_fill( 0, count( $order_ids ), '%d' ) );
647 $meta_table = OrdersTableDataStore::get_meta_table_name();
648
649 // 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.
650 $query = $wpdb->prepare(
651 "
652 SELECT {$wpdb->postmeta}.post_id as entity_id, {$wpdb->postmeta}.meta_key, {$wpdb->postmeta}.meta_value
653 FROM $wpdb->postmeta
654 WHERE
655 {$wpdb->postmeta}.post_id in ( $order_ids_placeholder ) AND
656 {$wpdb->postmeta}.meta_key not in ( $excluded_columns_placeholder )
657 ORDER BY {$wpdb->postmeta}.post_id ASC, {$wpdb->postmeta}.meta_key ASC;
658 ",
659 array_merge(
660 $order_ids,
661 $excluded_columns
662 )
663 );
664 $source_data = $wpdb->get_results( $query, ARRAY_A );
665 // phpcs:enable
666
667 $normalized_source_data = $this->normalize_raw_meta_data( $source_data );
668
669 // 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.
670 $migrated_query = $wpdb->prepare(
671 "
672 SELECT $meta_table.order_id as entity_id, $meta_table.meta_key, $meta_table.meta_value
673 FROM $meta_table
674 WHERE
675 $meta_table.order_id in ( $order_ids_placeholder )
676 ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
677 ",
678 $order_ids
679 );
680 $migrated_data = $wpdb->get_results( $migrated_query, ARRAY_A );
681 // phpcs:enable
682
683 $normalized_migrated_meta_data = $this->normalize_raw_meta_data( $migrated_data );
684
685 foreach ( $normalized_source_data as $order_id => $meta ) {
686 foreach ( $meta as $meta_key => $values ) {
687 $migrated_meta_values = isset( $normalized_migrated_meta_data[ $order_id ][ $meta_key ] ) ? $normalized_migrated_meta_data[ $order_id ][ $meta_key ] : array();
688 $diff = array_diff( $values, $migrated_meta_values );
689
690 if ( count( $diff ) ) {
691 if ( ! isset( $failed_ids[ $order_id ] ) ) {
692 $failed_ids[ $order_id ] = array();
693 }
694 $failed_ids[ $order_id ][] = array(
695 'order_id' => $order_id,
696 'meta_key' => $meta_key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Not a meta query.
697 'orig_meta_values' => $values,
698 'new_meta_values' => $migrated_meta_values,
699 );
700 }
701 }
702 }
703
704 return $failed_ids;
705 }
706
707 /**
708 * Helper method to normalize response from meta queries into order_id > meta_key > meta_values.
709 *
710 * @param array $data Data fetched from meta queries.
711 *
712 * @return array Normalized data.
713 */
714 private function normalize_raw_meta_data( array $data ): array {
715 $clubbed_data = array();
716 foreach ( $data as $row ) {
717 if ( ! isset( $clubbed_data[ $row['entity_id'] ] ) ) {
718 $clubbed_data[ $row['entity_id'] ] = array();
719 }
720 if ( ! isset( $clubbed_data[ $row['entity_id'] ][ $row['meta_key'] ] ) ) {
721 $clubbed_data[ $row['entity_id'] ][ $row['meta_key'] ] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Not a meta query.
722 }
723 $clubbed_data[ $row['entity_id'] ][ $row['meta_key'] ][] = $row['meta_value'];
724 }
725 return $clubbed_data;
726 }
727
728 /**
729 * 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.
730 *
731 * ## OPTIONS
732 *
733 * [--for-new-shop]
734 * : Enable only if this is a new shop, irrespective of whether tables are in sync.
735 * ---
736 * default: false
737 * ---
738 *
739 * [--with-sync]
740 * : Also enables sync (if it's currently not enabled).
741 * ---
742 * default: false
743 * ---
744 *
745 * [--ignore-plugin-compatibility]
746 * : Enable even if there are active plugins that are incompatible with HPOS.
747 *
748 * ### EXAMPLES
749 *
750 * # Enable HPOS on new shops.
751 * wp wc hpos enable --for-new-shop
752 *
753 * @param array $args Positional arguments passed to the command.
754 * @param array $assoc_args Associative arguments (options) passed to the command.
755 *
756 * @return void
757 */
758 public function enable( array $args = array(), array $assoc_args = array() ) {
759 $assoc_args = wp_parse_args(
760 $assoc_args,
761 array(
762 'for-new-shop' => false,
763 'with-sync' => false,
764 'ignore-plugin-compatibility' => false,
765 )
766 );
767
768 $enable_hpos = true;
769 WP_CLI::log( __( 'Running pre-enable checks...', 'woocommerce' ) );
770
771 $is_new_shop = \WC_Install::is_new_install();
772 if ( $assoc_args['for-new-shop'] && ! $is_new_shop ) {
773 WP_CLI::error( __( '[Failed] This is not a new shop, but --for-new-shop flag was passed.', 'woocommerce' ) );
774 }
775
776 $container = wc_get_container();
777 /** Feature controller instance @var FeaturesController $feature_controller */
778 $feature_controller = $container->get( FeaturesController::class );
779 if ( ! $assoc_args['ignore-plugin-compatibility'] ) {
780 $compatibility_info = $feature_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
781 /** Plugin util instance @var PluginUtil $plugin_util */
782 $plugin_util = $container->get( PluginUtil::class );
783 $incompatibles = $plugin_util->get_items_considered_incompatible( 'custom_order_tables', $compatibility_info );
784 if ( count( $incompatibles ) > 0 ) {
785 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' ) );
786 $enable_hpos = false;
787 }
788 }
789
790 /** DataSynchronizer instance @var DataSynchronizer $data_synchronizer */
791 $data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
792 $pending_orders = $data_synchronizer->get_total_pending_count();
793 $table_exists = $data_synchronizer->check_orders_table_exists();
794
795 if ( ! $table_exists ) {
796 WP_CLI::warning( __( 'Orders table does not exist. Creating...', 'woocommerce' ) );
797 if ( $is_new_shop || 0 === $pending_orders ) {
798 $data_synchronizer->create_database_tables();
799 if ( $data_synchronizer->check_orders_table_exists() ) {
800 WP_CLI::log( __( 'Orders table created.', 'woocommerce' ) );
801 $table_exists = true;
802 } else {
803 WP_CLI::warning( __( '[Failed] Orders table could not be created.', 'woocommerce' ) );
804 $enable_hpos = false;
805 }
806 } else {
807 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' ) );
808 $enable_hpos = false;
809 }
810 }
811
812 if ( $pending_orders > 0 ) {
813 WP_CLI::warning(
814 sprintf(
815 // translators: %s is the command to run (wp wc cot sync).
816 __( '[Failed] There are orders pending sync. Please run `%s` to sync pending orders.', 'woocommerce' ),
817 'wp wc hpos sync',
818 )
819 );
820 $enable_hpos = false;
821 }
822
823 if ( $assoc_args['with-sync'] && $table_exists ) {
824 $this->toggle_compat_mode( true );
825 }
826
827 if ( ! $enable_hpos ) {
828 WP_CLI::error( __( 'HPOS pre-checks failed, please see the errors above', 'woocommerce' ) );
829 return;
830 }
831
832 /** CustomOrdersTableController instance @var CustomOrdersTableController $cot_status */
833 $cot_status = wc_get_container()->get( CustomOrdersTableController::class );
834 if ( $cot_status->custom_orders_table_usage_is_enabled() ) {
835 WP_CLI::warning( __( 'HPOS is already enabled.', 'woocommerce' ) );
836 } else {
837 $feature_controller->change_feature_enable( 'custom_order_tables', true );
838 if ( $cot_status->custom_orders_table_usage_is_enabled() ) {
839 WP_CLI::success( __( 'HPOS enabled.', 'woocommerce' ) );
840 } else {
841 WP_CLI::error( __( 'HPOS could not be enabled.', 'woocommerce' ) );
842 }
843 }
844 }
845
846 /**
847 * Disables custom order tables (HPOS) and posts to authoritative if HPOS and post tables are in sync.
848 *
849 * ## OPTIONS
850 *
851 * [--with-sync]
852 * : Also disables sync (if it's currently enabled).
853 * ---
854 * default: false
855 * ---
856 *
857 * ### EXAMPLES
858 *
859 * # Disable HPOS.
860 * wp wc hpos disable
861 *
862 * @param array $args Positional arguments passed to the command.
863 * @param array $assoc_args Associative arguments (options) passed to the command.
864 */
865 public function disable( $args, $assoc_args ) {
866 $assoc_args = wp_parse_args(
867 $assoc_args,
868 array(
869 'with-sync' => false,
870 )
871 );
872
873 WP_CLI::log( __( 'Running pre-disable checks...', 'woocommerce' ) );
874
875 /** DataSynchronizer instance @var DataSynchronizer $data_synchronizer */
876 $data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
877 $pending_orders = $data_synchronizer->get_total_pending_count();
878 if ( $pending_orders > 0 ) {
879 return WP_CLI::error(
880 sprintf(
881 // translators: %s is the command to run (wp wc cot sync).
882 __( '[Failed] There are orders pending sync. Please run `%s` to sync pending orders.', 'woocommerce' ),
883 'wp wc hpos sync',
884 )
885 );
886 }
887
888 /** FeaturesController instance @var FeaturesController $feature_controller */
889 $feature_controller = wc_get_container()->get( FeaturesController::class );
890
891 /** CustomOrdersTableController instance @var CustomOrdersTableController $cot_status */
892 $cot_status = wc_get_container()->get( CustomOrdersTableController::class );
893 if ( ! $cot_status->custom_orders_table_usage_is_enabled() ) {
894 WP_CLI::warning( __( 'HPOS is already disabled.', 'woocommerce' ) );
895 } else {
896 $feature_controller->change_feature_enable( 'custom_order_tables', false );
897 if ( $cot_status->custom_orders_table_usage_is_enabled() ) {
898 return WP_CLI::warning( __( 'HPOS could not be disabled.', 'woocommerce' ) );
899 } else {
900 WP_CLI::success( __( 'HPOS disabled.', 'woocommerce' ) );
901 }
902 }
903
904 if ( $assoc_args['with-sync'] ) {
905 $this->toggle_compat_mode( false );
906 }
907 }
908
909 /**
910 * When HPOS is enabled, this command lets you remove redundant data from the postmeta table for migrated orders.
911 *
912 * ## OPTIONS
913 *
914 * <all|id|range>...
915 * : ID or range of orders to clean up.
916 *
917 * [--batch-size=<batch-size>]
918 * : Number of orders to process per batch. Applies only to cleaning up of 'all' orders.
919 * ---
920 * default: 500
921 * ---
922 *
923 * [--force]
924 * : When true, post meta will be cleaned up even if the post appears to have been updated more recently than the order.
925 * ---
926 * default: false
927 * ---
928 *
929 * ## EXAMPLES
930 *
931 * # Cleanup post data for order 314.
932 * $ wp wc hpos cleanup 314
933 *
934 * # Cleanup postmeta for orders with IDs between 10 and 100 and order 314.
935 * $ wp wc hpos cleanup 10-100 314
936 *
937 * # Cleanup postmeta for all orders.
938 * wp wc hpos cleanup all
939 *
940 * # Cleanup postmeta for all orders with a batch size of 200 (instead of the default 500).
941 * wp wc hpos cleanup all --batch-size=200
942 *
943 * @param array $args Positional arguments passed to the command.
944 * @param array $assoc_args Associative arguments (options) passed to the command.
945 * @return void
946 */
947 public function cleanup_post_data( array $args = array(), array $assoc_args = array() ) {
948 if ( ! $this->synchronizer->custom_orders_table_is_authoritative() || $this->synchronizer->data_sync_is_enabled() ) {
949 WP_CLI::error( __( 'Cleanup can only be performed when HPOS is active and compatibility mode is disabled.', 'woocommerce' ) );
950 }
951 $handler = wc_get_container()->get( LegacyDataHandler::class );
952
953 $all_orders = 'all' === $args[0];
954 $force = (bool) ( $assoc_args['force'] ?? false );
955 $q_order_ids = $all_orders ? array() : $args;
956 $q_limit = $all_orders ? absint( $assoc_args['batch-size'] ?? 500 ) : 0; // Limit per batch.
957
958 $order_count = $handler->count_orders_for_cleanup( $q_order_ids );
959 if ( ! $order_count ) {
960 WP_CLI::warning( __( 'No orders to cleanup.', 'woocommerce' ) );
961 return;
962 }
963
964 $progress = WP_CLI\Utils\make_progress_bar( __( 'HPOS cleanup', 'woocommerce' ), $order_count );
965 $count = 0;
966 $failed_ids = array();
967
968 // translators: %d is the number of orders to clean up.
969 WP_CLI::log( sprintf( _n( 'Starting cleanup for %d order...', 'Starting cleanup for %d orders...', $order_count, 'woocommerce' ), $order_count ) );
970
971 do {
972 $failed_ids_in_batch = array();
973 $order_ids = $handler->get_orders_for_cleanup( $q_order_ids, $q_limit );
974
975 if ( $failed_ids && empty( array_diff( $order_ids, $failed_ids ) ) ) {
976 break;
977 }
978
979 $order_ids = array_diff( $order_ids, $failed_ids ); // Do not reattempt IDs that have already failed.
980
981 foreach ( $order_ids as $order_id ) {
982 try {
983 $handler->cleanup_post_data( $order_id, $force );
984 ++$count;
985
986 // translators: %d is an order ID.
987 WP_CLI::debug( sprintf( __( 'Cleanup completed for order %d.', 'woocommerce' ), $order_id ), 'wc' );
988 } catch ( \Exception $e ) {
989 // translators: %1$d is an order ID, %2$s is an error message.
990 WP_CLI::warning( sprintf( __( 'An error occurred while cleaning up order %1$d: %2$s', 'woocommerce' ), $order_id, $e->getMessage() ) );
991 $failed_ids_in_batch[] = $order_id;
992 }
993
994 $progress->tick();
995 }
996
997 $failed_ids = array_merge( $failed_ids, $failed_ids_in_batch );
998
999 if ( ! $all_orders ) {
1000 break;
1001 }
1002
1003 if ( $failed_ids_in_batch && ! array_diff( $order_ids, $failed_ids_in_batch ) ) {
1004 WP_CLI::warning( __( 'Failed to clean up all orders in a batch. Aborting.', 'woocommerce' ) );
1005 break;
1006 }
1007
1008 $this->free_in_memory_usage();
1009 } while ( $order_ids );
1010
1011 $progress->finish();
1012
1013 if ( $failed_ids ) {
1014 return WP_CLI::error(
1015 sprintf(
1016 // translators: %d is the number of orders that were cleaned up.
1017 _n( 'Cleanup completed for %d order. Review errors above.', 'Cleanup completed for %d orders. Review errors above.', $count, 'woocommerce' ),
1018 $count
1019 )
1020 );
1021 }
1022
1023 WP_CLI::success(
1024 sprintf(
1025 // translators: %d is the number of orders that were cleaned up.
1026 _n( 'Cleanup completed for %d order.', 'Cleanup completed for %d orders.', $count, 'woocommerce' ),
1027 $count
1028 )
1029 );
1030 }
1031
1032 /**
1033 * Displays a summary of HPOS situation on this site.
1034 *
1035 * @since 8.6.0
1036 *
1037 * @param array $args Positional arguments passed to the command.
1038 * @param array $assoc_args Associative arguments (options) passed to the command.
1039 */
1040 public function status( array $args = array(), array $assoc_args = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- for backwards compat.
1041 $legacy_handler = wc_get_container()->get( LegacyDataHandler::class );
1042
1043 // translators: %s is either 'yes' or 'no'.
1044 WP_CLI::log( sprintf( __( 'HPOS enabled?: %s', 'woocommerce' ), wc_bool_to_string( $this->controller->custom_orders_table_usage_is_enabled() ) ) );
1045
1046 // translators: %s is either 'yes' or 'no'.
1047 WP_CLI::log( sprintf( __( 'Compatibility mode enabled?: %s', 'woocommerce' ), wc_bool_to_string( $this->synchronizer->data_sync_is_enabled() ) ) );
1048
1049 // translators: %d is an order count.
1050 WP_CLI::log( sprintf( __( 'Unsynced orders: %d', 'woocommerce' ), $this->synchronizer->get_current_orders_pending_sync_count() ) );
1051
1052 WP_CLI::log(
1053 sprintf(
1054 /* translators: %d is an order count. */
1055 __( 'Orders subject to cleanup: %d', 'woocommerce' ),
1056 ( $this->synchronizer->custom_orders_table_is_authoritative() && ! $this->synchronizer->data_sync_is_enabled() )
1057 ? $legacy_handler->count_orders_for_cleanup()
1058 : 0
1059 )
1060 );
1061 }
1062
1063 /**
1064 * Displays differences for an order between the HPOS and post datastore.
1065 *
1066 * ## OPTIONS
1067 *
1068 * <order_id>
1069 * :The ID of the order.
1070 *
1071 * [--format=<format>]
1072 * : Render output in a particular format.
1073 * ---
1074 * default: table
1075 * options:
1076 * - table
1077 * - csv
1078 * - json
1079 * - yaml
1080 * ---
1081 *
1082 * ## EXAMPLES
1083 *
1084 * # Find differences between datastores for order 123.
1085 * $ wp wc hpos diff 123
1086 *
1087 * # Find differences for order 123 and display as CSV.
1088 * $ wp wc hpos diff 123 --format=csv
1089 *
1090 * @since 8.6.0
1091 *
1092 * @param array $args Positional arguments passed to the command.
1093 * @param array $assoc_args Associative arguments (options) passed to the command.
1094 */
1095 public function diff( array $args = array(), array $assoc_args = array() ) {
1096 $id = absint( $args[0] );
1097
1098 try {
1099 $diff = wc_get_container()->get( LegacyDataHandler::class )->get_diff_for_order( $id );
1100 } catch ( \Exception $e ) {
1101 // translators: %1$d is an order ID, %2$s is an error message.
1102 WP_CLI::error( sprintf( __( 'An error occurred while computing a diff for order %1$d: %2$s', 'woocommerce' ), $id, $e->getMessage() ) );
1103 }
1104
1105 if ( ! $diff ) {
1106 WP_CLI::success( __( 'No differences found.', 'woocommerce' ) );
1107 return;
1108 }
1109
1110 // Format the diff array.
1111 $diff = array_map(
1112 function ( $key, $hpos_value, $cpt_value ) {
1113 // Format for dates.
1114 $hpos_value = is_a( $hpos_value, \WC_DateTime::class ) ? $hpos_value->format( DATE_ATOM ) : $hpos_value;
1115 $cpt_value = is_a( $cpt_value, \WC_DateTime::class ) ? $cpt_value->format( DATE_ATOM ) : $cpt_value;
1116
1117 // Format for NULL.
1118 $hpos_value = is_null( $hpos_value ) ? '' : $hpos_value;
1119 $cpt_value = is_null( $cpt_value ) ? '' : $cpt_value;
1120
1121 return array(
1122 'property' => $key,
1123 'hpos' => $hpos_value,
1124 'post' => $cpt_value,
1125 );
1126 },
1127 array_keys( $diff ),
1128 array_column( $diff, 0 ),
1129 array_column( $diff, 1 ),
1130 );
1131
1132 WP_CLI::warning(
1133 // translators: %d is an order ID.
1134 sprintf( __( 'Differences found for order %d:', 'woocommerce' ), $id )
1135 );
1136 WP_CLI\Utils\format_items(
1137 $assoc_args['format'] ?? 'table',
1138 $diff,
1139 array( 'property', 'hpos', 'post' )
1140 );
1141 }
1142
1143 /**
1144 * Backfills an order from either the HPOS or the posts datastore.
1145 *
1146 * ## OPTIONS
1147 *
1148 * <order_id>
1149 * : The ID of the order.
1150 *
1151 * --from=<datastore>
1152 * : Source datastore. Either 'hpos' or 'posts'.
1153 * ---
1154 * options:
1155 * - hpos
1156 * - posts
1157 * ---
1158 *
1159 * --to=<datastore>
1160 * : Destination datastore. Either 'hpos' or 'posts'.
1161 * ---
1162 * options:
1163 * - hpos
1164 * - posts
1165 * ---
1166 *
1167 * [--meta_keys=<meta_keys>]
1168 * : Comma-separated list of meta keys to backfill.
1169 *
1170 * [--props=<props>]
1171 * : Comma-separated list of order properties to backfill.
1172 *
1173 * @since 8.6.0
1174 *
1175 * @param array $args Positional arguments passed to the command.
1176 * @param array $assoc_args Associative arguments (options) passed to the command.
1177 */
1178 public function backfill( array $args = array(), array $assoc_args = array() ) {
1179 $legacy_handler = wc_get_container()->get( LegacyDataHandler::class );
1180
1181 $from = $assoc_args['from'] ?? '';
1182 $to = $assoc_args['to'] ?? '';
1183 $order_id = absint( $args[0] );
1184
1185 if ( ! $order_id ) {
1186 WP_CLI::error( __( 'Please provide a valid order ID.', 'woocommerce' ) );
1187 }
1188
1189 foreach ( array( 'from', 'to' ) as $datastore ) {
1190 if ( ! in_array( ${"$datastore"}, array( 'posts', 'hpos' ), true ) ) {
1191 // translators: %s is a shell argument representing a datastore name.
1192 WP_CLI::error( sprintf( __( '\'%s\' is not a valid datastore.', 'woocommerce' ), ${"$datastore"} ) );
1193 }
1194 }
1195
1196 if ( $from === $to ) {
1197 WP_CLI::error( __( 'Please use different source (--from) and destination (--to) datastores.', 'woocommerce' ) );
1198 }
1199
1200 $fields = array_intersect_key( $assoc_args, array_flip( array( 'meta_keys', 'props' ) ) );
1201 foreach ( $fields as &$field_names ) {
1202 $field_names = is_string( $field_names ) ? array_map( 'trim', explode( ',', $field_names ) ) : $field_names;
1203 $field_names = array_unique( array_filter( array_filter( $field_names, 'is_string' ) ) );
1204 }
1205
1206 try {
1207 $legacy_handler->backfill_order_to_datastore( $order_id, $from, $to, $fields );
1208 } catch ( \Exception $e ) {
1209 WP_CLI::error(
1210 sprintf(
1211 // translators: %1$d is an order ID, %2$s and %3$s are datastore names, %4$s is an error message.
1212 __( 'An error occurred while backfilling order %1$d from %2$s to %3$s: %4$s', 'woocommerce' ),
1213 $order_id,
1214 $from,
1215 $to,
1216 $e->getMessage()
1217 )
1218 );
1219 }
1220
1221 WP_CLI::success(
1222 sprintf(
1223 // translators: %1$d is an order ID, %2$s and %3$s are datastore names ("hpos" or "posts" for example).
1224 __( 'Order %1$d backfilled from %2$s to %3$s.', 'woocommerce' ),
1225 $order_id,
1226 $from,
1227 $to
1228 )
1229 );
1230 }
1231
1232 /**
1233 * 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.
1234 *
1235 * [--include-inactive]
1236 * : Include inactive plugins in the list.
1237 *
1238 * [--display-filenames]
1239 * : Print plugin file names instead of plugin names.
1240 *
1241 * @since 9.1.0
1242 *
1243 * @param array $args Positional arguments passed to the command.
1244 * @param array $assoc_args Associative arguments (options) passed to the command.
1245 */
1246 public function compatibility_info( array $args = array(), array $assoc_args = array() ): void {
1247 $container = wc_get_container();
1248 $feature_controller = $container->get( FeaturesController::class );
1249 $plugin_info = $feature_controller->get_compatible_plugins_for_feature( 'custom_order_tables', ! ( (bool) ( $assoc_args['include-inactive'] ?? null ) ) );
1250 $display_filenames = (bool) ( $assoc_args['display-filenames'] ?? null );
1251
1252 $compatibles = $this->get_printable_plugin_names( $plugin_info['compatible'], $display_filenames );
1253 $compatibles_count = count( $compatibles );
1254 $this->log(
1255 sprintf(
1256 // translators: $1$d = plugins count, %2$s = colon (if list follows) or empty.
1257 _n( "\n%%C%1\$d%%n compatible plugin found%2\$s", "\n%%C%1\$d%%n compatible plugins found%2\$s", $compatibles_count, 'woocommerce' ),
1258 $compatibles_count,
1259 $compatibles_count > 0 ? ":\n" : ''
1260 )
1261 );
1262 $this->print_plugin_names( $compatibles );
1263
1264 $incompatibles = $this->get_printable_plugin_names( $plugin_info['incompatible'], $display_filenames );
1265 $incompatibles_count = count( $incompatibles );
1266 $this->log(
1267 sprintf(
1268 // translators: $1$d = plugins count, %2$s = colon (if list follows) or empty.
1269 _n( "\n%%C%1\$d%%n incompatible plugin found%2\$s", "\n%%C%1\$d%%n incompatible plugins found%2\$s", $incompatibles_count, 'woocommerce' ),
1270 $incompatibles_count,
1271 $incompatibles_count > 0 ? ":\n" : ''
1272 )
1273 );
1274 $this->print_plugin_names( $incompatibles );
1275
1276 $uncertain = $this->get_printable_plugin_names( $plugin_info['uncertain'], $display_filenames );
1277 $uncertain_count = count( $uncertain );
1278 $this->log(
1279 sprintf(
1280 // translators: $1$d = plugins count, %2$s = colon (if list follows) or empty.
1281 _n( "\n%%C%1\$d%%n uncertain plugin found%2\$s", "\n%%C%1\$d%%n uncertain plugins found%2\$s", $uncertain_count, 'woocommerce' ),
1282 $uncertain_count,
1283 $uncertain_count > 0 ? ":\n" : ''
1284 )
1285 );
1286 $this->print_plugin_names( $uncertain );
1287 }
1288
1289 /**
1290 * Get the printable names for a set of plugins given their file names.
1291 *
1292 * @param array $plugins The plugin file names.
1293 * @param bool $display_filenames True to simply return the sorted list of plugin file names.
1294 * @return array A sorted array of plugin names or file names.
1295 */
1296 private function get_printable_plugin_names( array $plugins, bool $display_filenames ): array {
1297 if ( $display_filenames ) {
1298 sort( $plugins );
1299 return $plugins;
1300 }
1301
1302 $plugin_names = array_map(
1303 fn( $plugin_file ) => get_plugin_data( WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $plugin_file, false )['Name'] ?? $plugin_file,
1304 $plugins
1305 );
1306 sort( $plugin_names );
1307 return $plugin_names;
1308 }
1309
1310 /**
1311 * Print a list of plugin names.
1312 *
1313 * @param array $plugins The names to print.
1314 */
1315 private function print_plugin_names( array $plugins ): void {
1316 foreach ( $plugins as $plugin_file ) {
1317 $this->log( ' ' . $plugin_file );
1318 }
1319 }
1320
1321 /**
1322 * Show a log message using the WP_CLI text colorization feature.
1323 *
1324 * @param string $text Text to show.
1325 */
1326 private function log( string $text ) {
1327 WP_CLI::log( WP_CLI::colorize( $text ) );
1328 }
1329
1330 /**
1331 * Enables compatibility mode, which keeps the HPOS and posts datastore in sync.
1332 *
1333 * @since 9.1.0
1334 */
1335 public function enable_compat_mode(): void {
1336 $this->toggle_compat_mode( true );
1337 }
1338
1339 /**
1340 * Disables compatibility mode, which keeps the HPOS and posts datastore in sync.
1341 *
1342 * @since 9.1.0
1343 */
1344 public function disable_compat_mode(): void {
1345 $this->toggle_compat_mode( false );
1346 }
1347
1348 /**
1349 * Toggles compatibility mode on or off.
1350 *
1351 * @since 9.1.0
1352 *
1353 * @param bool $enabled TRUE to enable compatibility mode, FALSE to disable.
1354 */
1355 private function toggle_compat_mode( bool $enabled ): void {
1356 if ( ! $this->synchronizer->check_orders_table_exists() ) {
1357 if ( $enabled ) {
1358 $this->synchronizer->create_database_tables();
1359 } else {
1360 WP_CLI::error( __( 'HPOS tables do not exist.', 'woocommerce' ) );
1361 }
1362 }
1363
1364 $currently_enabled = $this->synchronizer->data_sync_is_enabled();
1365
1366 if ( $currently_enabled === $enabled ) {
1367 if ( $enabled ) {
1368 WP_CLI::warning( __( 'Compatibility mode is already enabled.', 'woocommerce' ) );
1369 } else {
1370 WP_CLI::warning( __( 'Compatibility mode is already disabled.', 'woocommerce' ) );
1371 }
1372
1373 return;
1374 }
1375
1376 update_option( $this->synchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, wc_bool_to_string( $enabled ) );
1377
1378 if ( $enabled ) {
1379 WP_CLI::success( __( 'Compatibility mode enabled.', 'woocommerce' ) );
1380 } else {
1381 WP_CLI::success( __( 'Compatibility mode disabled.', 'woocommerce' ) );
1382 }
1383 }
1384 }
1385