PostsToOrdersMigrationController.php
7.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
<?php
/**
* Class for implementing migration from wp_posts and wp_postmeta to custom order tables.
*/
namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Utilities\ArrayUtil;
/**
* This is the main class used to perform the complete migration of orders
* from the posts table to the custom orders table.
*
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
*/
class PostsToOrdersMigrationController {
/**
* Error logger for migration errors.
*
* @var \WC_Logger
*/
private $error_logger;
/**
* Array of objects used to perform the migration.
*
* @var array
*/
private $all_migrators;
/**
* The source name to use for logs.
*/
public const LOGS_SOURCE_NAME = 'posts-to-orders-migration';
/**
* PostsToOrdersMigrationController constructor.
*/
public function __construct() {
$this->all_migrators = array();
$this->all_migrators[] = new PostToOrderTableMigrator();
$this->all_migrators[] = new PostToOrderAddressTableMigrator( 'billing' );
$this->all_migrators[] = new PostToOrderAddressTableMigrator( 'shipping' );
$this->all_migrators[] = new PostToOrderOpTableMigrator();
$this->all_migrators[] = new PostMetaToOrderMetaMigrator( $this->get_migrated_meta_keys() );
$this->error_logger = wc_get_logger();
}
/**
* Helper method to get migrated keys for all the tables in this controller.
*
* @return string[] Array of meta keys.
*/
public function get_migrated_meta_keys() {
$migrated_meta_keys = array();
foreach ( $this->all_migrators as $migrator ) {
if ( method_exists( $migrator, 'get_meta_column_config' ) ) {
$migrated_meta_keys = array_merge( $migrated_meta_keys, $migrator->get_meta_column_config() );
}
}
return array_keys( $migrated_meta_keys );
}
/**
* Migrates a set of orders from the posts table to the custom orders tables.
*
* @param array $order_post_ids List of post IDs of the orders to migrate.
*/
public function migrate_orders( array $order_post_ids ): void {
$this->error_logger = WC()->call_function( 'wc_get_logger' );
$using_transactions = $this->maybe_start_transaction();
if ( null === $using_transactions ) {
return;
}
$errors_were_logged = false;
foreach ( $this->all_migrators as $migrator ) {
$errors_were_logged = $this->do_orders_migration_step( $migrator, $order_post_ids );
if ( $errors_were_logged && $using_transactions ) {
$this->rollback_transaction();
break;
}
}
if ( ! $errors_were_logged && $using_transactions ) {
$this->commit_transaction();
}
}
/**
* Start a database transaction if the configuration mandates so.
*
* @return bool|null True if transaction started, false if transactions won't be used, null if transaction failed to start.
*/
private function maybe_start_transaction(): ?bool {
if ( 'yes' !== get_option( CustomOrdersTableController::USE_DB_TRANSACTIONS_OPTION ) ) {
return false;
}
$transaction_isolation_level = get_option( CustomOrdersTableController::DB_TRANSACTIONS_ISOLATION_LEVEL_OPTION, CustomOrdersTableController::DEFAULT_DB_TRANSACTIONS_ISOLATION_LEVEL );
$this->verify_transaction_isolation_level( $transaction_isolation_level );
$set_transaction_isolation_level_command = "SET TRANSACTION ISOLATION LEVEL $transaction_isolation_level";
if ( ! $this->db_query( $set_transaction_isolation_level_command ) ) {
return null;
}
return $this->db_query( 'START TRANSACTION' ) ? true : null;
}
/**
* Verify that a given database transaction isolation level name is valid, and throw an exception if not.
*
* @param string $transaction_isolation_level Transaction isolation level name to check.
* @return void
* @throws \Exception Invalid transaction isolation level name.
*/
private function verify_transaction_isolation_level( string $transaction_isolation_level ): void {
if ( ! in_array( $transaction_isolation_level, CustomOrdersTableController::get_valid_transaction_isolation_levels(), true ) ) {
throw new \Exception( 'Invalid database transaction isolation level name ' . $transaction_isolation_level );
}
}
/**
* Commit the current database transaction.
*
* @return bool True on success, false on error.
*/
private function commit_transaction(): bool {
return $this->db_query( 'COMMIT' );
}
/**
* Rollback the current database transaction.
*
* @return bool True on success, false on error.
*/
private function rollback_transaction(): bool {
return $this->db_query( 'ROLLBACK' );
}
/**
* Execute a database query and log any errors.
*
* @param string $query The SQL query to execute.
* @return bool True if the query succeeded, false if there were errors.
*/
private function db_query( string $query ): bool {
$wpdb = WC()->get_global( 'wpdb' );
try {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query( $query );
} catch ( \Exception $exception ) {
$exception_class = get_class( $exception );
$this->error_logger->error(
"PostsToOrdersMigrationController: when executing $query: ($exception_class) {$exception->getMessage()}, {$exception->getTraceAsString()}",
array(
'source' => self::LOGS_SOURCE_NAME,
'exception' => $exception,
)
);
return false;
}
$error = $wpdb->last_error;
if ( '' !== $error ) {
$this->error_logger->error(
"PostsToOrdersMigrationController: when executing $query: $error",
array(
'source' => self::LOGS_SOURCE_NAME,
'error' => $error,
)
);
return false;
}
return true;
}
/**
* Performs one step of the migration for a set of order posts using one given migration class.
* All database errors and exceptions are logged.
*
* @param object $migration_class The migration class to use, must have a `process_migration_batch_for_ids(array of ids)` method.
* @param array $order_post_ids List of post IDs of the orders to migrate.
* @return bool True if errors were logged, false otherwise.
*/
private function do_orders_migration_step( object $migration_class, array $order_post_ids ): bool {
$result = $migration_class->process_migration_batch_for_ids( $order_post_ids );
$errors = array_unique( $result['errors'] );
$exception = $result['exception'];
if ( null === $exception && empty( $errors ) ) {
return false;
}
$migration_class_name = ( new \ReflectionClass( $migration_class ) )->getShortName();
$batch = ArrayUtil::to_ranges_string( $order_post_ids );
if ( null !== $exception ) {
$exception_class = get_class( $exception );
$this->error_logger->error(
"$migration_class_name: when processing ids $batch: ($exception_class) {$exception->getMessage()}, {$exception->getTraceAsString()}",
array(
'source' => self::LOGS_SOURCE_NAME,
'ids' => $order_post_ids,
'exception' => $exception,
)
);
}
foreach ( $errors as $error ) {
$this->error_logger->error(
"$migration_class_name: when processing ids $batch: $error",
array(
'source' => self::LOGS_SOURCE_NAME,
'ids' => $order_post_ids,
'error' => $error,
)
);
}
return true;
}
/**
* Verify whether the given order IDs were migrated properly or not.
*
* @param array $order_post_ids Order IDs.
*
* @return array Array of failed IDs along with columns.
*/
public function verify_migrated_orders( array $order_post_ids ): array {
$errors = array();
foreach ( $this->all_migrators as $migrator ) {
if ( method_exists( $migrator, 'verify_migrated_data' ) ) {
$errors = $errors + $migrator->verify_migrated_data( $order_post_ids );
}
}
return $errors;
}
/**
* Migrates an order from the posts table to the custom orders tables.
*
* @param int $order_post_id Post ID of the order to migrate.
*/
public function migrate_order( int $order_post_id ): void {
$this->migrate_orders( array( $order_post_id ) );
}
}