File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/ProductAttributesLookup.tar
DataRegenerator.php 0000644 00000046550 15154271250 0010340 0 ustar 00 <?php
/**
* DataRegenerator class file.
*/
namespace Automattic\WooCommerce\Internal\ProductAttributesLookup;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
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 'yes' will have been created, thus effectively enabling the table usage
* (with an exception: if the regeneration was manually aborted via deleting the
* 'woocommerce_attribute_lookup_regeneration_in_progress' option) the option will be set to 'no'.
*
* 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 {
use AccessiblePrivateMethods;
public 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';
self::add_filter( 'woocommerce_debug_tools', array( $this, 'add_initiate_regeneration_entry_to_tools_array' ), 1, 999 );
self::add_action( 'woocommerce_run_product_attribute_lookup_regeneration_callback', array( $this, 'run_regeneration_step_callback' ) );
self::add_action( 'woocommerce_installed', array( $this, 'run_woocommerce_installed_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 method is intended ONLY to be used as a callback for a db update in wc-update-functions,
* regeneration triggered from the tools page will use initiate_regeneration_from_tools_page instead.
*/
public function initiate_regeneration() {
$this->data_store->unset_regeneration_aborted_flag();
$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( true );
}
}
/**
* Delete all the existing data related to the lookup table, including the table itself.
*/
private 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_processed_count' );
$this->data_store->unset_regeneration_in_progress_flag();
if ( $this->data_store->check_lookup_table_exists() ) {
$wpdb->query( "TRUNCATE TABLE {$this->lookup_table_name}" ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
}
/**
* 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() {
$database_util = wc_get_container()->get( DatabaseUtil::class );
$database_util->dbdelta( $this->get_table_creation_sql() );
$last_existing_product_id = $this->get_last_existing_product_id();
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', $last_existing_product_id );
update_option( 'woocommerce_attribute_lookup_processed_count', 0 );
return true;
}
/**
* Get the highest existing product id.
*
* @return int|null Highest existing product id, or null if no products exist at all.
*/
private function get_last_existing_product_id(): ?int {
$last_existing_product_id_array =
WC()->call_function(
'wc_get_products',
array(
'return' => 'ids',
'limit' => 1,
'orderby' => array(
'ID' => 'DESC',
),
)
);
return empty( $last_existing_product_id_array ) ? null : current( $last_existing_product_id_array );
}
/**
* 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() ) {
// No regeneration in progress at this point means that the regeneration process
// was manually aborted via deleting the 'woocommerce_attribute_lookup_regeneration_in_progress' option.
$this->data_store->set_regeneration_aborted_flag();
$this->finalize_regeneration( false );
return;
}
$result = $this->do_regeneration_step();
if ( $result ) {
$this->enqueue_regeneration_step_run();
} else {
$this->finalize_regeneration( true );
}
}
/**
* 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() {
/**
* Filter to alter the count of products that will be processed in each step of the product attributes lookup table regeneration process.
*
* @since 6.3
* @param int $count Default processing step size.
*/
$products_per_generation_step = apply_filters( 'woocommerce_attribute_lookup_regeneration_step_size', self::PRODUCTS_PER_GENERATION_STEP );
$products_already_processed = get_option( 'woocommerce_attribute_lookup_processed_count', 0 );
$product_ids = WC()->call_function(
'wc_get_products',
array(
'limit' => $products_per_generation_step,
'offset' => $products_already_processed,
'orderby' => array(
'ID' => 'ASC',
),
'return' => 'ids',
)
);
if ( ! $product_ids ) {
return false;
}
foreach ( $product_ids as $id ) {
$this->data_store->create_data_for_product( $id );
}
$products_already_processed += count( $product_ids );
update_option( 'woocommerce_attribute_lookup_processed_count', $products_already_processed );
$last_product_id_to_process = get_option( 'woocommerce_attribute_lookup_last_product_id_to_process', PHP_INT_MAX );
return end( $product_ids ) < $last_product_id_to_process;
}
/**
* Cleanup/final option setup after the regeneration has been completed.
*
* @param bool $enable_usage Whether the table usage should be enabled or not.
*/
private function finalize_regeneration( bool $enable_usage ) {
delete_option( 'woocommerce_attribute_lookup_last_product_id_to_process' );
delete_option( 'woocommerce_attribute_lookup_processed_count' );
update_option( 'woocommerce_attribute_lookup_enabled', $enable_usage ? 'yes' : '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->check_lookup_table_exists() ) {
return $tools_array;
}
$generation_is_in_progress = $this->data_store->regeneration_is_in_progress();
$generation_was_aborted = $this->data_store->regeneration_was_aborted();
$entry = array(
'name' => __( 'Regenerate the product attributes lookup table', 'woocommerce' ),
'desc' => __( 'This tool will regenerate the product attributes lookup table data from existing product(s) data. This process may take a while.', 'woocommerce' ),
'requires_refresh' => true,
'callback' => function() {
$this->initiate_regeneration_from_tools_page();
return __( 'Product attributes lookup table data is regenerating', 'woocommerce' );
},
'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_processed_count', 0 )
);
$entry['disabled'] = true;
} else {
$entry['button'] = __( 'Regenerate', 'woocommerce' );
}
$tools_array['regenerate_product_attributes_lookup_table'] = $entry;
if ( $generation_is_in_progress ) {
$entry = array(
'name' => __( 'Abort the product attributes lookup table regeneration', 'woocommerce' ),
'desc' => __( 'This tool will abort the regenerate product attributes lookup table regeneration. After this is done the process can be either started over, or resumed to continue where it stopped.', 'woocommerce' ),
'requires_refresh' => true,
'callback' => function() {
$this->abort_regeneration_from_tools_page();
return __( 'Product attributes lookup table regeneration process has been aborted.', 'woocommerce' );
},
'button' => __( 'Abort', 'woocommerce' ),
);
$tools_array['abort_product_attributes_lookup_table_regeneration'] = $entry;
} elseif ( $generation_was_aborted ) {
$processed_count = get_option( 'woocommerce_attribute_lookup_processed_count', 0 );
$entry = array(
'name' => __( 'Resume the product attributes lookup table regeneration', 'woocommerce' ),
'desc' =>
sprintf(
/* translators: %1$s = count of products already processed. */
__( 'This tool will resume the product attributes lookup table regeneration at the point in which it was aborted (%1$s products were already processed).', 'woocommerce' ),
$processed_count
),
'requires_refresh' => true,
'callback' => function() {
$this->resume_regeneration_from_tools_page();
return __( 'Product attributes lookup table regeneration process has been resumed.', 'woocommerce' );
},
'button' => __( 'Resume', 'woocommerce' ),
);
$tools_array['resume_product_attributes_lookup_table_regeneration'] = $entry;
}
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() {
$this->verify_tool_execution_nonce();
//phpcs:disable WordPress.Security.NonceVerification.Recommended
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();
}
//phpcs:enable WordPress.Security.NonceVerification.Recommended
}
/**
* 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 ( $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" );
}
}
/**
* Callback to abort the regeneration process from the Status - Tools page.
*
* @throws \Exception The lookup table doesn't exist, or there's no regeneration process in progress to abort.
*/
private function abort_regeneration_from_tools_page() {
$this->verify_tool_execution_nonce();
if ( ! $this->data_store->check_lookup_table_exists() ) {
throw new \Exception( "Can't abort the product attribute lookup data regeneration process: lookup table doesn't exist" );
}
if ( ! $this->data_store->regeneration_is_in_progress() ) {
throw new \Exception( "Can't abort the product attribute lookup data regeneration process since it's not currently in progress" );
}
$queue = WC()->get_instance_of( \WC_Queue::class );
$queue->cancel_all( 'woocommerce_run_product_attribute_lookup_regeneration_callback' );
$this->data_store->unset_regeneration_in_progress_flag();
$this->data_store->set_regeneration_aborted_flag();
$this->enable_or_disable_lookup_table_usage( false );
// Note that we are NOT deleting the options that track the regeneration progress (processed count, last product id to process).
// This is on purpose so that the regeneration can be resumed where it stopped.
}
/**
* Callback to resume the regeneration process from the Status - Tools page.
*
* @throws \Exception The lookup table doesn't exist, or a regeneration process is already in place.
*/
private function resume_regeneration_from_tools_page() {
$this->verify_tool_execution_nonce();
if ( ! $this->data_store->check_lookup_table_exists() ) {
throw new \Exception( "Can't resume the product attribute lookup data regeneration process: lookup table doesn't exist" );
}
if ( $this->data_store->regeneration_is_in_progress() ) {
throw new \Exception( "Can't resume the product attribute lookup data regeneration process: regeneration is already in progress" );
}
$this->data_store->unset_regeneration_aborted_flag();
$this->data_store->set_regeneration_in_progress_flag();
$this->enqueue_regeneration_step_run();
}
/**
* Verify the validity of the nonce received when executing a tool from the Status - Tools page.
*
* @throws \Exception Missing or invalid nonce received.
*/
private function verify_tool_execution_nonce() {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
if ( ! isset( $_REQUEST['_wpnonce'] ) || wp_verify_nonce( $_REQUEST['_wpnonce'], 'debug_action' ) === false ) {
throw new \Exception( 'Invalid nonce' );
}
}
/**
* Get the name of the product attributes lookup table.
*
* @return string
*/
public function get_lookup_table_name() {
return $this->lookup_table_name;
}
/**
* Get the SQL statement that creates the product attributes lookup table, including the indices.
*
* @return string
*/
public function get_table_creation_sql() {
global $wpdb;
$collate = $wpdb->has_cap( 'collation' ) ? $wpdb->get_charset_collate() : '';
return "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,
INDEX is_variation_attribute_term_id (is_variation_attribute, term_id),
PRIMARY KEY ( `product_or_parent_id`, `term_id`, `product_id`, `taxonomy` )
) $collate;";
}
/**
* Create the primary key for the table if it doesn't exist already.
* It also deletes the product_or_parent_id_term_id index if it exists, since it's now redundant.
*
* @return void
*/
public function create_table_primary_index() {
$database_util = wc_get_container()->get( DatabaseUtil::class );
$database_util->create_primary_key( $this->lookup_table_name, array( 'product_or_parent_id', 'term_id', 'product_id', 'taxonomy' ) );
$database_util->drop_table_index( $this->lookup_table_name, 'product_or_parent_id_term_id' );
if ( empty( $database_util->get_index_columns( $this->lookup_table_name ) ) ) {
wc_get_logger()->error( "The creation of the primary key for the {$this->lookup_table_name} table failed" );
}
if ( ! empty( $database_util->get_index_columns( $this->lookup_table_name, 'product_or_parent_id_term_id' ) ) ) {
wc_get_logger()->error( "Dropping the product_or_parent_id_term_id index from the {$this->lookup_table_name} table failed" );
}
}
/**
* Run additional setup needed after a WooCommerce install or update finishes.
*/
private function run_woocommerce_installed_callback() {
// The table must exist at this point (created via dbDelta), but we check just in case.
if ( ! $this->data_store->check_lookup_table_exists() ) {
return;
}
// If a table regeneration is in progress, leave it alone.
if ( $this->data_store->regeneration_is_in_progress() ) {
return;
}
// 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() ) {
$must_enable = get_option( 'woocommerce_attribute_lookup_enabled' ) !== 'no';
$this->finalize_regeneration( $must_enable );
} else {
$this->initiate_regeneration();
}
}
}
Filterer.php 0000644 00000030014 15154271250 0007031 0 ustar 00 <?php
/**
* Filterer class file.
*/
namespace Automattic\WooCommerce\Internal\ProductAttributesLookup;
defined( 'ABSPATH' ) || exit;
/**
* Helper class for filtering products using the product attributes lookup table.
*/
class Filterer {
/**
* The product attributes lookup data store to use.
*
* @var LookupDataStore
*/
private $data_store;
/**
* The name of the product attributes lookup table.
*
* @var string
*/
private $lookup_table_name;
/**
* 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;
$this->lookup_table_name = $data_store->get_lookup_table_name();
}
/**
* Checks if the product attribute filtering via lookup table feature is enabled.
*
* @return bool
*/
public function filtering_via_lookup_table_is_active() {
return 'yes' === get_option( 'woocommerce_attribute_lookup_enabled' );
}
/**
* Adds post clauses for filtering via lookup table.
* This method should be invoked within a 'posts_clauses' filter.
*
* @param array $args Product query clauses as supplied to the 'posts_clauses' filter.
* @param \WP_Query $wp_query Current product query as supplied to the 'posts_clauses' filter.
* @param array $attributes_to_filter_by Attribute filtering data as generated by WC_Query::get_layered_nav_chosen_attributes.
* @return array The updated product query clauses.
*/
public function filter_by_attribute_post_clauses( array $args, \WP_Query $wp_query, array $attributes_to_filter_by ) {
global $wpdb;
if ( ! $wp_query->is_main_query() || ! $this->filtering_via_lookup_table_is_active() ) {
return $args;
}
// The extra derived table ("SELECT product_or_parent_id FROM") is needed for performance
// (causes the filtering subquery to be executed only once).
$clause_root = " {$wpdb->posts}.ID IN ( SELECT product_or_parent_id FROM (";
if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
$in_stock_clause = ' AND in_stock = 1';
} else {
$in_stock_clause = '';
}
$attribute_ids_for_and_filtering = array();
foreach ( $attributes_to_filter_by as $taxonomy => $data ) {
$all_terms = get_terms( $taxonomy, array( 'hide_empty' => false ) );
$term_ids_by_slug = wp_list_pluck( $all_terms, 'term_id', 'slug' );
$term_ids_to_filter_by = array_values( array_intersect_key( $term_ids_by_slug, array_flip( $data['terms'] ) ) );
$term_ids_to_filter_by = array_map( 'absint', $term_ids_to_filter_by );
$term_ids_to_filter_by_list = '(' . join( ',', $term_ids_to_filter_by ) . ')';
$is_and_query = 'and' === $data['query_type'];
$count = count( $term_ids_to_filter_by );
if ( 0 !== $count ) {
if ( $is_and_query && $count > 1 ) {
$attribute_ids_for_and_filtering = array_merge( $attribute_ids_for_and_filtering, $term_ids_to_filter_by );
} else {
$clauses[] = "
{$clause_root}
SELECT product_or_parent_id
FROM {$this->lookup_table_name} lt
WHERE term_id in {$term_ids_to_filter_by_list}
{$in_stock_clause}
)";
}
}
}
if ( ! empty( $attribute_ids_for_and_filtering ) ) {
$count = count( $attribute_ids_for_and_filtering );
$term_ids_to_filter_by_list = '(' . join( ',', $attribute_ids_for_and_filtering ) . ')';
$clauses[] = "
{$clause_root}
SELECT product_or_parent_id
FROM {$this->lookup_table_name} lt
WHERE is_variation_attribute=0
{$in_stock_clause}
AND term_id in {$term_ids_to_filter_by_list}
GROUP BY product_id
HAVING COUNT(product_id)={$count}
UNION
SELECT product_or_parent_id
FROM {$this->lookup_table_name} lt
WHERE is_variation_attribute=1
{$in_stock_clause}
AND term_id in {$term_ids_to_filter_by_list}
)";
}
if ( ! empty( $clauses ) ) {
// "temp" is needed because the extra derived tables require an alias.
$args['where'] .= ' AND (' . join( ' temp ) AND ', $clauses ) . ' temp ))';
} elseif ( ! empty( $attributes_to_filter_by ) ) {
$args['where'] .= ' AND 1=0';
}
return $args;
}
/**
* Count products within certain terms, taking the main WP query into consideration,
* for the WC_Widget_Layered_Nav widget.
*
* This query allows counts to be generated based on the viewed products, not all products.
*
* @param array $term_ids Term IDs.
* @param string $taxonomy Taxonomy.
* @param string $query_type Query Type.
* @return array
*/
public function get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type ) {
global $wpdb;
$use_lookup_table = $this->filtering_via_lookup_table_is_active();
$tax_query = \WC_Query::get_main_tax_query();
$meta_query = \WC_Query::get_main_meta_query();
if ( 'or' === $query_type ) {
foreach ( $tax_query as $key => $query ) {
if ( is_array( $query ) && $taxonomy === $query['taxonomy'] ) {
unset( $tax_query[ $key ] );
}
}
}
$meta_query = new \WP_Meta_Query( $meta_query );
$tax_query = new \WP_Tax_Query( $tax_query );
if ( $use_lookup_table ) {
$query = $this->get_product_counts_query_using_lookup_table( $tax_query, $meta_query, $taxonomy, $term_ids );
} else {
$query = $this->get_product_counts_query_not_using_lookup_table( $tax_query, $meta_query, $term_ids );
}
$query = apply_filters( 'woocommerce_get_filtered_term_product_counts_query', $query );
$query_sql = implode( ' ', $query );
// We have a query - let's see if cached results of this query already exist.
$query_hash = md5( $query_sql );
// Maybe store a transient of the count values.
$cache = apply_filters( 'woocommerce_layered_nav_count_maybe_cache', true );
if ( true === $cache ) {
$cached_counts = (array) get_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ) );
} else {
$cached_counts = array();
}
if ( ! isset( $cached_counts[ $query_hash ] ) ) {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$results = $wpdb->get_results( $query_sql, ARRAY_A );
$counts = array_map( 'absint', wp_list_pluck( $results, 'term_count', 'term_count_id' ) );
$cached_counts[ $query_hash ] = $counts;
if ( true === $cache ) {
set_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ), $cached_counts, DAY_IN_SECONDS );
}
}
return array_map( 'absint', (array) $cached_counts[ $query_hash ] );
}
/**
* Get the query for counting products by terms using the product attributes lookup table.
*
* @param \WP_Tax_Query $tax_query The current main tax query.
* @param \WP_Meta_Query $meta_query The current main meta query.
* @param string $taxonomy The attribute name to get the term counts for.
* @param string $term_ids The term ids to include in the search.
* @return array An array of SQL query parts.
*/
private function get_product_counts_query_using_lookup_table( $tax_query, $meta_query, $taxonomy, $term_ids ) {
global $wpdb;
$meta_query_sql = $meta_query->get_sql( 'post', $this->lookup_table_name, 'product_or_parent_id' );
$tax_query_sql = $tax_query->get_sql( $this->lookup_table_name, 'product_or_parent_id' );
$hide_out_of_stock = 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' );
$in_stock_clause = $hide_out_of_stock ? ' AND in_stock = 1' : '';
$query['select'] = 'SELECT COUNT(DISTINCT product_or_parent_id) as term_count, term_id as term_count_id';
$query['from'] = "FROM {$this->lookup_table_name}";
$query['join'] = "
{$tax_query_sql['join']} {$meta_query_sql['join']}
INNER JOIN {$wpdb->posts} ON {$wpdb->posts}.ID = {$this->lookup_table_name}.product_or_parent_id";
$encoded_taxonomy = sanitize_title( $taxonomy );
$term_ids_sql = $this->get_term_ids_sql( $term_ids );
$query['where'] = "
WHERE {$wpdb->posts}.post_type IN ( 'product' )
AND {$wpdb->posts}.post_status = 'publish'
{$tax_query_sql['where']} {$meta_query_sql['where']}
AND {$this->lookup_table_name}.taxonomy='{$encoded_taxonomy}'
AND {$this->lookup_table_name}.term_id IN $term_ids_sql
{$in_stock_clause}";
if ( ! empty( $term_ids ) ) {
$attributes_to_filter_by = \WC_Query::get_layered_nav_chosen_attributes();
if ( ! empty( $attributes_to_filter_by ) ) {
$and_term_ids = array();
foreach ( $attributes_to_filter_by as $taxonomy => $data ) {
if ( 'and' !== $data['query_type'] ) {
continue;
}
$all_terms = get_terms( $taxonomy, array( 'hide_empty' => false ) );
$term_ids_by_slug = wp_list_pluck( $all_terms, 'term_id', 'slug' );
$term_ids_to_filter_by = array_values( array_intersect_key( $term_ids_by_slug, array_flip( $data['terms'] ) ) );
$and_term_ids = array_merge( $and_term_ids, $term_ids_to_filter_by );
}
if ( ! empty( $and_term_ids ) ) {
$terms_count = count( $and_term_ids );
$term_ids_list = '(' . join( ',', $and_term_ids ) . ')';
// The extra derived table ("SELECT product_or_parent_id FROM") is needed for performance
// (causes the filtering subquery to be executed only once).
$query['where'] .= "
AND product_or_parent_id IN ( SELECT product_or_parent_id FROM (
SELECT product_or_parent_id
FROM {$this->lookup_table_name} lt
WHERE is_variation_attribute=0
{$in_stock_clause}
AND term_id in {$term_ids_list}
GROUP BY product_id
HAVING COUNT(product_id)={$terms_count}
UNION
SELECT product_or_parent_id
FROM {$this->lookup_table_name} lt
WHERE is_variation_attribute=1
{$in_stock_clause}
AND term_id in {$term_ids_list}
) temp )";
}
} else {
$query['where'] .= $in_stock_clause;
}
} elseif ( $hide_out_of_stock ) {
$query['where'] .= " AND {$this->lookup_table_name}.in_stock=1";
}
$search_query_sql = \WC_Query::get_main_search_query_sql();
if ( $search_query_sql ) {
$query['where'] .= ' AND ' . $search_query_sql;
}
$query['group_by'] = 'GROUP BY terms.term_id';
$query['group_by'] = "GROUP BY {$this->lookup_table_name}.term_id";
return $query;
}
/**
* Get the query for counting products by terms NOT using the product attributes lookup table.
*
* @param \WP_Tax_Query $tax_query The current main tax query.
* @param \WP_Meta_Query $meta_query The current main meta query.
* @param string $term_ids The term ids to include in the search.
* @return array An array of SQL query parts.
*/
private function get_product_counts_query_not_using_lookup_table( $tax_query, $meta_query, $term_ids ) {
global $wpdb;
$meta_query_sql = $meta_query->get_sql( 'post', $wpdb->posts, 'ID' );
$tax_query_sql = $tax_query->get_sql( $wpdb->posts, 'ID' );
// Generate query.
$query = array();
$query['select'] = "SELECT COUNT( DISTINCT {$wpdb->posts}.ID ) AS term_count, terms.term_id AS term_count_id";
$query['from'] = "FROM {$wpdb->posts}";
$query['join'] = "
INNER JOIN {$wpdb->term_relationships} AS term_relationships ON {$wpdb->posts}.ID = term_relationships.object_id
INNER JOIN {$wpdb->term_taxonomy} AS term_taxonomy USING( term_taxonomy_id )
INNER JOIN {$wpdb->terms} AS terms USING( term_id )
" . $tax_query_sql['join'] . $meta_query_sql['join'];
$term_ids_sql = $this->get_term_ids_sql( $term_ids );
$query['where'] = "
WHERE {$wpdb->posts}.post_type IN ( 'product' )
AND {$wpdb->posts}.post_status = 'publish'
{$tax_query_sql['where']} {$meta_query_sql['where']}
AND terms.term_id IN $term_ids_sql";
$search_query_sql = \WC_Query::get_main_search_query_sql();
if ( $search_query_sql ) {
$query['where'] .= ' AND ' . $search_query_sql;
}
$query['group_by'] = 'GROUP BY terms.term_id';
return $query;
}
/**
* Formats a list of term ids as "(id,id,id)".
*
* @param array $term_ids The list of terms to format.
* @return string The formatted list.
*/
private function get_term_ids_sql( $term_ids ) {
return '(' . implode( ',', array_map( 'absint', $term_ids ) ) . ')';
}
}
LookupDataStore.php 0000644 00000057725 15154271250 0010357 0 ustar 00 <?php
/**
* LookupDataStore class file.
*/
namespace Automattic\WooCommerce\Internal\ProductAttributesLookup;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Utilities\StringUtil;
defined( 'ABSPATH' ) || exit;
/**
* Data store class for the product attributes lookup table.
*/
class LookupDataStore {
use AccessiblePrivateMethods;
/**
* Types of updates to perform depending on the current changest
*/
public const ACTION_NONE = 0;
public const ACTION_INSERT = 1;
public const ACTION_UPDATE_STOCK = 2;
public const ACTION_DELETE = 3;
/**
* The lookup table name.
*
* @var string
*/
private $lookup_table_name;
/**
* LookupDataStore constructor. Makes the feature hidden by default.
*/
public function __construct() {
global $wpdb;
$this->lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup';
$this->init_hooks();
}
/**
* Initialize the hooks used by the class.
*/
private function init_hooks() {
self::add_action( 'woocommerce_run_product_attribute_lookup_update_callback', array( $this, 'run_update_callback' ), 10, 2 );
self::add_filter( 'woocommerce_get_sections_products', array( $this, 'add_advanced_section_to_product_settings' ), 100, 1 );
self::add_action( 'woocommerce_rest_insert_product', array( $this, 'on_product_created_or_updated_via_rest_api' ), 100, 2 );
self::add_filter( 'woocommerce_get_settings_products', array( $this, 'add_product_attributes_lookup_table_settings' ), 100, 2 );
}
/**
* Check if the lookup table exists in the database.
*
* @return bool
*/
public function check_lookup_table_exists() {
global $wpdb;
$query = $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $this->lookup_table_name ) );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
return $this->lookup_table_name === $wpdb->get_var( $query );
}
/**
* Get the name of the lookup table.
*
* @return string
*/
public function get_lookup_table_name() {
return $this->lookup_table_name;
}
/**
* Insert/update the appropriate lookup table entries for a new or modified product or variation.
* This must be invoked after a product or a variation is created (including untrashing and duplication)
* or modified.
*
* @param int|\WC_Product $product Product object or product id.
* @param null|array $changeset Changes as provided by 'get_changes' method in the product object, null if it's being created.
*/
public function on_product_changed( $product, $changeset = null ) {
if ( ! $this->check_lookup_table_exists() ) {
return;
}
if ( ! is_a( $product, \WC_Product::class ) ) {
$product = WC()->call_function( 'wc_get_product', $product );
}
$action = $this->get_update_action( $changeset );
if ( $action !== self::ACTION_NONE ) {
$this->maybe_schedule_update( $product->get_id(), $action );
}
}
/**
* 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 ( get_option( 'woocommerce_attribute_lookup_direct_updates' ) === 'yes' ) {
$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->check_lookup_table_exists() ) {
return;
}
$product = WC()->call_function( 'wc_get_product', $product_id );
if ( ! $product ) {
$action = self::ACTION_DELETE;
}
switch ( $action ) {
case self::ACTION_INSERT:
$this->delete_data_for( $product_id );
$this->create_data_for( $product );
break;
case self::ACTION_UPDATE_STOCK:
$this->update_stock_status_for( $product );
break;
case self::ACTION_DELETE:
$this->delete_data_for( $product_id );
break;
}
}
/**
* Determine the type of action to perform depending on the received changeset.
*
* @param array|null $changeset The changeset received by on_product_changed.
* @return int One of the ACTION_ constants.
*/
private function get_update_action( $changeset ) {
if ( is_null( $changeset ) ) {
// No changeset at all means that the product is new.
return self::ACTION_INSERT;
}
$keys = array_keys( $changeset );
// Order matters:
// - The change with the most precedence is a change in catalog visibility
// (which will result in all data being regenerated or deleted).
// - Then a change in attributes (all data will be regenerated).
// - And finally a change in stock status (existing data will be updated).
// Thus these conditions must be checked in that same order.
if ( in_array( 'catalog_visibility', $keys, true ) ) {
$new_visibility = $changeset['catalog_visibility'];
if ( $new_visibility === 'visible' || $new_visibility === 'catalog' ) {
return self::ACTION_INSERT;
} else {
return self::ACTION_DELETE;
}
}
if ( in_array( 'attributes', $keys, true ) ) {
return self::ACTION_INSERT;
}
if ( array_intersect( $keys, array( 'stock_quantity', 'stock_status', 'manage_stock' ) ) ) {
return self::ACTION_UPDATE_STOCK;
}
return self::ACTION_NONE;
}
/**
* Update the stock status of the lookup table entries for a given product.
*
* @param \WC_Product $product The product to update the entries for.
*/
private function update_stock_status_for( \WC_Product $product ) {
global $wpdb;
$in_stock = $product->is_in_stock();
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query(
$wpdb->prepare(
'UPDATE ' . $this->lookup_table_name . ' SET in_stock = %d WHERE product_id = %d',
$in_stock ? 1 : 0,
$product->get_id()
)
);
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Delete the lookup table contents related to a given product or variation,
* if it's a variable product it deletes the information for variations too.
* This must be invoked after a product or a variation is trashed or deleted.
*
* @param int|\WC_Product $product Product object or product id.
*/
public function on_product_deleted( $product ) {
if ( ! $this->check_lookup_table_exists() ) {
return;
}
if ( is_a( $product, \WC_Product::class ) ) {
$product_id = $product->get_id();
} else {
$product_id = $product;
}
$this->maybe_schedule_update( $product_id, self::ACTION_DELETE );
}
/**
* Create the lookup data for a given product, if a variable product is passed
* the information is created for all of its variations.
* This method is intended to be called from the data regenerator.
*
* @param int|WC_Product $product Product object or id.
* @throws \Exception A variation object is passed.
*/
public function create_data_for_product( $product ) {
if ( ! is_a( $product, \WC_Product::class ) ) {
$product = WC()->call_function( 'wc_get_product', $product );
}
if ( $this->is_variation( $product ) ) {
throw new \Exception( "LookupDataStore::create_data_for_product can't be called for variations." );
}
$this->delete_data_for( $product->get_id() );
$this->create_data_for( $product );
}
/**
* Create lookup table data for a given product.
*
* @param \WC_Product $product The product to create the data for.
*/
private function create_data_for( \WC_Product $product ) {
if ( $this->is_variation( $product ) ) {
$this->create_data_for_variation( $product );
} elseif ( $this->is_variable_product( $product ) ) {
$this->create_data_for_variable_product( $product );
} else {
$this->create_data_for_simple_product( $product );
}
}
/**
* Delete all the lookup table entries for a given product,
* if it's a variable product information for variations is deleted too.
*
* @param int $product_id Simple product id, or main/parent product id for variable products.
*/
private function delete_data_for( int $product_id ) {
global $wpdb;
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query(
$wpdb->prepare(
'DELETE FROM ' . $this->lookup_table_name . ' WHERE product_id = %d OR product_or_parent_id = %d',
$product_id,
$product_id
)
);
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Create lookup table entries for a simple (non variable) product.
* Assumes that no entries exist yet.
*
* @param \WC_Product $product The product to create the entries for.
*/
private function create_data_for_simple_product( \WC_Product $product ) {
$product_attributes_data = $this->get_attribute_taxonomies( $product );
$has_stock = $product->is_in_stock();
$product_id = $product->get_id();
foreach ( $product_attributes_data as $taxonomy => $data ) {
$term_ids = $data['term_ids'];
foreach ( $term_ids as $term_id ) {
$this->insert_lookup_table_data( $product_id, $product_id, $taxonomy, $term_id, false, $has_stock );
}
}
}
/**
* Create lookup table entries for a variable product.
* Assumes that no entries exist yet.
*
* @param \WC_Product_Variable $product The product to create the entries for.
*/
private function create_data_for_variable_product( \WC_Product_Variable $product ) {
$product_attributes_data = $this->get_attribute_taxonomies( $product );
$variation_attributes_data = array_filter(
$product_attributes_data,
function( $item ) {
return $item['used_for_variations'];
}
);
$non_variation_attributes_data = array_filter(
$product_attributes_data,
function( $item ) {
return ! $item['used_for_variations'];
}
);
$main_product_has_stock = $product->is_in_stock();
$main_product_id = $product->get_id();
foreach ( $non_variation_attributes_data as $taxonomy => $data ) {
$term_ids = $data['term_ids'];
foreach ( $term_ids as $term_id ) {
$this->insert_lookup_table_data( $main_product_id, $main_product_id, $taxonomy, $term_id, false, $main_product_has_stock );
}
}
$term_ids_by_slug_cache = $this->get_term_ids_by_slug_cache( array_keys( $variation_attributes_data ) );
$variations = $this->get_variations_of( $product );
foreach ( $variation_attributes_data as $taxonomy => $data ) {
foreach ( $variations as $variation ) {
$this->insert_lookup_table_data_for_variation( $variation, $taxonomy, $main_product_id, $data['term_ids'], $term_ids_by_slug_cache );
}
}
}
/**
* Create all the necessary lookup data for a given variation.
*
* @param \WC_Product_Variation $variation The variation to create entries for.
*/
private function create_data_for_variation( \WC_Product_Variation $variation ) {
$main_product = WC()->call_function( 'wc_get_product', $variation->get_parent_id() );
$product_attributes_data = $this->get_attribute_taxonomies( $main_product );
$variation_attributes_data = array_filter(
$product_attributes_data,
function( $item ) {
return $item['used_for_variations'];
}
);
$term_ids_by_slug_cache = $this->get_term_ids_by_slug_cache( array_keys( $variation_attributes_data ) );
foreach ( $variation_attributes_data as $taxonomy => $data ) {
$this->insert_lookup_table_data_for_variation( $variation, $taxonomy, $main_product->get_id(), $data['term_ids'], $term_ids_by_slug_cache );
}
}
/**
* Create lookup table entries for a given variation, corresponding to a given taxonomy and a set of term ids.
*
* @param \WC_Product_Variation $variation The variation to create entries for.
* @param string $taxonomy The taxonomy to create the entries for.
* @param int $main_product_id The parent product id.
* @param array $term_ids The term ids to create entries for.
* @param array $term_ids_by_slug_cache A dictionary of term ids by term slug, as returned by 'get_term_ids_by_slug_cache'.
*/
private function insert_lookup_table_data_for_variation( \WC_Product_Variation $variation, string $taxonomy, int $main_product_id, array $term_ids, array $term_ids_by_slug_cache ) {
$variation_id = $variation->get_id();
$variation_has_stock = $variation->is_in_stock();
$variation_definition_term_id = $this->get_variation_definition_term_id( $variation, $taxonomy, $term_ids_by_slug_cache );
if ( $variation_definition_term_id ) {
$this->insert_lookup_table_data( $variation_id, $main_product_id, $taxonomy, $variation_definition_term_id, true, $variation_has_stock );
} else {
$term_ids_for_taxonomy = $term_ids;
foreach ( $term_ids_for_taxonomy as $term_id ) {
$this->insert_lookup_table_data( $variation_id, $main_product_id, $taxonomy, $term_id, true, $variation_has_stock );
}
}
}
/**
* Get a cache of term ids by slug for a set of taxonomies, with this format:
*
* [
* 'taxonomy' => [
* 'slug_1' => id_1,
* 'slug_2' => id_2,
* ...
* ], ...
* ]
*
* @param array $taxonomies List of taxonomies to build the cache for.
* @return array A dictionary of taxonomies => dictionary of term slug => term id.
*/
private function get_term_ids_by_slug_cache( $taxonomies ) {
$result = array();
foreach ( $taxonomies as $taxonomy ) {
$terms = WC()->call_function(
'get_terms',
array(
'taxonomy' => wc_sanitize_taxonomy_name( $taxonomy ),
'hide_empty' => false,
'fields' => 'id=>slug',
)
);
$result[ $taxonomy ] = array_flip( $terms );
}
return $result;
}
/**
* Get the id of the term that defines a variation for a given taxonomy,
* or null if there's no such defining id (for variations having "Any <taxonomy>" as the definition)
*
* @param \WC_Product_Variation $variation The variation to get the defining term id for.
* @param string $taxonomy The taxonomy to get the defining term id for.
* @param array $term_ids_by_slug_cache A term ids by slug as generated by get_term_ids_by_slug_cache.
* @return int|null The term id, or null if there's no defining id for that taxonomy in that variation.
*/
private function get_variation_definition_term_id( \WC_Product_Variation $variation, string $taxonomy, array $term_ids_by_slug_cache ) {
$variation_attributes = $variation->get_attributes();
$term_slug = ArrayUtil::get_value_or_default( $variation_attributes, $taxonomy );
if ( $term_slug ) {
return $term_ids_by_slug_cache[ $taxonomy ][ $term_slug ];
} else {
return null;
}
}
/**
* Get the variations of a given variable product.
*
* @param \WC_Product_Variable $product The product to get the variations for.
* @return array An array of WC_Product_Variation objects.
*/
private function get_variations_of( \WC_Product_Variable $product ) {
$variation_ids = $product->get_children();
return array_map(
function( $id ) {
return WC()->call_function( 'wc_get_product', $id );
},
$variation_ids
);
}
/**
* Check if a given product is a variable product.
*
* @param \WC_Product $product The product to check.
* @return bool True if it's a variable product, false otherwise.
*/
private function is_variable_product( \WC_Product $product ) {
return is_a( $product, \WC_Product_Variable::class );
}
/**
* Check if a given product is a variation.
*
* @param \WC_Product $product The product to check.
* @return bool True if it's a variation, false otherwise.
*/
private function is_variation( \WC_Product $product ) {
return is_a( $product, \WC_Product_Variation::class );
}
/**
* Return the list of taxonomies used for variations on a product together with
* the associated term ids, with the following format:
*
* [
* 'taxonomy_name' =>
* [
* 'term_ids' => [id, id, ...],
* 'used_for_variations' => true|false
* ], ...
* ]
*
* @param \WC_Product $product The product to get the attribute taxonomies for.
* @return array Information about the attribute taxonomies of the product.
*/
private function get_attribute_taxonomies( \WC_Product $product ) {
$product_attributes = $product->get_attributes();
$result = array();
foreach ( $product_attributes as $taxonomy_name => $attribute_data ) {
if ( ! $attribute_data->get_id() ) {
// Custom product attribute, not suitable for attribute-based filtering.
continue;
}
$result[ $taxonomy_name ] = array(
'term_ids' => $attribute_data->get_options(),
'used_for_variations' => $attribute_data->get_variation(),
);
}
return $result;
}
/**
* Insert one entry in the lookup table.
*
* @param int $product_id The product id.
* @param int $product_or_parent_id The product id for non-variable products, the main/parent product id for variations.
* @param string $taxonomy Taxonomy name.
* @param int $term_id Term id.
* @param bool $is_variation_attribute True if the taxonomy corresponds to an attribute used to define variations.
* @param bool $has_stock True if the product is in stock.
*/
private function insert_lookup_table_data( int $product_id, int $product_or_parent_id, string $taxonomy, int $term_id, bool $is_variation_attribute, bool $has_stock ) {
global $wpdb;
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query(
$wpdb->prepare(
'INSERT INTO ' . $this->lookup_table_name . ' (
product_id,
product_or_parent_id,
taxonomy,
term_id,
is_variation_attribute,
in_stock)
VALUES
( %d, %d, %s, %d, %d, %d )',
$product_id,
$product_or_parent_id,
$taxonomy,
$term_id,
$is_variation_attribute ? 1 : 0,
$has_stock ? 1 : 0
)
);
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Handler for the woocommerce_rest_insert_product hook.
* Needed to update the lookup table when the REST API batch insert/update endpoints are used.
*
* @param \WP_Post $product The post representing the created or updated product.
* @param \WP_REST_Request $request The REST request that caused the hook to be fired.
* @return void
*/
private function on_product_created_or_updated_via_rest_api( \WP_Post $product, \WP_REST_Request $request ): void {
if ( StringUtil::ends_with( $request->get_route(), '/batch' ) ) {
$this->on_product_changed( $product->ID );
}
}
/**
* Tells if a lookup table regeneration is currently in progress.
*
* @return bool True if a lookup table regeneration is already in progress.
*/
public function regeneration_is_in_progress() {
return get_option( 'woocommerce_attribute_lookup_regeneration_in_progress', null ) === 'yes';
}
/**
* Set a permanent flag (via option) indicating that the lookup table regeneration is in process.
*/
public function set_regeneration_in_progress_flag() {
update_option( 'woocommerce_attribute_lookup_regeneration_in_progress', 'yes' );
}
/**
* Remove the flag indicating that the lookup table regeneration is in process.
*/
public function unset_regeneration_in_progress_flag() {
delete_option( 'woocommerce_attribute_lookup_regeneration_in_progress' );
}
/**
* Set a flag indicating that the last lookup table regeneration process started was aborted.
*/
public function set_regeneration_aborted_flag() {
update_option( 'woocommerce_attribute_lookup_regeneration_aborted', 'yes' );
}
/**
* Remove the flag indicating that the last lookup table regeneration process started was aborted.
*/
public function unset_regeneration_aborted_flag() {
delete_option( 'woocommerce_attribute_lookup_regeneration_aborted' );
}
/**
* Tells if the last lookup table regeneration process started was aborted
* (via deleting the 'woocommerce_attribute_lookup_regeneration_in_progress' option).
*
* @return bool True if the last lookup table regeneration process was aborted.
*/
public function regeneration_was_aborted(): bool {
return get_option( 'woocommerce_attribute_lookup_regeneration_aborted' ) === 'yes';
}
/**
* Check if the lookup table contains any entry at all.
*
* @return bool True if the table contains entries, false if the table is empty.
*/
public function lookup_table_has_data(): bool {
global $wpdb;
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return ( (int) $wpdb->get_var( "SELECT EXISTS (SELECT 1 FROM {$this->lookup_table_name})" ) ) !== 0;
}
/**
* Handler for 'woocommerce_get_sections_products', adds the "Advanced" section to the product settings.
*
* @param array $products Original array of settings sections.
* @return array New array of settings sections.
*/
private function add_advanced_section_to_product_settings( array $products ): array {
if ( $this->check_lookup_table_exists() ) {
$products['advanced'] = __( 'Advanced', 'woocommerce' );
}
return $products;
}
/**
* Handler for 'woocommerce_get_settings_products', adds the settings related to the product attributes lookup table.
*
* @param array $settings Original settings configuration array.
* @param string $section_id Settings section identifier.
* @return array New settings configuration array.
*/
private function add_product_attributes_lookup_table_settings( array $settings, string $section_id ): array {
if ( $section_id === 'advanced' && $this->check_lookup_table_exists() ) {
$title_item = array(
'title' => __( 'Product attributes lookup table', 'woocommerce' ),
'type' => 'title',
);
$regeneration_is_in_progress = $this->regeneration_is_in_progress();
if ( $regeneration_is_in_progress ) {
$title_item['desc'] = __( 'These settings are not available while the lookup table regeneration is in progress.', 'woocommerce' );
}
$settings[] = $title_item;
if ( ! $regeneration_is_in_progress ) {
$regeneration_aborted_warning =
$this->regeneration_was_aborted() ?
sprintf(
"<p><strong style='color: #E00000'>%s</strong></p><p>%s</p>",
__( 'WARNING: The product attributes lookup table regeneration process was aborted.', 'woocommerce' ),
__( 'This means that the table is probably in an inconsistent state. It\'s recommended to run a new regeneration process or to resume the aborted process (Status - Tools - Regenerate the product attributes lookup table/Resume the product attributes lookup table regeneration) before enabling the table usage.', 'woocommerce' )
) : null;
$settings[] = array(
'title' => __( 'Enable table usage', 'woocommerce' ),
'desc' => __( 'Use the product attributes lookup table for catalog filtering.', 'woocommerce' ),
'desc_tip' => $regeneration_aborted_warning,
'id' => 'woocommerce_attribute_lookup_enabled',
'default' => 'no',
'type' => 'checkbox',
'checkboxgroup' => 'start',
);
$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;
}
}