PluginProbe
WooCommerce / 11.0.0
WooCommerce v11.0.0
11.1.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 All 648 releases
woocommerce / src / Internal / DataStores / Orders / CustomOrdersTableController.php
CustomOrdersTableController.php
848 lines 30.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * CustomOrdersTableController class file.
4 */
5
6 namespace Automattic\WooCommerce\Internal\DataStores\Orders;
7
8 use Automattic\WooCommerce\Caches\OrderCache;
9 use Automattic\WooCommerce\Caches\OrderCacheController;
10 use Automattic\WooCommerce\Enums\FeaturePluginCompatibility;
11 use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;
12 use Automattic\WooCommerce\Internal\Features\FeaturesController;
13 use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
14 use Automattic\WooCommerce\Utilities\OrderUtil;
15 use Automattic\WooCommerce\Utilities\PluginUtil;
16 use WC_Admin_Settings;
17
18 defined( 'ABSPATH' ) || exit;
19
20 /**
21 * This is the main class that controls the custom orders tables feature. Its responsibilities are:
22 *
23 * - Displaying UI components (entries in the tools page and in settings)
24 * - Providing the proper data store for orders via 'woocommerce_order_data_store' hook
25 *
26 * ...and in general, any functionality that doesn't imply database access.
27 */
28 class CustomOrdersTableController {
29
30 private const SYNC_QUERY_ARG = 'wc_hpos_sync_now';
31
32 private const STOP_SYNC_QUERY_ARG = 'wc_hpos_stop_sync';
33
34 /**
35 * The name of the option for enabling the usage of the custom orders tables
36 */
37 public const CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION = 'woocommerce_custom_orders_table_enabled';
38
39 /**
40 * The name of the option that tells whether database transactions are to be used or not for data synchronization.
41 */
42 public const USE_DB_TRANSACTIONS_OPTION = 'woocommerce_use_db_transactions_for_custom_orders_table_data_sync';
43
44 /**
45 * The name of the option to store the transaction isolation level to use when database transactions are enabled.
46 */
47 public const DB_TRANSACTIONS_ISOLATION_LEVEL_OPTION = 'woocommerce_db_transactions_isolation_level_for_custom_orders_table_data_sync';
48
49 public const DEFAULT_DB_TRANSACTIONS_ISOLATION_LEVEL = 'READ UNCOMMITTED';
50
51 public const HPOS_FTS_INDEX_OPTION = 'woocommerce_hpos_fts_index_enabled';
52
53 public const HPOS_FTS_ADDRESS_INDEX_CREATED_OPTION = 'woocommerce_hpos_address_fts_index_created';
54
55 public const HPOS_FTS_ORDER_ITEM_INDEX_CREATED_OPTION = 'woocommerce_hpos_order_item_fts_index_created';
56
57 public const HPOS_DATASTORE_CACHING_ENABLED_OPTION = 'woocommerce_hpos_datastore_caching_enabled';
58
59 /**
60 * The data store object to use.
61 *
62 * @var OrdersTableDataStore
63 */
64 private $data_store;
65
66 /**
67 * Refunds data store object to use.
68 *
69 * @var OrdersTableRefundDataStore
70 */
71 private $refund_data_store;
72
73 /**
74 * The data synchronizer object to use.
75 *
76 * @var DataSynchronizer
77 */
78 private $data_synchronizer;
79
80 /**
81 * The data cleanup instance to use.
82 *
83 * @var LegacyDataCleanup
84 */
85 private $data_cleanup;
86
87 /**
88 * The batch processing controller to use.
89 *
90 * @var BatchProcessingController
91 */
92 private $batch_processing_controller;
93
94 /**
95 * The features controller to use.
96 *
97 * @var FeaturesController
98 */
99 private $features_controller;
100
101 /**
102 * The orders cache object to use.
103 *
104 * @var OrderCache
105 */
106 private $order_cache;
107
108 /**
109 * The orders cache controller object to use.
110 *
111 * @var OrderCacheController
112 */
113 private $order_cache_controller;
114
115 /**
116 * The plugin util object to use.
117 *
118 * @var PluginUtil
119 */
120 private $plugin_util;
121
122 /**
123 * The db util object to use.
124 *
125 * @var DatabaseUtil;
126 */
127 private $db_util;
128
129 /**
130 * Class constructor.
131 */
132 public function __construct() {
133 $this->init_hooks();
134 }
135
136 /**
137 * Initialize the hooks used by the class.
138 */
139 private function init_hooks() {
140 add_filter( 'woocommerce_order_data_store', array( $this, 'get_orders_data_store' ), 999, 1 );
141 add_filter( 'woocommerce_order-refund_data_store', array( $this, 'get_refunds_data_store' ), 999, 1 );
142 add_filter( 'woocommerce_debug_tools', array( $this, 'add_hpos_tools' ), 999 );
143 add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 );
144 add_filter( 'updated_option', array( $this, 'process_updated_option_fts_index' ), 999, 3 );
145 add_filter( 'pre_update_option', array( $this, 'process_pre_update_option' ), 999, 3 );
146 add_action( 'woocommerce_after_register_post_type', array( $this, 'register_post_type_for_order_placeholders' ), 10, 0 );
147 add_action( 'woocommerce_sections_advanced', array( $this, 'sync_now' ) );
148 add_filter( 'removable_query_args', array( $this, 'register_removable_query_arg' ) );
149 add_filter( 'get_edit_post_link', array( $this, 'maybe_rewrite_order_edit_link' ), 10, 2 );
150 add_action( 'before_woocommerce_init', array( $this, 'maybe_set_order_cache_group_as_non_persistent' ) );
151 add_filter( 'map_meta_cap', array( $this, 'maybe_translate_order_caps' ), 0, 4 );
152 }
153
154 /**
155 * Class initialization, invoked by the DI container.
156 *
157 * @internal
158 * @param OrdersTableDataStore $data_store The data store to use.
159 * @param DataSynchronizer $data_synchronizer The data synchronizer to use.
160 * @param LegacyDataCleanup $data_cleanup The legacy data cleanup instance to use.
161 * @param OrdersTableRefundDataStore $refund_data_store The refund data store to use.
162 * @param BatchProcessingController $batch_processing_controller The batch processing controller to use.
163 * @param FeaturesController $features_controller The features controller instance to use.
164 * @param OrderCache $order_cache The order cache engine to use.
165 * @param OrderCacheController $order_cache_controller The order cache controller to use.
166 * @param PluginUtil $plugin_util The plugin util to use.
167 * @param DatabaseUtil $db_util The database util to use.
168 */
169 final public function init(
170 OrdersTableDataStore $data_store,
171 DataSynchronizer $data_synchronizer,
172 LegacyDataCleanup $data_cleanup,
173 OrdersTableRefundDataStore $refund_data_store,
174 BatchProcessingController $batch_processing_controller,
175 FeaturesController $features_controller,
176 OrderCache $order_cache,
177 OrderCacheController $order_cache_controller,
178 PluginUtil $plugin_util,
179 DatabaseUtil $db_util
180 ) {
181 $this->data_store = $data_store;
182 $this->data_synchronizer = $data_synchronizer;
183 $this->data_cleanup = $data_cleanup;
184 $this->batch_processing_controller = $batch_processing_controller;
185 $this->refund_data_store = $refund_data_store;
186 $this->features_controller = $features_controller;
187 $this->order_cache = $order_cache;
188 $this->order_cache_controller = $order_cache_controller;
189 $this->plugin_util = $plugin_util;
190 $this->db_util = $db_util;
191 }
192
193 /**
194 * Translate capabilities for HPOS orders when sync is not active.
195 *
196 * Only activates when HPOS is the authoritative source and sync is off,
197 * then lazily delegates to HposOrderCapabilityHelper for the actual
198 * capability translation.
199 *
200 * @since 10.7.0
201 *
202 * @param string[] $caps The resolved primitive capabilities.
203 * @param string $cap The meta capability being checked.
204 * @param int $user_id The user ID.
205 * @param array $args Additional arguments (object ID).
206 * @return string[] Translated capabilities.
207 */
208 public function maybe_translate_order_caps( $caps, $cap, $user_id, $args ) {
209 if ( ! $this->custom_orders_table_usage_is_enabled() ) {
210 return $caps;
211 }
212
213 if ( ! $this->data_synchronizer instanceof DataSynchronizer ) {
214 return $caps;
215 }
216
217 if ( $this->data_synchronizer->data_sync_is_enabled() ) {
218 return $caps;
219 }
220
221 return wc_get_container()->get( HposOrderCapabilityHelper::class )->translate_order_caps( $caps, $cap, $user_id, $args );
222 }
223
224 /**
225 * Is the custom orders table usage enabled via settings?
226 * This can be true only if the feature is enabled and a table regeneration has been completed.
227 *
228 * @return bool True if the custom orders table usage is enabled
229 */
230 public function custom_orders_table_usage_is_enabled(): bool {
231 return get_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ) === 'yes';
232 }
233
234 /**
235 * Is caching of data within the CustomerOrdersTable datastores enabled?
236 *
237 * @return bool True if the caching is enabled within the CustomeOrderTable Datastores.
238 */
239 public function hpos_data_caching_is_enabled(): bool {
240 return get_option( self::HPOS_DATASTORE_CACHING_ENABLED_OPTION ) === 'yes' &&
241 $this->custom_orders_table_usage_is_enabled();
242 }
243
244 /**
245 * Gets the instance of the orders data store to use.
246 *
247 * @param \WC_Object_Data_Store_Interface|string $default_data_store The default data store (as received via the woocommerce_order_data_store hook).
248 *
249 * @return \WC_Object_Data_Store_Interface|string The actual data store to use.
250 *
251 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
252 */
253 public function get_orders_data_store( $default_data_store ) {
254 return $this->get_data_store_instance( $default_data_store, 'order' );
255 }
256
257 /**
258 * Gets the instance of the refunds data store to use.
259 *
260 * @param \WC_Object_Data_Store_Interface|string $default_data_store The default data store (as received via the woocommerce_order-refund_data_store hook).
261 *
262 * @return \WC_Object_Data_Store_Interface|string The actual data store to use.
263 *
264 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
265 */
266 public function get_refunds_data_store( $default_data_store ) {
267 return $this->get_data_store_instance( $default_data_store, 'order_refund' );
268 }
269
270 /**
271 * Gets the instance of a given data store.
272 *
273 * @param \WC_Object_Data_Store_Interface|string $default_data_store The default data store (as received via the appropriate hooks).
274 * @param string $type The type of the data store to get.
275 *
276 * @return \WC_Object_Data_Store_Interface|string The actual data store to use.
277 */
278 private function get_data_store_instance( $default_data_store, string $type ) {
279 if ( $this->custom_orders_table_usage_is_enabled() ) {
280 switch ( $type ) {
281 case 'order_refund':
282 return $this->refund_data_store;
283 default:
284 return $this->data_store;
285 }
286 } else {
287 return $default_data_store;
288 }
289 }
290
291 /**
292 * Add an entry to Status - Tools to create or regenerate the custom orders table,
293 * and also an entry to delete the table as appropriate.
294 *
295 * @param array $tools_array The array of tools to add the tool to.
296 * @return array The updated array of tools.
297 *
298 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
299 */
300 public function add_hpos_tools( array $tools_array ): array {
301 if ( ! $this->data_synchronizer->check_orders_table_exists() ) {
302 return $tools_array;
303 }
304
305 // Cleanup tool.
306 $tools_array = array_merge( $tools_array, $this->data_cleanup->get_tools_entries() );
307
308 // Delete HPOS tables tool.
309 if ( $this->custom_orders_table_usage_is_enabled() || $this->data_synchronizer->data_sync_is_enabled() || $this->batch_processing_controller->is_enqueued( get_class( $this->data_synchronizer ) ) ) {
310 $disabled = true;
311 $message = __( 'This will delete the custom orders tables. The tables can be deleted only if the "High-Performance order storage" is not authoritative and sync is disabled (via Settings > Advanced > Features).', 'woocommerce' );
312 } else {
313 $disabled = false;
314 $message = __( 'This will delete the custom orders tables. To create them again enable the "High-Performance order storage" feature (via Settings > Advanced > Features).', 'woocommerce' );
315 }
316
317 $tools_array['delete_custom_orders_table'] = array(
318 'name' => __( 'Delete the custom orders tables', 'woocommerce' ),
319 'desc' => sprintf(
320 '<strong class="red">%1$s</strong> %2$s',
321 __( 'Note:', 'woocommerce' ),
322 $message
323 ),
324 'requires_refresh' => true,
325 'callback' => function () use ( $disabled ) {
326 if ( $disabled ) {
327 return;
328 }
329
330 $this->features_controller->change_feature_enable( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, false );
331 $this->delete_custom_orders_tables();
332 return __( 'Custom orders tables have been deleted.', 'woocommerce' );
333 },
334 'button' => __( 'Delete', 'woocommerce' ),
335 'disabled' => $disabled,
336 );
337
338 return $tools_array;
339 }
340
341 /**
342 * Delete the custom orders tables and any related options and data in response to the user pressing the tool button.
343 *
344 * @throws \Exception Can't delete the tables.
345 */
346 private function delete_custom_orders_tables() {
347 if ( $this->custom_orders_table_usage_is_enabled() ) {
348 throw new \Exception( "Can't delete the custom orders tables: they are currently in use (via Settings > Advanced > Features)." );
349 }
350
351 delete_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION );
352 $this->data_synchronizer->delete_database_tables();
353 }
354
355 /**
356 * Handler for the individual setting updated hook.
357 *
358 * @param string $option Setting name.
359 * @param mixed $old_value Old value of the setting.
360 * @param mixed $value New value of the setting.
361 *
362 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
363 */
364 public function process_updated_option( $option, $old_value, $value ) {
365 if ( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION === $option && 'no' === $value ) {
366 $this->data_synchronizer->cleanup_synchronization_state();
367 }
368 if ( self::HPOS_DATASTORE_CACHING_ENABLED_OPTION === $option && $old_value !== $value && 'yes' === $value ) {
369 $this->data_store->clear_all_cached_data();
370 }
371 }
372
373 /**
374 * Process option that enables FTS index on orders table. Tries to create an FTS index when option is enabled.
375 *
376 * @param string $option Option name.
377 * @param string $old_value Old value of the option.
378 * @param string $value New value of the option.
379 *
380 * @return void
381 *
382 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
383 */
384 public function process_updated_option_fts_index( $option, $old_value, $value ) {
385 if ( self::HPOS_FTS_INDEX_OPTION !== $option ) {
386 return;
387 }
388
389 if ( 'yes' !== $value ) {
390 return;
391 }
392
393 if ( ! $this->custom_orders_table_usage_is_enabled() ) {
394 update_option( self::HPOS_FTS_INDEX_OPTION, 'no', true );
395 if ( class_exists( 'WC_Admin_Settings' ) ) {
396 WC_Admin_Settings::add_error( __( 'Failed to create FTS index on orders table. This feature is only available when High-performance order storage is enabled.', 'woocommerce' ) );
397 }
398 return;
399 }
400
401 if ( ! $this->db_util->fts_index_on_order_address_table_exists() ) {
402 $this->db_util->create_fts_index_order_address_table();
403 }
404
405 // Check again to see if index was actually created.
406 if ( $this->db_util->fts_index_on_order_address_table_exists() ) {
407 update_option( self::HPOS_FTS_ADDRESS_INDEX_CREATED_OPTION, 'yes', false );
408 } else {
409 update_option( self::HPOS_FTS_ADDRESS_INDEX_CREATED_OPTION, 'no', false );
410 if ( class_exists( 'WC_Admin_Settings ' ) ) {
411 WC_Admin_Settings::add_error( __( 'Failed to create FTS index on address table', 'woocommerce' ) );
412 }
413 }
414
415 if ( ! $this->db_util->fts_index_on_order_item_table_exists() ) {
416 $this->db_util->create_fts_index_order_item_table();
417 }
418
419 // Check again to see if index was actually created.
420 if ( $this->db_util->fts_index_on_order_item_table_exists() ) {
421 update_option( self::HPOS_FTS_ORDER_ITEM_INDEX_CREATED_OPTION, 'yes', false );
422 } else {
423 update_option( self::HPOS_FTS_ORDER_ITEM_INDEX_CREATED_OPTION, 'no', false );
424 if ( class_exists( 'WC_Admin_Settings ' ) ) {
425 WC_Admin_Settings::add_error( __( 'Failed to create FTS index on order item table', 'woocommerce' ) );
426 }
427 }
428 }
429
430 /**
431 * Recreate order addresses FTS index. Useful when updating to 9.4 when phone number was added to index, or when other recreating index is needed.
432 *
433 * @since 9.4.0.
434 *
435 * @return array Array with keys status (bool) and message (string).
436 */
437 public function recreate_order_address_fts_index(): array {
438 $this->db_util->drop_fts_index_order_address_table();
439 if ( $this->db_util->fts_index_on_order_address_table_exists() ) {
440 return array(
441 'status' => false,
442 'message' => __( 'Failed to modify existing FTS index. Please go to WooCommerce > Status > Tools and run the "Re-create Order Address FTS index" tool.', 'woocommerce' ),
443 );
444 } else {
445 update_option( self::HPOS_FTS_ADDRESS_INDEX_CREATED_OPTION, 'no', false );
446 }
447
448 $this->db_util->create_fts_index_order_address_table();
449 if ( ! $this->db_util->fts_index_on_order_address_table_exists() ) {
450 return array(
451 'status' => false,
452 'message' => __( 'Failed to create FTS index on order address table. Please go to WooCommerce > Status > Tools and run the "Re-create Order Address FTS index" tool.', 'woocommerce' ),
453 );
454 } else {
455 update_option( self::HPOS_FTS_ADDRESS_INDEX_CREATED_OPTION, 'yes', false );
456 return array(
457 'status' => true,
458 'message' => __( 'FTS index recreated.', 'woocommerce' ),
459 );
460 }
461 }
462
463 /**
464 * Handler for the setting pre-update hook.
465 * We use it to verify that authoritative orders table switch doesn't happen while sync is pending.
466 *
467 * @param mixed $value New value of the setting.
468 * @param string $option Setting name.
469 * @param mixed $old_value Old value of the setting.
470 *
471 * @throws \Exception Attempt to change the authoritative orders table while orders sync is pending.
472 *
473 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
474 */
475 public function process_pre_update_option( $value, $option, $old_value ) {
476 if ( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION === $option && $value !== $old_value ) {
477 $this->order_cache->flush();
478 return $value;
479 }
480
481 if ( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION !== $option ) {
482 return $value;
483 }
484
485 if ( $old_value === $value ) {
486 return $value;
487 }
488
489 $this->order_cache->flush();
490 if ( ! $this->data_synchronizer->check_orders_table_exists() ) {
491 $this->data_synchronizer->create_database_tables();
492 }
493
494 $tables_created = get_option( DataSynchronizer::ORDERS_TABLE_CREATED ) === 'yes';
495 if ( ! $tables_created ) {
496 return 'no';
497 }
498
499 if ( ! $this->changing_data_source_with_sync_pending_is_allowed() && $this->data_synchronizer->has_orders_pending_sync() ) {
500 throw new \Exception( "The authoritative table for orders storage can't be changed while there are orders out of sync" );
501 }
502
503 return $value;
504 }
505
506 /**
507 * Callback to trigger a sync immediately by clicking a button on the Features screen.
508 *
509 * @return void
510 *
511 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
512 */
513 public function sync_now() {
514 $section = filter_input( INPUT_GET, 'section' );
515 if ( 'features' !== $section ) {
516 return;
517 }
518
519 if ( filter_input( INPUT_GET, self::SYNC_QUERY_ARG, FILTER_VALIDATE_BOOLEAN ) ) {
520 $action = 'sync-now';
521 } elseif ( filter_input( INPUT_GET, self::STOP_SYNC_QUERY_ARG, FILTER_VALIDATE_BOOLEAN ) ) {
522 $action = 'stop-sync';
523 } else {
524 return;
525 }
526
527 if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), "hpos-{$action}" ) ) {
528 WC_Admin_Settings::add_error(
529 'sync-now' === $action ?
530 esc_html__( 'Unable to start synchronization. The link you followed may have expired.', 'woocommerce' )
531 : esc_html__( 'Unable to stop synchronization. The link you followed may have expired.', 'woocommerce' )
532 );
533 return;
534 }
535
536 $this->data_cleanup->toggle_flag( false );
537
538 if ( 'sync-now' === $action ) {
539 if ( ! $this->data_synchronizer->check_orders_table_exists() && ! $this->data_synchronizer->create_database_tables() ) {
540 WC_Admin_Settings::add_error(
541 __( 'Unable to create HPOS tables for synchronization.', 'woocommerce' )
542 );
543 return;
544 }
545
546 $this->batch_processing_controller->enqueue_processor( DataSynchronizer::class );
547 } else {
548 $this->batch_processing_controller->remove_processor( DataSynchronizer::class );
549 }
550 }
551
552 /**
553 * Tell WP Admin to remove the sync query arg from the URL.
554 *
555 * @param array $query_args The query args that are removable.
556 *
557 * @return array
558 *
559 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
560 */
561 public function register_removable_query_arg( $query_args ) {
562 $query_args[] = self::SYNC_QUERY_ARG;
563 $query_args[] = self::STOP_SYNC_QUERY_ARG;
564
565 return $query_args;
566 }
567
568 /**
569 * Handler for the woocommerce_after_register_post_type post,
570 * registers the post type for placeholder orders.
571 *
572 * @return void
573 *
574 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
575 */
576 public function register_post_type_for_order_placeholders(): void {
577 wc_register_order_type(
578 DataSynchronizer::PLACEHOLDER_ORDER_POST_TYPE,
579 array(
580 'public' => false,
581 'exclude_from_search' => true,
582 'publicly_queryable' => false,
583 'show_ui' => false,
584 'show_in_menu' => false,
585 'show_in_nav_menus' => false,
586 'show_in_admin_bar' => false,
587 'show_in_rest' => false,
588 'rewrite' => false,
589 'query_var' => false,
590 'can_export' => false,
591 'supports' => array(),
592 'capabilities' => array(),
593 'exclude_from_order_count' => true,
594 'exclude_from_order_views' => true,
595 'exclude_from_order_reports' => true,
596 'exclude_from_order_sales_reports' => true,
597 )
598 );
599 }
600
601 /**
602 * Add the definition for the HPOS feature.
603 *
604 * @param FeaturesController $features_controller The instance of FeaturesController.
605 *
606 * @return void
607 *
608 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
609 */
610 public function add_feature_definition( $features_controller ) {
611 $definition = array(
612 'option_key' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
613 'is_experimental' => false,
614 'enabled_by_default' => false,
615 'order' => 50,
616 'setting' => $this->get_hpos_setting_for_feature(),
617 'default_plugin_compatibility' => FeaturePluginCompatibility::INCOMPATIBLE,
618 'additional_settings' => array(
619 $this->get_hpos_setting_for_sync(),
620 ),
621 );
622
623 $features_controller->add_feature_definition(
624 'custom_order_tables',
625 __( 'High-Performance order storage', 'woocommerce' ),
626 $definition
627 );
628 }
629
630 /**
631 * Returns the HPOS setting for rendering HPOS vs Post setting block in Features section of the settings page.
632 *
633 * @return array Feature setting object.
634 */
635 private function get_hpos_setting_for_feature() {
636 if ( 'yes' === get_transient( 'wc_installing' ) ) {
637 return array();
638 }
639
640 $get_value = function () {
641 return $this->custom_orders_table_usage_is_enabled() ? 'yes' : 'no';
642 };
643
644 /**
645 * ⚠️The FeaturesController instance must only be accessed from within the callback functions. Otherwise it
646 * gets called while it's still being instantiated and creates and endless loop.
647 */
648
649 $get_desc = function () {
650 $plugin_compatibility = $this->features_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
651
652 return $this->plugin_util->generate_incompatible_plugin_feature_warning( 'custom_order_tables', $plugin_compatibility );
653 };
654
655 $get_disabled = function () {
656 $compatibility_info = $this->features_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
657 $sync_complete = ! $this->data_synchronizer->has_orders_pending_sync();
658 $disabled = array();
659 // Changing something here? You might also want to look at `enable|disable` functions in Automattic\WooCommerce\Database\Migrations\CustomOrderTable\CLIRunner.
660 $incompatible_plugins = $this->plugin_util->get_items_considered_incompatible( 'custom_order_tables', $compatibility_info );
661 $incompatible_plugins = array_diff( $incompatible_plugins, $this->plugin_util->get_plugins_excluded_from_compatibility_ui() );
662 if ( count( $incompatible_plugins ) > 0 ) {
663 $disabled = array( 'yes' );
664 }
665 if ( ! $sync_complete && ! $this->changing_data_source_with_sync_pending_is_allowed() ) {
666 $disabled = array( 'yes', 'no' );
667 }
668
669 return $disabled;
670 };
671
672 return array(
673 'id' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
674 'title' => __( 'Order data storage', 'woocommerce' ),
675 'type' => 'radio',
676 'options' => array(
677 'no' => __( 'WordPress posts storage (legacy)', 'woocommerce' ),
678 'yes' => __( 'High-performance order storage (recommended)', 'woocommerce' ),
679 ),
680 'value' => $get_value,
681 'disabled' => $get_disabled,
682 'desc' => $get_desc,
683 'desc_at_end' => true,
684 'row_class' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
685 );
686 }
687
688 /**
689 * Returns the setting for rendering sync enabling setting block in Features section of the settings page.
690 *
691 * @return array Feature setting object.
692 */
693 private function get_hpos_setting_for_sync() {
694 if ( 'yes' === get_transient( 'wc_installing' ) ) {
695 return array();
696 }
697
698 $get_value = function () {
699 return get_option( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION );
700 };
701
702 $get_sync_message = function () {
703 $sync_in_progress = $this->batch_processing_controller->is_enqueued( get_class( $this->data_synchronizer ) );
704 $sync_enabled = $this->data_synchronizer->data_sync_is_enabled();
705 $sync_is_pending = $this->data_synchronizer->has_orders_pending_sync( true );
706 $sync_message = array();
707 $is_dangerous = $sync_is_pending && $this->changing_data_source_with_sync_pending_is_allowed();
708
709 if ( $is_dangerous ) {
710 $sync_message[] = wp_kses_data(
711 __( "There are orders pending sync.", 'woocommerce' )
712 . '<strong>'
713 . __( 'Switching data storage while sync is incomplete is dangerous and can lead to order data corruption or loss!', 'woocommerce' )
714 . '</strong>'
715 );
716 }
717
718 if ( ! $sync_enabled && $this->data_synchronizer->background_sync_is_enabled() ) {
719 $sync_message[] = __( 'Background sync is enabled.', 'woocommerce' );
720 }
721
722 if ( $sync_in_progress && $sync_is_pending ) {
723 $orders_pending_sync_count = $this->data_synchronizer->get_current_orders_pending_sync_count( true );
724
725 $sync_message[] = sprintf(
726 // translators: %s: number of pending orders.
727 __( 'Currently syncing orders... %s pending', 'woocommerce' ),
728 number_format_i18n( $orders_pending_sync_count )
729 );
730
731 if ( ! $sync_enabled ) {
732 $stop_sync_url = wp_nonce_url(
733 add_query_arg(
734 array(
735 self::STOP_SYNC_QUERY_ARG => true,
736 ),
737 wc_get_container()->get( FeaturesController::class )->get_features_page_url()
738 ),
739 'hpos-stop-sync'
740 );
741
742 $sync_message[] = sprintf(
743 '<a href="%1$s" class="button-link">%2$s</a>',
744 esc_url( $stop_sync_url ),
745 __( 'Stop sync', 'woocommerce' )
746 );
747 }
748 } elseif ( $sync_is_pending ) {
749 $sync_now_url = wp_nonce_url(
750 add_query_arg(
751 array(
752 self::SYNC_QUERY_ARG => true,
753 ),
754 wc_get_container()->get( FeaturesController::class )->get_features_page_url()
755 ),
756 'hpos-sync-now'
757 );
758
759 if ( ! $is_dangerous ) {
760 $sync_message[] = wp_kses_data(
761 __( "You can switch order data storage <strong>only when the posts and orders tables are in sync</strong>. There are currently orders out of sync.", 'woocommerce' ),
762 );
763 }
764
765 $sync_message[] = sprintf(
766 '<a href="%1$s" class="button-link">%2$s</a>',
767 esc_url( $sync_now_url ),
768 __( 'Sync orders now', 'woocommerce' )
769 );
770 }
771
772 return implode( '<br />', $sync_message );
773 };
774
775 $get_description_is_error = function () {
776 $sync_is_pending = $this->data_synchronizer->has_orders_pending_sync();
777
778 return $sync_is_pending && $this->changing_data_source_with_sync_pending_is_allowed();
779 };
780
781 return array(
782 'id' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
783 'title' => '',
784 'type' => 'checkbox',
785 'desc' => __( 'Enable compatibility mode (Synchronize orders between High-performance order storage and WordPress posts storage).', 'woocommerce' ),
786 'value' => $get_value,
787 'desc_tip' => $get_sync_message,
788 'description_is_error' => $get_description_is_error,
789 'row_class' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
790 );
791 }
792
793 /**
794 * Returns a value indicating if changing the authoritative data source for orders while there are orders pending synchronization is allowed.
795 *
796 * @return bool
797 */
798 private function changing_data_source_with_sync_pending_is_allowed(): bool {
799 /**
800 * Filter to allow changing where order data is stored, even when there are orders pending synchronization.
801 *
802 * DANGER! This filter is intended for usage when doing manual and automated testing in development environments only,
803 * it should NEVER be used in production environments. Order data corruption or loss can happen!
804 *
805 * @param bool $allow True to allow changing order storage when there are orders pending synchronization, false to disallow.
806 * @returns bool
807 *
808 * @since 8.3.0
809 */
810 return apply_filters( 'wc_allow_changing_orders_storage_while_sync_is_pending', false );
811 }
812
813 /**
814 * Rewrites post edit links for HPOS placeholder posts so that they go to the HPOS order itself.
815 * Hooked onto `get_edit_post_link`.
816 *
817 * @since 9.0.0
818 *
819 * @param string $link The edit link.
820 * @param int $post_id Post ID.
821 * @return string
822 *
823 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
824 */
825 public function maybe_rewrite_order_edit_link( $link, $post_id ) {
826 if ( DataSynchronizer::PLACEHOLDER_ORDER_POST_TYPE === get_post_type( $post_id ) ) {
827 $link = OrderUtil::get_order_admin_edit_url( $post_id );
828 }
829
830 return $link;
831 }
832
833 /**
834 * Set the `order_objects` cache group as non-persistent if Custom Order data caching is enabled.
835 *
836 * With order datastore cache enabled, caching of raw data is now handled by the datastore, rather than full object
837 * being stored in persistent cache.
838 *
839 * @return void
840 */
841 public function maybe_set_order_cache_group_as_non_persistent() {
842 if ( OrderUtil::custom_orders_table_datastore_cache_enabled() ) {
843 // If we're using datastore cache, we don't want to persist the order objects in cache. It should be in-memory only.
844 wp_cache_add_non_persistent_groups( array( $this->order_cache->get_object_type() ) );
845 }
846 }
847 }
848