ibility = $changeset['catalog_visibility'];
if ( 'visible' === $new_visibility || 'catalog' === $new_visibility ) {
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' => $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 " 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 'yes' === get_option( 'woocommerce_attribute_lookup_regeneration_in_progress', null );
}
/**
* 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 'yes' === get_option( 'woocommerce_attribute_lookup_regeneration_aborted' );
}
/**
* 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;
}
}