Hotline 2897 6541 22
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 ( 'advanced' === $section_id && $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( "
%s
%s
", __( '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( 'title' => __( 'Optimized updates', 'woocommerce' ), 'desc' => __( 'Uses much more performant queries to update the lookup table, but may not be compatible with some extensions.', 'woocommerce' ), 'desc_tip' => __( 'This setting only works when product data is stored in the posts table.', 'woocommerce' ), 'id' => 'woocommerce_attribute_lookup_optimized_updates', 'default' => 'no', 'type' => 'checkbox', 'checkboxgroup' => 'start', ); } $settings[] = array( 'type' => 'sectionend' ); } return $settings; } /** * Check if the optimized database access setting is enabled. * * @return bool True if the optimized database access setting is enabled. */ public function optimized_data_access_is_enabled() { return 'yes' === get_option( 'woocommerce_attribute_lookup_optimized_updates' ); } /** * Create the lookup table data for a product or variation using optimized database access. * For variable products entries are created for the main product and for all the variations. * * @param int $product_id Product or variation id. */ private function create_data_for_product_cpt( int $product_id ) { $this->last_create_operation_failed = false; try { $this->create_data_for_product_cpt_core( $product_id ); } catch ( \Exception $e ) { $data = array( 'source' => 'palt-updates', 'product_id' => $product_id, ); if ( $e instanceof \WC_Data_Exception ) { $data = array_merge( $data, $e->getErrorData() ); } else { $data['exception'] = $e; } WC()->call_function( 'wc_get_logger' ) ->error( "Lookup data creation (optimized) failed for product $product_id: " . $e->getMessage(), $data ); $this->last_create_operation_failed = true; } } /** * Core version of create_data_for_product_cpt (doesn't catch exceptions). * * @param int $product_id Product or variation id. * @return void * @throws \WC_Data_Exception Wrongly serialized attribute data found, or INSERT statement failed. */ private function create_data_for_product_cpt_core( int $product_id ) { global $wpdb; // phpcs:disable WordPress.DB.PreparedSQL $sql = $wpdb->prepare( "delete from {$this->lookup_table_name} where product_or_parent_id=%d", $product_id ); $wpdb->query( $sql ); // phpcs:enable WordPress.DB.PreparedSQL // * Obtain list of product variations, together with stock statuses; also get the product type. // For a variation this will return just one entry, with type 'variation'. // 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. // $is_variation = true or false. $sql = $wpdb->prepare( "(select p.ID as id, null parent, 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, p.post_parent as parent, m.meta_value as stock_status, 'variation' 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.ID=%d or p.post_parent=%d)); ", $product_id, $product_id, $product_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $product_ids_with_stock_status = $wpdb->get_results( $sql, ARRAY_A ); $main_product_row = array_filter( $product_ids_with_stock_status, fn( $item ) => 'variation' !== $item['product_type'] ); $is_variation = empty( $main_product_row ); $main_product_id = $is_variation ? current( $product_ids_with_stock_status )['parent'] : $product_id; $is_variable_product = ! $is_variation && ( 'variable' === current( $main_product_row )['product_type'] ); $product_ids_with_stock_status = ArrayUtil::group_by_column( $product_ids_with_stock_status, 'id', true ); $variation_ids = $is_variation ? array( $product_id ) : array_keys( array_diff_key( $product_ids_with_stock_status, array( $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", $main_product_id, '_product_attributes' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $temp = $wpdb->get_var( $sql ); if ( is_null( $temp ) ) { // The product has no attributes, thus there's no attributes lookup data to generate. return; } // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize $temp = unserialize( $temp ); if ( false === $temp ) { throw new \WC_Data_Exception( 0, 'The product attributes metadata row is not properly serialized' ); } $temp = array_filter( $temp, fn( $item, $slug ) => StringUtil::starts_with( $slug, 'pa_' ) && '' === $item['value'], ARRAY_FILTER_USE_BOTH ); $attributes_not_for_variations = $is_variation || $is_variable_product ? array_keys( array_filter( $temp, fn( $item ) => 0 === $item['is_variation'] ) ) : array_keys( $temp ); // * Obtain the terms used for each attribute. // Output: $terms_used_per_attribute = // [ // 'pa_...' => [ // [ // 'term_id' =>