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
PostsToOrdersMigrationController.php
290 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Class for implementing migration from wp_posts and wp_postmeta to custom order tables. |
| 4 | */ |
| 5 | |
| 6 | namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable; |
| 7 | |
| 8 | use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController; |
| 9 | use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore; |
| 10 | use Automattic\WooCommerce\Utilities\ArrayUtil; |
| 11 | use Automattic\WooCommerce\Utilities\OrderUtil; |
| 12 | |
| 13 | /** |
| 14 | * This is the main class used to perform the complete migration of orders |
| 15 | * from the posts table to the custom orders table. |
| 16 | * |
| 17 | * @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable |
| 18 | */ |
| 19 | class PostsToOrdersMigrationController { |
| 20 | |
| 21 | /** |
| 22 | * Error logger for migration errors. |
| 23 | * |
| 24 | * @var \WC_Logger |
| 25 | */ |
| 26 | private $error_logger; |
| 27 | |
| 28 | /** |
| 29 | * Array of objects used to perform the migration. |
| 30 | * |
| 31 | * @var \Automattic\WooCommerce\Database\Migrations\TableMigrator[] |
| 32 | */ |
| 33 | private $all_migrators; |
| 34 | |
| 35 | /** |
| 36 | * The source name to use for logs. |
| 37 | */ |
| 38 | public const LOGS_SOURCE_NAME = 'posts-to-orders-migration'; |
| 39 | |
| 40 | /** |
| 41 | * PostsToOrdersMigrationController constructor. |
| 42 | */ |
| 43 | public function __construct() { |
| 44 | |
| 45 | $this->all_migrators = array(); |
| 46 | $this->all_migrators['order'] = new PostToOrderTableMigrator(); |
| 47 | $this->all_migrators['order_address_billing'] = new PostToOrderAddressTableMigrator( 'billing' ); |
| 48 | $this->all_migrators['order_address_shipping'] = new PostToOrderAddressTableMigrator( 'shipping' ); |
| 49 | $this->all_migrators['order_operational_data'] = new PostToOrderOpTableMigrator(); |
| 50 | $this->all_migrators['order_meta'] = new PostMetaToOrderMetaMigrator( $this->get_migrated_meta_keys() ); |
| 51 | $this->error_logger = wc_get_logger(); |
| 52 | } |
| 53 | |
| 54 | /** |
| 55 | * Helper method to get migrated keys for all the tables in this controller. |
| 56 | * |
| 57 | * @return string[] Array of meta keys. |
| 58 | */ |
| 59 | public function get_migrated_meta_keys() { |
| 60 | $migrated_meta_keys = array(); |
| 61 | foreach ( $this->all_migrators as $name => $migrator ) { |
| 62 | if ( method_exists( $migrator, 'get_meta_column_config' ) ) { |
| 63 | $migrated_meta_keys = array_merge( $migrated_meta_keys, $migrator->get_meta_column_config() ); |
| 64 | } |
| 65 | } |
| 66 | return array_keys( $migrated_meta_keys ); |
| 67 | } |
| 68 | |
| 69 | /** |
| 70 | * Migrates a set of orders from the posts table to the custom orders tables. |
| 71 | * |
| 72 | * @param array $order_post_ids List of post IDs of the orders to migrate. |
| 73 | */ |
| 74 | public function migrate_orders( array $order_post_ids ): void { |
| 75 | $this->error_logger = WC()->call_function( 'wc_get_logger' ); |
| 76 | |
| 77 | $data = array(); |
| 78 | try { |
| 79 | foreach ( $this->all_migrators as $name => $migrator ) { |
| 80 | $data[ $name ] = $migrator->fetch_sanitized_migration_data( $order_post_ids ); |
| 81 | if ( ! empty( $data[ $name ]['errors'] ) ) { |
| 82 | $this->handle_migration_error( $order_post_ids, $data[ $name ]['errors'], null, null, $name ); |
| 83 | return; |
| 84 | } |
| 85 | } |
| 86 | } catch ( \Exception $e ) { |
| 87 | $this->handle_migration_error( $order_post_ids, $data, $e, null, 'Fetching data' ); |
| 88 | return; |
| 89 | } |
| 90 | |
| 91 | $using_transactions = $this->maybe_start_transaction(); |
| 92 | |
| 93 | foreach ( $this->all_migrators as $name => $migrator ) { |
| 94 | $results = $migrator->process_migration_data( $data[ $name ] ); |
| 95 | $errors = array_unique( $results['errors'] ); |
| 96 | $exception = $results['exception']; |
| 97 | |
| 98 | if ( null === $exception && empty( $errors ) ) { |
| 99 | continue; |
| 100 | } |
| 101 | $this->handle_migration_error( $order_post_ids, $errors, $exception, $using_transactions, $name ); |
| 102 | return; |
| 103 | } |
| 104 | |
| 105 | if ( $using_transactions ) { |
| 106 | $this->commit_transaction(); |
| 107 | } |
| 108 | $this->maybe_clear_order_datastore_cache_for_ids( $order_post_ids ); |
| 109 | } |
| 110 | |
| 111 | /** |
| 112 | * Log migration errors if any. |
| 113 | * |
| 114 | * @param array $order_post_ids List of post IDs of the orders to migrate. |
| 115 | * @param array $errors List of errors to log. |
| 116 | * @param \Exception|null $exception Exception to log. |
| 117 | * @param bool|null $using_transactions Whether transactions were used. |
| 118 | * @param string $name Name of the migrator. |
| 119 | */ |
| 120 | private function handle_migration_error( array $order_post_ids, array $errors, ?\Exception $exception, ?bool $using_transactions, string $name ) { |
| 121 | $batch = ArrayUtil::to_ranges_string( $order_post_ids ); |
| 122 | |
| 123 | if ( null !== $exception ) { |
| 124 | $exception_class = get_class( $exception ); |
| 125 | $this->error_logger->error( |
| 126 | "$name: when processing ids $batch: ($exception_class) {$exception->getMessage()}, {$exception->getTraceAsString()}", |
| 127 | array( |
| 128 | 'source' => self::LOGS_SOURCE_NAME, |
| 129 | 'ids' => $order_post_ids, |
| 130 | 'exception' => $exception, |
| 131 | ) |
| 132 | ); |
| 133 | } |
| 134 | |
| 135 | foreach ( $errors as $error ) { |
| 136 | $this->error_logger->error( |
| 137 | "$name: when processing ids $batch: $error", |
| 138 | array( |
| 139 | 'source' => self::LOGS_SOURCE_NAME, |
| 140 | 'ids' => $order_post_ids, |
| 141 | 'error' => $error, |
| 142 | ) |
| 143 | ); |
| 144 | } |
| 145 | |
| 146 | if ( $using_transactions ) { |
| 147 | $this->rollback_transaction(); |
| 148 | } else { |
| 149 | $this->maybe_clear_order_datastore_cache_for_ids( $order_post_ids ); |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | /** |
| 154 | * Clear the cache of order data for modified orders during migration if cache is enabled. |
| 155 | * |
| 156 | * @param array $order_post_ids List of order IDs of the orders to clear from cache. |
| 157 | * |
| 158 | * @return void |
| 159 | */ |
| 160 | private function maybe_clear_order_datastore_cache_for_ids( array $order_post_ids ) { |
| 161 | if ( OrderUtil::custom_orders_table_datastore_cache_enabled() ) { |
| 162 | $orders_table_datastore = wc_get_container()->get( OrdersTableDataStore::class ); |
| 163 | if ( is_callable( array( $orders_table_datastore, 'clear_cached_data' ) ) ) { |
| 164 | $orders_table_datastore->clear_cached_data( $order_post_ids ); |
| 165 | } |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | /** |
| 170 | * Start a database transaction if the configuration mandates so. |
| 171 | * |
| 172 | * @return bool|null True if transaction started, false if transactions won't be used, null if transaction failed to start. |
| 173 | * |
| 174 | * @throws \Exception If the transaction isolation level is invalid. |
| 175 | */ |
| 176 | private function maybe_start_transaction(): ?bool { |
| 177 | |
| 178 | $use_transactions = get_option( CustomOrdersTableController::USE_DB_TRANSACTIONS_OPTION, 'yes' ); |
| 179 | if ( 'yes' !== $use_transactions ) { |
| 180 | return null; |
| 181 | } |
| 182 | |
| 183 | $transaction_isolation_level = get_option( CustomOrdersTableController::DB_TRANSACTIONS_ISOLATION_LEVEL_OPTION, CustomOrdersTableController::DEFAULT_DB_TRANSACTIONS_ISOLATION_LEVEL ); |
| 184 | $valid_transaction_isolation_levels = array( 'READ UNCOMMITTED', 'READ COMMITTED', 'REPEATABLE READ', 'SERIALIZABLE' ); |
| 185 | if ( ! in_array( $transaction_isolation_level, $valid_transaction_isolation_levels, true ) ) { |
| 186 | throw new \Exception( "Invalid database transaction isolation level name $transaction_isolation_level" ); |
| 187 | } |
| 188 | |
| 189 | $set_transaction_isolation_level_command = "SET TRANSACTION ISOLATION LEVEL $transaction_isolation_level"; |
| 190 | |
| 191 | // We suppress errors in transaction isolation level setting because it's not supported by all DB engines, additionally, this might be executing in context of another transaction with a different isolation level. |
| 192 | if ( ! $this->db_query( $set_transaction_isolation_level_command, true ) ) { |
| 193 | return null; |
| 194 | } |
| 195 | |
| 196 | return $this->db_query( 'START TRANSACTION' ) ? true : null; |
| 197 | } |
| 198 | |
| 199 | /** |
| 200 | * Commit the current database transaction. |
| 201 | * |
| 202 | * @return bool True on success, false on error. |
| 203 | */ |
| 204 | private function commit_transaction(): bool { |
| 205 | return $this->db_query( 'COMMIT' ); |
| 206 | } |
| 207 | |
| 208 | /** |
| 209 | * Rollback the current database transaction. |
| 210 | * |
| 211 | * @return bool True on success, false on error. |
| 212 | */ |
| 213 | private function rollback_transaction(): bool { |
| 214 | return $this->db_query( 'ROLLBACK' ); |
| 215 | } |
| 216 | |
| 217 | /** |
| 218 | * Execute a database query and log any errors. |
| 219 | * |
| 220 | * @param string $query The SQL query to execute. |
| 221 | * @param bool $supress_errors Whether to suppress errors. |
| 222 | * |
| 223 | * @return bool True if the query succeeded, false if there were errors. |
| 224 | */ |
| 225 | private function db_query( string $query, bool $supress_errors = false ): bool { |
| 226 | $wpdb = WC()->get_global( 'wpdb' ); |
| 227 | |
| 228 | try { |
| 229 | if ( $supress_errors ) { |
| 230 | $suppress = $wpdb->suppress_errors( true ); |
| 231 | } |
| 232 | // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared |
| 233 | $wpdb->query( $query ); |
| 234 | if ( $supress_errors ) { |
| 235 | $wpdb->suppress_errors( $suppress ); |
| 236 | } |
| 237 | } catch ( \Exception $exception ) { |
| 238 | $exception_class = get_class( $exception ); |
| 239 | $this->error_logger->error( |
| 240 | "PostsToOrdersMigrationController: when executing $query: ($exception_class) {$exception->getMessage()}, {$exception->getTraceAsString()}", |
| 241 | array( |
| 242 | 'source' => self::LOGS_SOURCE_NAME, |
| 243 | 'exception' => $exception, |
| 244 | ) |
| 245 | ); |
| 246 | return false; |
| 247 | } |
| 248 | |
| 249 | $error = $wpdb->last_error; |
| 250 | if ( '' !== $error ) { |
| 251 | $this->error_logger->error( |
| 252 | "PostsToOrdersMigrationController: when executing $query: $error", |
| 253 | array( |
| 254 | 'source' => self::LOGS_SOURCE_NAME, |
| 255 | 'error' => $error, |
| 256 | ) |
| 257 | ); |
| 258 | return false; |
| 259 | } |
| 260 | |
| 261 | return true; |
| 262 | } |
| 263 | |
| 264 | /** |
| 265 | * Verify whether the given order IDs were migrated properly or not. |
| 266 | * |
| 267 | * @param array $order_post_ids Order IDs. |
| 268 | * |
| 269 | * @return array Array of failed IDs along with columns. |
| 270 | */ |
| 271 | public function verify_migrated_orders( array $order_post_ids ): array { |
| 272 | $errors = array(); |
| 273 | foreach ( $this->all_migrators as $migrator ) { |
| 274 | if ( method_exists( $migrator, 'verify_migrated_data' ) ) { |
| 275 | $errors = $errors + $migrator->verify_migrated_data( $order_post_ids ); |
| 276 | } |
| 277 | } |
| 278 | return $errors; |
| 279 | } |
| 280 | |
| 281 | /** |
| 282 | * Migrates an order from the posts table to the custom orders tables. |
| 283 | * |
| 284 | * @param int $order_post_id Post ID of the order to migrate. |
| 285 | */ |
| 286 | public function migrate_order( int $order_post_id ): void { |
| 287 | $this->migrate_orders( array( $order_post_id ) ); |
| 288 | } |
| 289 | } |
| 290 |