From 0192ed0b936378b801cdf01c7122471c9f3afec5 Mon Sep 17 00:00:00 2001 From: Nestor Soriano Date: Tue, 15 Jun 2021 16:33:29 +0200 Subject: [PATCH] Change LookupDataStore to allow updates being done in a scheduled action. There's a new option, 'woocommerce_attribute_lookup__direct_updates'. When set to 'yes', updates to the lookup table are performed as soon as the change happen; otherwise, a scheduled action will do it, the hook name is 'woocommerce_run_product_attribute_lookup_update_callback' (the existing hook in the DataRegenerator class is renamed to 'woocommerce_run_product_attribute_lookup_update_callback') Also, the settings page has a new "Advanced" section with a checkbox to control the value of that new option; the section is visible only when the feature has been enabled via LookupDataStore::show_feature. --- includes/class-woocommerce.php | 6 +- .../DataRegenerator.php | 4 +- .../LookupDataStore.php | 116 +++++++++++++++++- .../DataRegeneratorTest.php | 10 +- 4 files changed, 123 insertions(+), 13 deletions(-) diff --git a/includes/class-woocommerce.php b/includes/class-woocommerce.php index c75a69cf2c9..b05bdb26e02 100644 --- a/includes/class-woocommerce.php +++ b/includes/class-woocommerce.php @@ -8,10 +8,11 @@ defined( 'ABSPATH' ) || exit; -use Automattic\WooCommerce\Internal\DownloadPermissionsAdjuster; -use Automattic\WooCommerce\Internal\RestockRefundedItemsAdjuster; use Automattic\WooCommerce\Internal\AssignDefaultCategory; +use Automattic\WooCommerce\Internal\DownloadPermissionsAdjuster; use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator; +use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore; +use Automattic\WooCommerce\Internal\RestockRefundedItemsAdjuster; use Automattic\WooCommerce\Proxies\LegacyProxy; /** @@ -213,6 +214,7 @@ final class WooCommerce { wc_get_container()->get( DownloadPermissionsAdjuster::class ); wc_get_container()->get( AssignDefaultCategory::class ); wc_get_container()->get( DataRegenerator::class ); + wc_get_container()->get( LookupDataStore::class ); wc_get_container()->get( RestockRefundedItemsAdjuster::class ); } diff --git a/src/Internal/ProductAttributesLookup/DataRegenerator.php b/src/Internal/ProductAttributesLookup/DataRegenerator.php index d027fb2b8a5..88f0539d799 100644 --- a/src/Internal/ProductAttributesLookup/DataRegenerator.php +++ b/src/Internal/ProductAttributesLookup/DataRegenerator.php @@ -61,7 +61,7 @@ class DataRegenerator { ); add_action( - 'woocommerce_run_product_attribute_lookup_update_callback', + 'woocommerce_run_product_attribute_lookup_regeneration_callback', function () { $this->run_regeneration_step_callback(); } @@ -202,7 +202,7 @@ CREATE TABLE ' . $this->lookup_table_name . '( $queue = WC()->get_instance_of( \WC_Queue::class ); $queue->schedule_single( WC()->call_function( 'time' ) + 1, - 'woocommerce_run_product_attribute_lookup_update_callback', + 'woocommerce_run_product_attribute_lookup_regeneration_callback', array(), 'woocommerce-db-updates' ); diff --git a/src/Internal/ProductAttributesLookup/LookupDataStore.php b/src/Internal/ProductAttributesLookup/LookupDataStore.php index d5c242fba5f..c9945c069fd 100644 --- a/src/Internal/ProductAttributesLookup/LookupDataStore.php +++ b/src/Internal/ProductAttributesLookup/LookupDataStore.php @@ -56,6 +56,61 @@ class LookupDataStore { $this->is_feature_visible = false; $this->lookup_table_exists = $this->check_lookup_table_exists(); + + $this->init_hooks(); + } + + /** + * Initialize the hooks used by the class. + */ + private function init_hooks() { + add_action( + 'woocommerce_run_product_attribute_lookup_update_callback', + function ( $product_id, $action ) { + $this->run_update_callback( $product_id, $action ); + }, + 10, + 2 + ); + + add_filter( + 'woocommerce_get_sections_products', + function ( $products ) { + if ( $this->is_feature_visible() ) { + $products['advanced'] = __( 'Advanced', 'woocommerce' ); + } + return $products; + }, + 100, + 1 + ); + + add_filter( + 'woocommerce_get_settings_products', + function ( $settings, $section_id ) { + if ( 'advanced' === $section_id && $this->is_feature_visible() ) { + $settings[] = + array( + 'title' => __( 'Product attributes lookup table', 'woocommerce' ), + 'type' => 'title', + ); + + $settings[] = array( + 'title' => __( 'Direct updates', 'woocommerce' ), + 'desc' => __( 'Update the table directly upon product changes, instead of scheduling a deferred update.', 'woocommerce' ), + 'id' => 'woocommerce_attribute_lookup__direct_updates', + 'default' => 'no', + 'type' => 'checkbox', + 'checkboxgroup' => 'start', + ); + + $settings[] = array( 'type' => 'sectionend' ); + } + return $settings; + }, + 100, + 2 + ); } /** @@ -129,9 +184,62 @@ AND table_name = %s;', $product = WC()->call_function( 'wc_get_product', $product ); } - $update_type = $this->get_update_type( $changeset ); + $action = $this->get_update_action( $changeset ); + $this->maybe_schedule_update( $product->get_id(), $action ); + } - switch ( $update_type ) { + /** + * Schedule an update of the product attributes lookup table for a given product. + * If an update for the same action is already scheduled, nothing is done. + * + * If the 'woocommerce_attribute_lookup__direct_update' option is set to 'yes', + * the update is done directly, without scheduling. + * + * @param int $product_id The product id to schedule the update for. + * @param int $action The action to perform, one of the ACTION_ constants. + */ + private function maybe_schedule_update( int $product_id, int $action ) { + if ( 'yes' === get_option( 'woocommerce_attribute_lookup__direct_updates' ) ) { + $this->run_update_callback( $product_id, $action ); + return; + } + + $args = array( $product_id, $action ); + + $queue = WC()->get_instance_of( \WC_Queue::class ); + $already_scheduled = $queue->search( + array( + 'hook' => 'woocommerce_run_product_attribute_lookup_update_callback', + 'args' => $args, + 'status' => \ActionScheduler_Store::STATUS_PENDING, + ), + 'ids' + ); + + if ( empty( $already_scheduled ) ) { + $queue->schedule_single( + WC()->call_function( 'time' ) + 1, + 'woocommerce_run_product_attribute_lookup_update_callback', + $args, + 'woocommerce-db-updates' + ); + } + } + + /** + * Perform an update of the lookup table for a specific product. + * + * @param int $product_id The product id to perform the update for. + * @param int $action The action to perform, one of the ACTION_ constants. + */ + private function run_update_callback( int $product_id, int $action ) { + if ( ! $this->lookup_table_exists ) { + return; + } + + $product = WC()->call_function( 'wc_get_product', $product_id ); + + switch ( $action ) { case self::ACTION_INSERT: $this->delete_data_for( $product->get_id() ); $this->create_data_for( $product ); @@ -151,7 +259,7 @@ AND table_name = %s;', * @param array|null $changeset The changeset received by on_product_changed. * @return int One of the ACTION_ constants. */ - private function get_update_type( $changeset ) { + private function get_update_action( $changeset ) { if ( is_null( $changeset ) ) { // No changeset at all means that the product is new. return self::ACTION_INSERT; @@ -225,7 +333,7 @@ AND table_name = %s;', $product_id = $product; } - $this->delete_data_for( $product_id ); + $this->maybe_schedule_update( $product_id, self::ACTION_DELETE ); } /** diff --git a/tests/php/src/Internal/ProductAttributesLookup/DataRegeneratorTest.php b/tests/php/src/Internal/ProductAttributesLookup/DataRegeneratorTest.php index 8265371dda7..2699cc81537 100644 --- a/tests/php/src/Internal/ProductAttributesLookup/DataRegeneratorTest.php +++ b/tests/php/src/Internal/ProductAttributesLookup/DataRegeneratorTest.php @@ -58,7 +58,7 @@ class DataRegeneratorTest extends \WC_Unit_Test_Case { // phpcs:enable Squiz.Commenting // This is needed to prevent the hook to act on the already registered LookupDataStore class. - remove_all_actions( 'woocommerce_run_product_attribute_lookup_update_callback' ); + remove_all_actions( 'woocommerce_run_product_attribute_lookup_regeneration_callback' ); $container = wc_get_container(); $container->reset_all_resolved(); @@ -128,7 +128,7 @@ class DataRegeneratorTest extends \WC_Unit_Test_Case { 'method' => 'schedule_single', 'args' => array(), 'timestamp' => 1001, - 'hook' => 'woocommerce_run_product_attribute_lookup_update_callback', + 'hook' => 'woocommerce_run_product_attribute_lookup_regeneration_callback', 'group' => 'woocommerce-db-updates', ); $actual_enqueued = current( $this->queue->methods_called ); @@ -188,7 +188,7 @@ class DataRegeneratorTest extends \WC_Unit_Test_Case { update_option( 'woocommerce_attribute_lookup__last_products_page_processed', 7 ); - do_action( 'woocommerce_run_product_attribute_lookup_update_callback' ); + do_action( 'woocommerce_run_product_attribute_lookup_regeneration_callback' ); $this->assertEquals( array( 1, 2, 3 ), $this->lookup_data_store->passed_products ); $this->assertEquals( array( 8 ), $requested_products_pages ); @@ -198,7 +198,7 @@ class DataRegeneratorTest extends \WC_Unit_Test_Case { 'method' => 'schedule_single', 'args' => array(), 'timestamp' => 1001, - 'hook' => 'woocommerce_run_product_attribute_lookup_update_callback', + 'hook' => 'woocommerce_run_product_attribute_lookup_regeneration_callback', 'group' => 'woocommerce-db-updates', ); $actual_enqueued = current( $this->queue->methods_called ); @@ -233,7 +233,7 @@ class DataRegeneratorTest extends \WC_Unit_Test_Case { $this->sut->initiate_regeneration(); $this->queue->methods_called = array(); - do_action( 'woocommerce_run_product_attribute_lookup_update_callback' ); + do_action( 'woocommerce_run_product_attribute_lookup_regeneration_callback' ); $this->assertEquals( $product_ids, $this->lookup_data_store->passed_products ); $this->assertFalse( get_option( 'woocommerce_attribute_lookup__last_product_id_to_process' ) );