392 lines
14 KiB
PHP
392 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* DataRegenerator class file.
|
|
*/
|
|
|
|
namespace Automattic\WooCommerce\Internal\ProductAttributesLookup;
|
|
|
|
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
|
|
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
|
|
|
defined( 'ABSPATH' ) || exit;
|
|
|
|
/**
|
|
* This class handles the (re)generation of the product attributes lookup table.
|
|
* It schedules the regeneration in small product batches by itself, so it can be used outside the
|
|
* regular WooCommerce data regenerations mechanism.
|
|
*
|
|
* After the regeneration is completed a wp_wc_product_attributes_lookup table will exist with entries for
|
|
* all the products that existed when initiate_regeneration was invoked; entries for products created after that
|
|
* are supposed to be created/updated by the appropriate data store classes (or by the code that uses
|
|
* the data store classes) whenever a product is created/updated.
|
|
*
|
|
* Additionally, after the regeneration is completed a 'woocommerce_attribute_lookup_enabled' option
|
|
* with a value of 'no' will have been created.
|
|
*
|
|
* This class also adds two entries to the Status - Tools menu: one for manually regenerating the table contents,
|
|
* and another one for enabling or disabling the actual lookup table usage.
|
|
*/
|
|
class DataRegenerator {
|
|
|
|
const PRODUCTS_PER_GENERATION_STEP = 10;
|
|
|
|
/**
|
|
* The data store to use.
|
|
*
|
|
* @var LookupDataStore
|
|
*/
|
|
private $data_store;
|
|
|
|
/**
|
|
* The lookup table name.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $lookup_table_name;
|
|
|
|
/**
|
|
* DataRegenerator constructor.
|
|
*/
|
|
public function __construct() {
|
|
global $wpdb;
|
|
|
|
$this->lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup';
|
|
|
|
add_filter(
|
|
'woocommerce_debug_tools',
|
|
function( $tools ) {
|
|
return $this->add_initiate_regeneration_entry_to_tools_array( $tools );
|
|
},
|
|
1,
|
|
999
|
|
);
|
|
|
|
add_action(
|
|
'woocommerce_run_product_attribute_lookup_regeneration_callback',
|
|
function () {
|
|
$this->run_regeneration_step_callback();
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Class initialization, invoked by the DI container.
|
|
*
|
|
* @internal
|
|
* @param LookupDataStore $data_store The data store to use.
|
|
*/
|
|
final public function init( LookupDataStore $data_store ) {
|
|
$this->data_store = $data_store;
|
|
}
|
|
|
|
/**
|
|
* Initialize the regeneration procedure:
|
|
* deletes the lookup table and related options if they exist,
|
|
* then it creates the table and runs the first step of the regeneration process.
|
|
*
|
|
* This is the method that should be used as a callback for a data regeneration in wc-update-functions, e.g.:
|
|
*
|
|
* function wc_update_XX_regenerate_product_attributes_lookup_table() {
|
|
* wc_get_container()->get(DataRegenerator::class)->initiate_regeneration();
|
|
* return false;
|
|
* }
|
|
*
|
|
* (Note how we are returning "false" since the class handles the step scheduling by itself).
|
|
*/
|
|
public function initiate_regeneration() {
|
|
$this->enable_or_disable_lookup_table_usage( false );
|
|
|
|
$this->delete_all_attributes_lookup_data();
|
|
$products_exist = $this->initialize_table_and_data();
|
|
if ( $products_exist ) {
|
|
$this->enqueue_regeneration_step_run();
|
|
} else {
|
|
$this->finalize_regeneration();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete all the existing data related to the lookup table, including the table itself.
|
|
*
|
|
* Shortcut to run this method in case the debug tools UI isn't available or for quick debugging:
|
|
*
|
|
* wp eval "wc_get_container()->get(Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator::class)->delete_all_attributes_lookup_data();"
|
|
*/
|
|
public function delete_all_attributes_lookup_data() {
|
|
global $wpdb;
|
|
|
|
delete_option( 'woocommerce_attribute_lookup_enabled' );
|
|
delete_option( 'woocommerce_attribute_lookup_last_product_id_to_process' );
|
|
delete_option( 'woocommerce_attribute_lookup_last_products_page_processed' );
|
|
$this->data_store->unset_regeneration_in_progress_flag();
|
|
|
|
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
|
$wpdb->query( 'DROP TABLE IF EXISTS ' . $this->lookup_table_name );
|
|
}
|
|
|
|
/**
|
|
* Create the lookup table and initialize the options that will be temporarily used
|
|
* while the regeneration is in progress.
|
|
*
|
|
* @return bool True if there's any product at all in the database, false otherwise.
|
|
*/
|
|
private function initialize_table_and_data() {
|
|
global $wpdb;
|
|
|
|
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
|
|
$wpdb->query(
|
|
'
|
|
CREATE TABLE ' . $this->lookup_table_name . '(
|
|
product_id bigint(20) NOT NULL,
|
|
product_or_parent_id bigint(20) NOT NULL,
|
|
taxonomy varchar(32) NOT NULL,
|
|
term_id bigint(20) NOT NULL,
|
|
is_variation_attribute tinyint(1) NOT NULL,
|
|
in_stock tinyint(1) NOT NULL
|
|
);
|
|
'
|
|
);
|
|
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
|
|
|
|
$last_existing_product_id =
|
|
WC()->call_function(
|
|
'wc_get_products',
|
|
array(
|
|
'return' => 'ids',
|
|
'limit' => 1,
|
|
'orderby' => array(
|
|
'ID' => 'DESC',
|
|
),
|
|
)
|
|
);
|
|
|
|
if ( ! $last_existing_product_id ) {
|
|
// No products exist, nothing to (re)generate.
|
|
return false;
|
|
}
|
|
|
|
$this->data_store->set_regeneration_in_progress_flag();
|
|
update_option( 'woocommerce_attribute_lookup_last_product_id_to_process', current( $last_existing_product_id ) );
|
|
update_option( 'woocommerce_attribute_lookup_last_products_page_processed', 0 );
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Action scheduler callback, performs one regeneration step and then
|
|
* schedules the next step if necessary.
|
|
*/
|
|
private function run_regeneration_step_callback() {
|
|
if ( ! $this->data_store->regeneration_is_in_progress() ) {
|
|
return;
|
|
}
|
|
|
|
$result = $this->do_regeneration_step();
|
|
if ( $result ) {
|
|
$this->enqueue_regeneration_step_run();
|
|
} else {
|
|
$this->finalize_regeneration();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enqueue one regeneration step in action scheduler.
|
|
*/
|
|
private function enqueue_regeneration_step_run() {
|
|
$queue = WC()->get_instance_of( \WC_Queue::class );
|
|
$queue->schedule_single(
|
|
WC()->call_function( 'time' ) + 1,
|
|
'woocommerce_run_product_attribute_lookup_regeneration_callback',
|
|
array(),
|
|
'woocommerce-db-updates'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Perform one regeneration step: grabs a chunk of products and creates
|
|
* the appropriate entries for them in the lookup table.
|
|
*
|
|
* @return bool True if more steps need to be run, false otherwise.
|
|
*/
|
|
private function do_regeneration_step() {
|
|
$last_products_page_processed = get_option( 'woocommerce_attribute_lookup_last_products_page_processed' );
|
|
$current_products_page = (int) $last_products_page_processed + 1;
|
|
|
|
$product_ids = WC()->call_function(
|
|
'wc_get_products',
|
|
array(
|
|
'limit' => self::PRODUCTS_PER_GENERATION_STEP,
|
|
'page' => $current_products_page,
|
|
'orderby' => array(
|
|
'ID' => 'ASC',
|
|
),
|
|
'return' => 'ids',
|
|
)
|
|
);
|
|
|
|
if ( ! $product_ids ) {
|
|
return false;
|
|
}
|
|
|
|
foreach ( $product_ids as $id ) {
|
|
$this->data_store->create_data_for_product( $id );
|
|
}
|
|
|
|
update_option( 'woocommerce_attribute_lookup_last_products_page_processed', $current_products_page );
|
|
|
|
$last_product_id_to_process = get_option( 'woocommerce_attribute_lookup_last_product_id_to_process' );
|
|
return end( $product_ids ) < $last_product_id_to_process;
|
|
}
|
|
|
|
/**
|
|
* Cleanup/final option setup after the regeneration has been completed.
|
|
*/
|
|
private function finalize_regeneration() {
|
|
delete_option( 'woocommerce_attribute_lookup_last_product_id_to_process' );
|
|
delete_option( 'woocommerce_attribute_lookup_last_products_page_processed' );
|
|
update_option( 'woocommerce_attribute_lookup_enabled', 'no' );
|
|
$this->data_store->unset_regeneration_in_progress_flag();
|
|
}
|
|
|
|
/**
|
|
* Add a 'Regenerate product attributes lookup table' entry to the Status - Tools page.
|
|
*
|
|
* @param array $tools_array The tool definitions array that is passed ro the woocommerce_debug_tools filter.
|
|
* @return array The tools array with the entry added.
|
|
*/
|
|
private function add_initiate_regeneration_entry_to_tools_array( array $tools_array ) {
|
|
if ( ! $this->data_store->is_feature_visible() ) {
|
|
return $tools_array;
|
|
}
|
|
|
|
$lookup_table_exists = $this->data_store->check_lookup_table_exists();
|
|
$generation_is_in_progress = $this->data_store->regeneration_is_in_progress();
|
|
|
|
// Regenerate table.
|
|
|
|
if ( $lookup_table_exists ) {
|
|
$generate_item_name = __( 'Regenerate the product attributes lookup table', 'woocommerce' );
|
|
$generate_item_desc = __( 'This tool will regenerate the product attributes lookup table data from existing product(s) data. This process may take a while.', 'woocommerce' );
|
|
$generate_item_return = __( 'Product attributes lookup table data is regenerating', 'woocommerce' );
|
|
$generate_item_button = __( 'Regenerate', 'woocommerce' );
|
|
} else {
|
|
$generate_item_name = __( 'Create and fill product attributes lookup table', 'woocommerce' );
|
|
$generate_item_desc = __( 'This tool will create the product attributes lookup table data and fill it with existing products data. This process may take a while.', 'woocommerce' );
|
|
$generate_item_return = __( 'Product attributes lookup 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 ( $lookup_table_exists ) {
|
|
$entry['selector'] = array(
|
|
'description' => __( 'Select a product to regenerate the data for, or leave empty for a full table regeneration:', 'woocommerce' ),
|
|
'class' => 'wc-product-search',
|
|
'search_action' => 'woocommerce_json_search_products',
|
|
'name' => 'regenerate_product_attribute_lookup_data_product_id',
|
|
'placeholder' => esc_attr__( 'Search for a product…', 'woocommerce' ),
|
|
);
|
|
}
|
|
|
|
if ( $generation_is_in_progress ) {
|
|
$entry['button'] = sprintf(
|
|
/* translators: %d: How many products have been processed so far. */
|
|
__( 'Filling in progress (%d)', 'woocommerce' ),
|
|
get_option( 'woocommerce_attribute_lookup_last_products_page_processed', 0 ) * self::PRODUCTS_PER_GENERATION_STEP
|
|
);
|
|
$entry['disabled'] = true;
|
|
} else {
|
|
$entry['button'] = $generate_item_button;
|
|
}
|
|
|
|
$tools_array['regenerate_product_attributes_lookup_table'] = $entry;
|
|
|
|
if ( $lookup_table_exists ) {
|
|
|
|
// Delete the table.
|
|
|
|
$tools_array['delete_product_attributes_lookup_table'] = array(
|
|
'name' => __( 'Delete the product attributes lookup table', 'woocommerce' ),
|
|
'desc' => sprintf(
|
|
'<strong class="red">%1$s</strong> %2$s',
|
|
__( 'Note:', 'woocommerce' ),
|
|
__( 'This will delete the product attributes lookup table. You can create it again with the "Create and fill product attributes lookup table" tool.', 'woocommerce' )
|
|
),
|
|
'button' => __( 'Delete', 'woocommerce' ),
|
|
'requires_refresh' => true,
|
|
'callback' => function () {
|
|
$this->delete_all_attributes_lookup_data();
|
|
return __( 'Product attributes lookup table has been deleted.', 'woocommerce' );
|
|
},
|
|
);
|
|
}
|
|
|
|
return $tools_array;
|
|
}
|
|
|
|
/**
|
|
* Callback to initiate the regeneration process from the Status - Tools page.
|
|
*
|
|
* @throws \Exception The regeneration is already in progress.
|
|
*/
|
|
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' );
|
|
}
|
|
|
|
if ( isset( $_REQUEST['regenerate_product_attribute_lookup_data_product_id'] ) ) {
|
|
$product_id = (int) $_REQUEST['regenerate_product_attribute_lookup_data_product_id'];
|
|
$this->check_can_do_lookup_table_regeneration( $product_id );
|
|
$this->data_store->create_data_for_product( $product_id );
|
|
} else {
|
|
$this->check_can_do_lookup_table_regeneration();
|
|
$this->initiate_regeneration();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enable or disable the actual lookup table usage.
|
|
*
|
|
* @param bool $enable True to enable, false to disable.
|
|
* @throws \Exception A lookup table regeneration is currently in progress.
|
|
*/
|
|
private function enable_or_disable_lookup_table_usage( $enable ) {
|
|
if ( $this->data_store->regeneration_is_in_progress() ) {
|
|
throw new \Exception( "Can't enable or disable the attributes lookup table usage while it's regenerating." );
|
|
}
|
|
|
|
update_option( 'woocommerce_attribute_lookup_enabled', $enable ? 'yes' : 'no' );
|
|
}
|
|
|
|
/**
|
|
* Check if everything is good to go to perform a complete or per product lookup table data regeneration
|
|
* and throw an exception if not.
|
|
*
|
|
* @param mixed $product_id The product id to check the regeneration viability for, or null to check if a complete regeneration is possible.
|
|
* @throws \Exception Something prevents the regeneration from starting.
|
|
*/
|
|
private function check_can_do_lookup_table_regeneration( $product_id = null ) {
|
|
if ( ! $this->data_store->is_feature_visible() ) {
|
|
throw new \Exception( "Can't do product attribute lookup data regeneration: feature is not visible" );
|
|
}
|
|
if ( $product_id && ! $this->data_store->check_lookup_table_exists() ) {
|
|
throw new \Exception( "Can't do product attribute lookup data regeneration: lookup table doesn't exist" );
|
|
}
|
|
if ( $this->data_store->regeneration_is_in_progress() ) {
|
|
throw new \Exception( "Can't do product attribute lookup data regeneration: regeneration is already in progress" );
|
|
}
|
|
if ( $product_id && ! wc_get_product( $product_id ) ) {
|
|
throw new \Exception( "Can't do product attribute lookup data regeneration: product doesn't exist" );
|
|
}
|
|
}
|
|
}
|