From 2565df378e8a9a8102c3419f1485bb9419d1a662 Mon Sep 17 00:00:00 2001 From: Nestor Soriano Date: Fri, 21 Jan 2022 15:50:02 +0100 Subject: [PATCH] Add scafolding for the custom orders table feature. - Add the src/Internal/DataStores/Orders, with the appropriate class files. - Add an entry in the tools page to initiate the (re)generation of the table data (does nothing for now). - Add a new data store class (empty for now). --- .../includes/class-woocommerce.php | 2 + plugins/woocommerce/src/Container.php | 2 + .../DataStores/Orders/DataSynchronizer.php | 89 +++++ .../DataStores/Orders/FeatureController.php | 324 ++++++++++++++++++ .../Orders/OrdersTableDataStore.php | 143 ++++++++ .../OrdersDataStoreServiceProvider.php | 37 ++ 6 files changed, 597 insertions(+) create mode 100644 plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php create mode 100644 plugins/woocommerce/src/Internal/DataStores/Orders/FeatureController.php create mode 100644 plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php create mode 100644 plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrdersDataStoreServiceProvider.php diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php index ff2d0a79556..67efc877654 100644 --- a/plugins/woocommerce/includes/class-woocommerce.php +++ b/plugins/woocommerce/includes/class-woocommerce.php @@ -9,6 +9,7 @@ defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Internal\AssignDefaultCategory; +use Automattic\WooCommerce\Internal\DataStores\Orders\FeatureController as OrdersTableFeatureController; use Automattic\WooCommerce\Internal\DownloadPermissionsAdjuster; use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator; use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore; @@ -216,6 +217,7 @@ final class WooCommerce { wc_get_container()->get( DataRegenerator::class ); wc_get_container()->get( LookupDataStore::class ); wc_get_container()->get( RestockRefundedItemsAdjuster::class ); + wc_get_container()->get( OrdersTableFeatureController::class ); } /** diff --git a/plugins/woocommerce/src/Container.php b/plugins/woocommerce/src/Container.php index b0b99b94e14..9d5ed8c23ff 100644 --- a/plugins/woocommerce/src/Container.php +++ b/plugins/woocommerce/src/Container.php @@ -8,6 +8,7 @@ namespace Automattic\WooCommerce; use Automattic\WooCommerce\Internal\DependencyManagement\ExtendedContainer; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\DownloadPermissionsAdjusterServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\AssignDefaultCategoryServiceProvider; +use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\OrdersDataStoreServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ProductAttributesLookupServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ProxiesServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\RestockRefundedItemsAdjusterServiceProvider; @@ -38,6 +39,7 @@ final class Container implements \Psr\Container\ContainerInterface { private $service_providers = array( AssignDefaultCategoryServiceProvider::class, DownloadPermissionsAdjusterServiceProvider::class, + OrdersDataStoreServiceProvider::class, ProductAttributesLookupServiceProvider::class, ProxiesServiceProvider::class, RestockRefundedItemsAdjusterServiceProvider::class, diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php new file mode 100644 index 00000000000..30aedb9b05a --- /dev/null +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php @@ -0,0 +1,89 @@ +data_store = $data_store; + } + + /** + * Does the custom orders table exist in the database? + * + * @return bool True if the custom orders table exist in the database. + */ + public function check_orders_table_exists(): bool { + global $wpdb; + + $table_name = $this->data_store->get_orders_table_name(); + + $query = $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $this->$table_name ) ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + return $table_name === $wpdb->get_var( $query ); + } + + /** + * Is a table regeneration in progress? + * + * @return bool True if a table regeneration in currently progress + */ + public function data_regeneration_is_in_progress(): bool { + return 'yes' === get_option( self::CUSTOM_ORDERS_TABLE_DATA_REGENERATION_IN_PROGRESS ); + } + + /** + * Initiate a table regeneration process. + */ + public function initiate_regeneration() { + update_option( self::CUSTOM_ORDERS_TABLE_DATA_REGENERATION_IN_PROGRESS, 'yes' ); + update_option( self::CUSTOM_ORDERS_TABLE_DATA_REGENERATION_DONE_COUNT, 0 ); + + // TODO: Create the tables as appropriate, schedule the table filling in batches with Action Scheduler. + } + + /** + * How many orders have been processed as part of the custom orders table regeneration? + * + * @return int Number of orders already processed, 0 if no regeneration is in progress. + */ + public function get_regeneration_processed_orders_count(): int { + return get_option( self::CUSTOM_ORDERS_TABLE_DATA_REGENERATION_DONE_COUNT, 0 ); + } + + /** + * Delete the custom orders table and the associated information. + */ + public function delete_custom_orders_table() { + // TODO: Delete the tables and any associated data (e.g. options). + } +} diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/FeatureController.php b/plugins/woocommerce/src/Internal/DataStores/Orders/FeatureController.php new file mode 100644 index 00000000000..e953803de29 --- /dev/null +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/FeatureController.php @@ -0,0 +1,324 @@ +is_feature_visible = false; + + $this->init_hooks(); + } + + /** + * Initialize the hooks used by the class. + */ + private function init_hooks() { + add_filter( + 'woocommerce_order_data_store', + function ( $default_data_store ) { + return $this->get_data_store_instance( $default_data_store ); + }, + 10, + 999 + ); + + add_filter( + 'woocommerce_debug_tools', + function( $tools ) { + return $this->add_initiate_regeneration_entry_to_tools_array( $tools ); + }, + 1, + 999 + ); + + add_filter( + 'woocommerce_get_sections_advanced', + function( $sections ) { + return $this->get_settings_sections( $sections ); + }, + 1, + 999 + ); + + add_filter( + 'woocommerce_get_settings_advanced', + function ( $settings, $section_id ) { + return $this->get_settings( $settings, $section_id ); + }, + 2, + 999 + ); + } + + /** + * Class initialization, invoked by the DI container. + * + * @internal + * @param OrdersTableDataStore $data_store The data store to use. + * @param DataSynchronizer $data_synchronizer The data synchronizer to use. + */ + final public function init( OrdersTableDataStore $data_store, DataSynchronizer $data_synchronizer ) { + $this->data_store = $data_store; + $this->data_synchronizer = $data_synchronizer; + } + + /** + * Checks if the feature is visible (so that dedicated entries will be added to the debug tools page). + * + * @return bool True if the feature is visible. + */ + public function is_feature_visible(): bool { + return $this->is_feature_visible; + } + + /** + * Makes the feature visible, so that dedicated entries will be added to the debug tools page. + */ + public function show_feature() { + $this->is_feature_visible = true; + } + + /** + * Hides the feature, so that no entries will be added to the debug tools page. + */ + public function hide_feature() { + $this->is_feature_visible = false; + } + + /** + * Is the custom orders table usage enabled via settings? + * This can be true only if the feature is enabled and a table regeneration has been completed. + * + * @return bool True if the custom orders table usage is enabled + */ + public function custom_orders_table_usage_is_enabled(): bool { + return 'yes' === get_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ); + } + + /** + * Gets the instance of the orders data store to use. + * + * @param WC_Object_Data_Store_Interface|string $default_data_store The default data store (as received via the woocommerce_order_data_store hooks). + * @return WC_Object_Data_Store_Interface|string The actual data store to use. + */ + private function get_data_store_instance( $default_data_store ) { + if ( $this->is_feature_visible() && $this->custom_orders_table_usage_is_enabled() && ! $this->data_synchronizer->data_regeneration_is_in_progress() ) { + return $this->data_store; + } else { + return $default_data_store; + } + } + + /** + * Add an entry to Status - Tools to create or regenerate the custom orders table, + * and also an entry to delete the table as appropriate. + * + * @param array $tools_array The array of tools to add the tool to. + * @return array The updated array of tools- + */ + private function add_initiate_regeneration_entry_to_tools_array(array $tools_array ): array { + if ( ! $this->is_feature_visible() ) { + return $tools_array; + } + + $orders_table_exists = $this->data_synchronizer->check_orders_table_exists(); + $generation_is_in_progress = $this->data_synchronizer->data_regeneration_is_in_progress(); + + if ( $orders_table_exists ) { + $generate_item_name = __( 'Regenerate the custom orders table', 'woocommerce' ); + $generate_item_desc = __( 'This tool will regenerate the custom orders table data from existing orders data from the posts table. This process may take a while.', 'woocommerce' ); + $generate_item_return = __( 'Custom orders table is being regenerated', 'woocommerce' ); + $generate_item_button = __( 'Regenerate', 'woocommerce' ); + } else { + $generate_item_name = __( 'Create and fill custom orders table', 'woocommerce' ); + $generate_item_desc = __( 'This tool will create the custom orders table and fill it with existing orders data from the posts table. This process may take a while.', 'woocommerce' ); + $generate_item_return = __( 'Custom orders table is being filled', 'woocommerce' ); + $generate_item_button = __( 'Create', 'woocommerce' ); + } + + $entry = array( + 'name' => $generate_item_name, + 'desc' => $generate_item_desc, + 'requires_refresh' => true, + 'callback' => function() use ( $generate_item_return ) { + $this->initiate_regeneration_from_tools_page(); + return $generate_item_return; + }, + ); + + if ( $generation_is_in_progress ) { + $entry['button'] = sprintf( + /* translators: %d: How many orders have been processed so far. */ + __( 'Filling in progress (%d)', 'woocommerce' ), + $this->data_synchronizer->get_regeneration_processed_orders_count() + ); + $entry['disabled'] = true; + } else { + $entry['button'] = $generate_item_button; + } + + $tools_array['regenerate_custom_orders_table'] = $entry; + + if ( $orders_table_exists ) { + + // Delete the table. + + $tools_array['delete_custom_orders_table'] = array( + 'name' => __( 'Delete the custom orders table', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + __( 'This will delete the custom orders table. You can create it again with the "Create and fill custom orders table" tool.', 'woocommerce' ) + ), + 'button' => __( 'Delete', 'woocommerce' ), + 'requires_refresh' => true, + 'callback' => function () { + $this->delete_custom_orders_table(); + return __( 'Custom orders table has been deleted.', 'woocommerce' ); + }, + ); + } + + return $tools_array; + } + + /** + * Initiate the custom orders table (re)generation in response to the user pressing the tool button. + * + * @throws \Exception Can't initiate regeneration. + */ + private function initiate_regeneration_from_tools_page() { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput + if ( ! isset( $_REQUEST['_wpnonce'] ) || false === wp_verify_nonce( $_REQUEST['_wpnonce'], 'debug_action' ) ) { + throw new \Exception( 'Invalid nonce' ); + } + + $this->check_can_do_table_regeneration(); + $this->data_synchronizer->initiate_regeneration(); + } + + /** + * Can the custom orders table regeneration be started? + * + * @throws \Exception The table regeneration can't be started. + */ + private function check_can_do_table_regeneration() { + if ( ! $this->$this->is_feature_visible() ) { + throw new \Exception( "Can't do custom orders table regeneration: the feature isn't enabled" ); + } + + if ( $this->data_synchronizer->data_regeneration_is_in_progress() ) { + throw new \Exception( "Can't do custom orders table regeneration: regeneration is already in progress" ); + } + } + + /** + * Delete the custom orders table and any related options and data. + */ + private function delete_custom_orders_table() { + delete_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ); + $this->data_synchronizer->delete_custom_orders_table(); + } + + /** + * Get the settings sections for the "Advanced" tab, with a "Custom data stores" section added if appropriate. + * + * @param array $sections The original settings sections array. + * @return array The updated settings sections array. + */ + private function get_settings_sections( array $sections ): array { + if ( ! $this->is_feature_visible() || ! $this->data_synchronizer->check_orders_table_exists() ) { + return $sections; + } + + $sections['custom_data_stores'] = __( 'Custom data stores', 'woocommerce' ); + + return $sections; + } + + /** + * Get the settings for the "Custom data stores" section in the "Advanced" tab, + * with entries for managing the custom orders table if appropriate. + * + * @param array $settings The original settings array. + * @param string $section_id The settings section to get the settings for. + * @return array The updated settings array. + */ + private function get_settings( array $settings, string $section_id ): array { + if ( ! $this->is_feature_visible() || 'custom_data_stores' !== $section_id || ! $this->data_synchronizer->check_orders_table_exists() ) { + return $settings; + } + + $title_item = array( + 'title' => __( 'Custom orders table', 'woocommerce' ), + 'type' => 'title', + ); + + $regeneration_is_in_progress = $this->data_synchronizer->data_regeneration_is_in_progress(); + + if ( $regeneration_is_in_progress ) { + $title_item['desc'] = __( 'These settings are not available while the orders table regeneration is in progress.', 'woocommerce' ); + } + + $settings[] = $title_item; + + if ( ! $regeneration_is_in_progress ) { + $settings[] = array( + 'title' => __( 'Enable table usage', 'woocommerce' ), + 'desc' => __( 'Use the custom orders table as the main orders data store.', 'woocommerce' ), + 'id' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, + 'default' => 'no', + 'type' => 'checkbox', + 'checkboxgroup' => 'start', + ); + } + + $settings[] = array( 'type' => 'sectionend' ); + + return $settings; + } +} diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php new file mode 100644 index 00000000000..7e7796f4119 --- /dev/null +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -0,0 +1,143 @@ +prefix . 'wc_orders'; + } + + // TODO: Add methods for other table names as appropriate. + + //phpcs:disable Squiz.Commenting.FunctionComment.Missing + + public function get_total_refunded( $order ) { + // TODO: Implement get_total_refunded() method. + return 0; + } + + public function get_total_tax_refunded( $order ) { + // TODO: Implement get_total_tax_refunded() method. + return 0; + } + + public function get_total_shipping_refunded( $order ) { + // TODO: Implement get_total_shipping_refunded() method. + return 0; + } + + public function get_order_id_by_order_key( $order_key ) { + // TODO: Implement get_order_id_by_order_key() method. + return 0; + } + + public function get_order_count( $status ) { + // TODO: Implement get_order_count() method. + return 0; + } + + public function get_orders( $args = array() ) { + // TODO: Implement get_orders() method. + return array(); + } + + public function get_unpaid_orders( $date ) { + // TODO: Implement get_unpaid_orders() method. + return array(); + } + + public function search_orders( $term ) { + // TODO: Implement search_orders() method. + return array(); + } + + public function get_download_permissions_granted( $order ) { + // TODO: Implement get_download_permissions_granted() method. + false; + } + + public function set_download_permissions_granted( $order, $set ) { + // TODO: Implement set_download_permissions_granted() method. + } + + public function get_recorded_sales( $order ) { + // TODO: Implement get_recorded_sales() method. + return false; + } + + public function set_recorded_sales( $order, $set ) { + // TODO: Implement set_recorded_sales() method. + } + + public function get_recorded_coupon_usage_counts( $order ) { + // TODO: Implement get_recorded_coupon_usage_counts() method. + return false; + } + + public function set_recorded_coupon_usage_counts( $order, $set ) { + // TODO: Implement set_recorded_coupon_usage_counts() method. + } + + public function get_order_type( $order_id ) { + // TODO: Implement get_order_type() method. + return 'shop_order'; + } + + public function create( &$order ) { + throw new \Exception( 'Unimplemented' ); + } + + public function update( &$order ) { + throw new \Exception( 'Unimplemented' ); + } + + public function get_coupon_held_keys( $order, $coupon_id = null ) { + return array(); + } + + public function get_coupon_held_keys_for_users( $order, $coupon_id = null ) { + return array(); + } + + public function set_coupon_held_keys( $order, $held_keys, $held_keys_for_user ) { + throw new \Exception( 'Unimplemented' ); + } + + public function release_held_coupons( $order, $save = true ) { + throw new \Exception( 'Unimplemented' ); + } + + public function get_stock_reduced( $order ) { + return false; + } + + public function set_stock_reduced( $order, $set ) { + throw new \Exception( 'Unimplemented' ); + } + + public function query( $query_vars ) { + return array(); + } + + public function get_order_item_type( $order, $order_item_id ) { + return 'line_item'; + } + + //phpcs:enable Squiz.Commenting.FunctionComment.Missing +} diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrdersDataStoreServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrdersDataStoreServiceProvider.php new file mode 100644 index 00000000000..98f17a418eb --- /dev/null +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrdersDataStoreServiceProvider.php @@ -0,0 +1,37 @@ +share( DataSynchronizer::class )->addArgument( OrdersTableDataStore::class ); + $this->share( FeatureController::class )->addArguments( array( OrdersTableDataStore::class, DataSynchronizer::class ) ); + $this->share( OrdersTableDataStore::class ); + } +}