Merge pull request #31692 from woocommerce/scafolding-for-custom-order-tables

Add scafolding for the custom orders table feature.
This commit is contained in:
Néstor Soriano 2022-02-14 15:02:16 +01:00 committed by GitHub
commit fbc67db556
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 597 additions and 0 deletions

View File

@ -9,6 +9,7 @@
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Internal\AssignDefaultCategory;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
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( CustomOrdersTableController::class );
}
/**

View File

@ -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,

View File

@ -0,0 +1,324 @@
<?php
/**
* CustomOrdersTableController class file.
*/
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
defined( 'ABSPATH' ) || exit;
/**
* This is the main class that controls the custom orders table feature. Its responsibilities are:
*
* - Allowing to enable and disable the feature while it's in development (show_feature method)
* - Displaying UI components (entries in the tools page and in settings)
* - Providing the proper data store for orders via 'woocommerce_order_data_store' hook
*
* ...and in general, any functionality that doesn't imply database access.
*/
class CustomOrdersTableController {
/**
* The name of the option for enabling the usage of the custom orders table
*/
const CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION = 'woocommerce_custom_orders_table_enabled';
/**
* The data store object to use.
*
* @var OrdersTableDataStore
*/
private $data_store;
/**
* The data synchronizer object to use.
*
* @var DataSynchronizer
*/
private $data_synchronizer;
/**
* Is the feature visible?
*
* @var bool
*/
private $is_feature_visible;
/**
* Class constructor.
*/
public function __construct() {
$this->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 );
},
999,
1
);
add_filter(
'woocommerce_debug_tools',
function( $tools ) {
return $this->add_initiate_regeneration_entry_to_tools_array( $tools );
},
999,
1
);
add_filter(
'woocommerce_get_sections_advanced',
function( $sections ) {
return $this->get_settings_sections( $sections );
},
999,
1
);
add_filter(
'woocommerce_get_settings_advanced',
function ( $settings, $section_id ) {
return $this->get_settings( $settings, $section_id );
},
999,
2
);
}
/**
* 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(
'<strong class="red">%1$s</strong> %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->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;
}
}

View File

@ -0,0 +1,89 @@
<?php
/**
* DataSynchronizer class file.
*/
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
defined( 'ABSPATH' ) || exit;
/**
* This class handles the data migration/synchronization for the custom orders table. Its responsibilites are:
*
* - Performing the initial table creation and filling (triggered by initiate_regeneration)
* - Synchronizing changes between the custom orders table and the posts table whenever changes in orders happen.
*/
class DataSynchronizer {
const CUSTOM_ORDERS_TABLE_DATA_REGENERATION_IN_PROGRESS = 'woocommerce_custom_orders_table_data_regeneration_in_progress';
const CUSTOM_ORDERS_TABLE_DATA_REGENERATION_DONE_COUNT = 'woocommerce_custom_orders_table_data_regeneration_done_count';
/**
* The data store object to use.
*
* @var OrdersTableDataStore
*/
private $data_store;
// TODO: Add a constructor to handle hooks as appropriate.
/**
* Class initialization, invoked by the DI container.
*
* @internal
* @param OrdersTableDataStore $data_store The data store to use.
*/
final public function init( OrdersTableDataStore $data_store ) {
$this->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 (int)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).
}
}

View File

@ -0,0 +1,143 @@
<?php
/**
* OrdersTableDataStore class file.
*/
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
defined( 'ABSPATH' ) || exit;
/**
* This class is the standard data store to be used when the custom orders table is in use.
*/
class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements \WC_Object_Data_Store_Interface, \WC_Order_Data_Store_Interface {
/**
* Get the custom orders table name.
*
* @return string The custom orders table name.
*/
public function get_orders_table_name() {
global $wpdb;
return $wpdb->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
}

View File

@ -0,0 +1,37 @@
<?php
/**
* ProductAttributesLookupServiceProvider class file.
*/
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
/**
* Service provider for the ProductAttributesLookupServiceProvider namespace.
*/
class OrdersDataStoreServiceProvider extends AbstractServiceProvider {
/**
* The classes/interfaces that are serviced by this service provider.
*
* @var array
*/
protected $provides = array(
DataSynchronizer::class,
CustomOrdersTableController::class,
OrdersTableDataStore::class,
);
/**
* Register the classes.
*/
public function register() {
$this->share( DataSynchronizer::class )->addArgument( OrdersTableDataStore::class );
$this->share( CustomOrdersTableController::class )->addArguments( array( OrdersTableDataStore::class, DataSynchronizer::class ) );
$this->share( OrdersTableDataStore::class );
}
}