Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
Konamiman committed May 22, 2024
1 parent 7d286c8 commit e4e440c
Show file tree
Hide file tree
Showing 8 changed files with 309 additions and 24 deletions.
1 change: 0 additions & 1 deletion plugins/woocommerce/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,6 @@
"node_modules/@woocommerce/e2e-core-tests/CHANGELOG.md",
"node_modules/@woocommerce/api/dist/",
"node_modules/@woocommerce/admin-e2e-tests/build",
"node_modules/@woocommerce/classic-assets/build",
"node_modules/@woocommerce/block-library/build",
"node_modules/@woocommerce/block-library/blocks.ini",
"node_modules/@woocommerce/admin-library/build",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,12 @@ private function disable_core( array $args, array $assoc_args ) {
* <product-id>
* : The id of the product for which the data will be regenerated.
*
* [--use-data-store]
* : Force the usage of the WooCommerce data store classes for the regeneration, instead of direct database access is possible, even if the posts table is currently used for storing products.
*
* ## EXAMPLES
*
* wp wc palt regenerate 34
* wp wc palt regenerate 34 --use-data-store
*
* @param array $args Positional arguments passed to the command.
* @param array $assoc_args Associative arguments (options) passed to the command.
Expand All @@ -154,8 +157,9 @@ public function regenerate_for_product( array $args = array(), array $assoc_args
private function regenerate_for_product_core( array $args = array(), array $assoc_args = array() ) {
$product_id = current( $args );
$this->data_regenerator->check_can_do_lookup_table_regeneration( $product_id );
$force_use_data_store = array_key_exists( 'use-data-store', $assoc_args );
$start_time = microtime( true );
$this->lookup_data_store->create_data_for_product( $product_id );
$this->lookup_data_store->create_data_for_product( $product_id, $force_use_data_store );
$total_time = microtime( true ) - $start_time;
WP_CLI::success( sprintf( 'Attributes lookup data for product %d regenerated in %f seconds.', $product_id, $total_time ) );
}
Expand Down Expand Up @@ -338,6 +342,9 @@ private function initiate_regeneration_core( array $args, array $assoc_args ) {
* [--from-scratch]
* : Start table regeneration from scratch even if a regeneration is already in progress.
*
* [--use-data-store]
* : Force the usage of the WooCommerce data store classes for the regeneration, instead of direct database access is possible, even if the posts table is currently used for storing products.
*
* [--batch-size=<size>]
* : How many products to process in each iteration of the loop.
* ---
Expand Down Expand Up @@ -401,10 +408,12 @@ private function regenerate_core( array $args = array(), array $assoc_args = arr

$this->data_regenerator->cancel_regeneration_scheduled_action();

$force_use_data_store = array_key_exists( 'use-data-store', $assoc_args );
$progress = WP_CLI\Utils\make_progress_bar( '', $last_product_id - $processed_count );
$progress->display();
$last_product_processed = 0;
$this->log( "Regenerating %W{$table_name}%n..." );
while ( $this->data_regenerator->do_regeneration_step( $batch_size ) ) {
while ( $this->data_regenerator->do_regeneration_step( $batch_size, $force_use_data_store ) ) {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$last_product_in_table = $wpdb->get_var( 'select max(product_or_parent_id) from ' . $table_name ) ?? 0;
$progress->tick( $last_product_in_table - $last_product_processed );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,10 @@ private function enqueue_regeneration_step_run() {
* the appropriate entries for them in the lookup table.
*
* @param int|null $step_size How many products to process, by default PRODUCTS_PER_GENERATION_STEP will be used.
* @param bool $force_use_data_store Force the usage of the WooCommerce data store classes for the regeneration, instead of direct database access is possible, even if the posts table is currently used for storing products.
* @return bool True if more steps need to be run, false otherwise.
*/
public function do_regeneration_step( ?int $step_size = self::PRODUCTS_PER_GENERATION_STEP ) {
public function do_regeneration_step( ?int $step_size = self::PRODUCTS_PER_GENERATION_STEP, bool $force_use_data_store = false ) {
/**
* Filter to alter the count of products that will be processed in each step of the product attributes lookup table regeneration process.
*
Expand Down Expand Up @@ -235,7 +236,7 @@ public function do_regeneration_step( ?int $step_size = self::PRODUCTS_PER_GENER
}

foreach ( $product_ids as $id ) {
$this->data_store->create_data_for_product( $id );
$this->data_store->create_data_for_product( $id, $force_use_data_store );
}

$products_already_processed += count( $product_ids );
Expand Down Expand Up @@ -545,7 +546,9 @@ private function run_woocommerce_installed_callback() {
// If the lookup table has data, or if it's empty because there are no products yet, we're good.
// Otherwise (lookup table is empty but products exist) we need to initiate a regeneration if one isn't already in progress.
if ( $this->data_store->lookup_table_has_data() || ! $this->get_last_existing_product_id() ) {
$this->delete_all_attributes_lookup_data( false );
$must_enable = get_option( 'woocommerce_attribute_lookup_enabled' ) !== 'no';
$this->delete_all_attributes_lookup_data( false );
update_option( 'woocommerce_attribute_lookup_enabled', $must_enable ? 'yes' : 'no' );
} else {
$this->initiate_regeneration();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,16 @@ class LookupDataStore {
*/
private $lookup_table_name;

private bool $products_store_is_cpt;

/**
* LookupDataStore constructor. Makes the feature hidden by default.
*/
public function __construct() {
global $wpdb;

$this->lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup';
$this->products_store_is_cpt = 'WC_Product_Data_Store_CPT' === \WC_Data_Store::load( 'product' )->get_current_class_name();

$this->init_hooks();
}
Expand Down Expand Up @@ -258,9 +261,38 @@ public function on_product_deleted( $product ) {
* This method is intended to be called from the data regenerator.
*
* @param int|WC_Product $product Product object or id.
* @param bool $force_use_data_store Force the usage of the WooCommerce data store classes for the regeneration, instead of direct database access is possible, even if the posts table is currently used for storing products.
* @throws \Exception A variation object is passed.
*/
public function create_data_for_product( $product ) {
public function create_data_for_product( $product, $force_use_data_store = false) {
global $wpdb;
//$force_use_data_store=true;
if(!$force_use_data_store && $this->products_store_is_cpt) {
$product_id = intval( ($product instanceof \WC_Product) ? $product->get_id() : $product );
try {
$result = $this->create_data_for_product_cpt( $product_id );
} catch (\Exception $e) {
wc_get_logger()->error("Lookup data creation failed for product $product_id: " . $e->getMessage(), ['source' => 'palt regeneration', 'exception' => $e]);
return;
}

if(1 === $result) {
$is_variation = $wpdb->get_var(
$wpdb->prepare(
"select exists(select id from {$wpdb->posts} where ID=%d and post_type='product_variation')",
$product_id
)
);
if ($is_variation) {
throw new \Exception("LookupDataStore::create_data_for_product can't be called for variations.");
}

wc_get_logger()->error("Lookup data creation failed for product $product_id: No data exists for the product in the database", ['source' => 'palt regeneration']);
}

return;
}

if ( ! is_a( $product, \WC_Product::class ) ) {
$product = WC()->call_function( 'wc_get_product', $product );
}
Expand Down Expand Up @@ -710,4 +742,215 @@ private function add_product_attributes_lookup_table_settings( array $settings,

return $settings;
}

public function create_data_for_product_cpt( int $product_id ) {
global $wpdb;

$wpdb->query("delete from {$wpdb->prefix}wc_product_attributes_lookup where product_or_parent_id = " . $product_id);

// * Obtain list of product variations, together with stock statuses; also get the product type.
// Output: $product_ids_with_stock_status = associative array where 'id' is the key and values are the stock status (1 for "in stock", 0 otherwise).
// $variation_ids = raw list of variation ids.
// $is_variable_product = true or false.

$sql = $wpdb->prepare(
"(select p.ID as id, m.meta_value as stock_status, t.name as product_type from {$wpdb->posts} p
left join {$wpdb->postmeta} m on p.id=m.post_id and m.meta_key='_stock_status'
left join {$wpdb->term_relationships} tr on tr.object_id=p.id
left join {$wpdb->term_taxonomy} tt on tt.term_taxonomy_id=tr.term_taxonomy_id
left join {$wpdb->terms} t on t.term_id=tt.term_id
where p.post_type = 'product'
and p.post_status in ('publish', 'draft', 'pending', 'private')
and tt.taxonomy='product_type'
and t.name != 'exclude-from-search'
and p.id=%d
limit 1)
union
(select p.ID as id, m.meta_value as stock_status, null as product_type from {$wpdb->posts} p
left join {$wpdb->postmeta} m on p.id=m.post_id and m.meta_key='_stock_status'
where p.post_type = 'product_variation'
and p.post_status in ('publish', 'draft', 'pending', 'private')
and p.post_parent=%d);
", $product_id, $product_id );

$product_ids_with_stock_status = $wpdb->get_results($sql, ARRAY_A);
$main_product_row = current(array_filter($product_ids_with_stock_status, fn($item) => intval($item['id']) === $product_id));
if(false === $main_product_row) {
return 1;
}
$is_variable_product = 'variable' === $main_product_row['product_type'];

$product_ids_with_stock_status = ArrayUtil::group_by_column($product_ids_with_stock_status, 'id', true);
$variation_ids = array_keys(array_diff_key($product_ids_with_stock_status, [$product_id=>null]));
$product_ids_with_stock_status = ArrayUtil::select($product_ids_with_stock_status, 'stock_status');;
$product_ids_with_stock_status = array_map(fn($item) => 'instock' === $item ? 1 : 0, $product_ids_with_stock_status);

// * Obtain the list of attributes used for variations and not.
// Output: two lists of attribute slugs, all starting with 'pa_'.

$sql = $wpdb->prepare(
"select meta_value from {$wpdb->postmeta} where post_id=%d and meta_key=%s",
$product_id,
'_product_attributes'
);

$temp = $wpdb->get_var($sql);
if( is_null( $temp ) ) {
return 2;
}

$temp = unserialize($temp);
if(false === $temp) {
return 2;
}

$temp = array_filter($temp, fn($item, $slug) => StringUtil::starts_with($slug, 'pa_') && '' === $item['value'], ARRAY_FILTER_USE_BOTH);

$attributes_not_for_variations =
array_keys(array_filter($temp, fn($item) => 0 === $item['is_variation']));

if($is_variable_product) {
$attributes_for_variations =
array_diff(array_keys($temp), $attributes_not_for_variations);
}

// * Obtain the terms used for each attribute.
// Output: $terms_used_per_attribute =
// [
// 'pa_...' => [
// [
// 'term_id' => <term id>,
// 'attribute' => 'pa_...'
// 'slug' => <term slug>
// ],...
// ],...
// ]

$sql = $wpdb->prepare(
"select tt.term_id, tt.taxonomy as attribute, t.slug from {$wpdb->prefix}term_relationships tr
join wp_term_taxonomy tt on tt.term_taxonomy_id = tr.term_taxonomy_id
join wp_terms t on t.term_id=tt.term_id
where tr.object_id=%d and taxonomy like 'pa_%';",
$product_id
);

$terms_used_per_attribute = $wpdb->get_results($sql, ARRAY_A);
foreach($terms_used_per_attribute as &$term) {
$term['attribute'] = strtolower(urlencode($term['attribute']));
}
$terms_used_per_attribute = ArrayUtil::group_by_column($terms_used_per_attribute, 'attribute');

//* Obtain the actual variations defined (only if variations exist).
// Output: $variations_defined =
// [
// <variation id> => [
// [
// 'variation_id' => <variation id>,
// 'attribute' => 'pa_...'
// 'slug' => <term slug>
// ],...
// ],...
// ]
//
// Note that this does NOT include "any..." attributes!

if(!$is_variable_product || empty($variation_ids)) {
$variations_defined = [];
} else {
$sql = $wpdb->prepare(
"select post_id as variation_id, substr(meta_key,11) as attribute, meta_value as slug from {$wpdb->postmeta}
where post_id in (select ID from {$wpdb->posts} where post_parent=%d and post_type = 'product_variation')
and meta_key like 'attribute_pa_%'
and meta_value != ''",
$product_id
);
$variations_defined = $wpdb->get_results($sql, ARRAY_A);
$variations_defined = ArrayUtil::group_by_column($variations_defined, 'variation_id');
}

// We have all the data, let's go!

$insert_data = [];

//* Insert data for the main product

foreach($attributes_not_for_variations as $attribute_name) {
foreach(($terms_used_per_attribute[$attribute_name] ?? []) as $attribute_data) {
$insert_data[] = [$product_id, $product_id, $attribute_name, $attribute_data['term_id'], 0, $product_ids_with_stock_status[$product_id]];
//$this->insert_lookup_table_data($product_id, $product_id, $attribute_name, $attribute_data['term_id'], false, $product_ids_with_stock_status[$product_id]);
}
}

/*if(empty($variation_ids)) {
return;
}*/

//* Insert data for the variations defined

$used_attributes_per_variation = [];
foreach($variations_defined as $variation_id => $variation_data) {
$used_attributes_per_variation[$variation_id] = [];
foreach ($variation_data as $variation_attribute_data) {
$attribute_name = $variation_attribute_data['attribute'];
$used_attributes_per_variation[$variation_id][] = $attribute_name;
$term_id = current(array_filter(($terms_used_per_attribute[$attribute_name] ?? []), fn($item) => $item['slug'] === $variation_attribute_data['slug']))['term_id'] ?? null;
if (is_null($term_id)) {
continue;
}
$insert_data[] = [$variation_id, $product_id, $attribute_name, $term_id, 1, $product_ids_with_stock_status[$variation_id] ?? false];
//$this->insert_lookup_table_data($variation_id, $product_id, $attribute_name, $term_id, true, $product_ids_with_stock_status[$variation_id] ?? false);
}
}

//* Insert data for variations that have "any..." attributes and at least one defined attribute

$terms_used_per_attribute = array_diff_key($terms_used_per_attribute, array_flip($attributes_not_for_variations));
foreach($used_attributes_per_variation as $variation_id => $attributes_list) {
$any_attributes = array_diff_key($terms_used_per_attribute, array_flip($attributes_list));
foreach($any_attributes as $attributes_data) {
foreach($attributes_data as $attribute_data) {
$insert_data[] = [$variation_id, $product_id, $attribute_data['attribute'], $attribute_data['term_id'], 1, $product_ids_with_stock_status[$variation_id] ?? false];
//$this->insert_lookup_table_data($variation_id, $product_id, $attribute_data['attribute'], $attribute_data['term_id'], true, $product_ids_with_stock_status[$variation_id] ?? false);
}
}
}

//* Insert data for variations that have all their attributes defined as "any..."

$variations_with_all_any = array_keys(array_diff_key(array_flip($variation_ids), $used_attributes_per_variation));
foreach($variations_with_all_any as $variation_id) {
foreach($terms_used_per_attribute as $attribute_name => $attribute_terms) {
foreach($attribute_terms as $attribute_term) {
$insert_data[] = [$variation_id, $product_id, $attribute_name, $attribute_term['term_id'], 1, $product_ids_with_stock_status[$variation_id] ?? false];
//$this->insert_lookup_table_data($variation_id, $product_id, $attribute_name, $attribute_term['term_id'], true, $product_ids_with_stock_status[$variation_id] ?? false);
}
}
}

//* We have all the data to insert, go and insert it!

$insert_data_chunks = array_chunk($insert_data, 100);
foreach($insert_data_chunks as $insert_data_chunk) {
$sql = 'INSERT INTO ' . $this->lookup_table_name . ' (
product_id,
product_or_parent_id,
taxonomy,
term_id,
is_variation_attribute,
in_stock)
VALUES (';

$values_strings = [];
foreach($insert_data_chunk as $dataset) {
$attribute_name = esc_sql($dataset[2]);
$values_strings[] = "{$dataset[0]},{$dataset[1]},'{$attribute_name}',{$dataset[3]},{$dataset[4]},{$dataset[5]}";
}

$sql .= implode('),(', $values_strings) . ')';

$wpdb->query($sql);
}

return 0;
}
}
16 changes: 15 additions & 1 deletion plugins/woocommerce/src/Utilities/ArrayUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -321,5 +321,19 @@ public static function ensure_key_is_array( array &$array, string $key, bool $th

return false;
}
}

public static function group_by_column(array $array, string $column, bool $single_values = false): array {
if($single_values) {
return array_combine(array_column($array, $column), array_values($array) );
}

$distinct_column_values = array_unique(array_column($array, $column), SORT_REGULAR);
$result = array_fill_keys($distinct_column_values, array());

foreach($array as $value) {
$result[$value[$column]][] = $value;
}

return $result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public function setUp(): void {
$this->lookup_data_store = new class() extends LookupDataStore {
public $passed_products = array();

public function create_data_for_product( $product ) {
public function create_data_for_product( $product, $force_use_datastore = false ) {
$this->passed_products[] = $product;
}
};
Expand Down

0 comments on commit e4e440c

Please sign in to comment.