Filter data count mismatch > Create the get_attribute_and_meta_counts method (https://github.com/woocommerce/woocommerce-blocks/pull/8599)

* Introduce the new get_attribute_and_meta_counts method.

* Ensure that if no term_slug or term_id is found for counting, the default list with all terms (with count equal zero) is returned instead.

* update conditional for the slug, the empty state for requests without filter attributes and the condition query for 'and'

* Introduce the get_terms_list method.

* Remove the legacy get_attribute_counts method and update its calls to rely on the new get_attribute_and_meta_counts method instead.

* Update the query to ensure that if a parent product has multiple identical attributes, they are counted once.

* Update to start relying on the get_product_by_metas method for counting product metas

* Add a new where_clause to only include product metas and attributes in the macro query if they are not empty.

* Add wpdb->prepare to the macro query and the get_terms_list method.

* Replace the raw atomic query for fetching the filtered terms with the new get_product_by_filtered_terms method.

* Update the request params for the get_attribute_and_meta_counts method.

* Update the request params for the product metas (min and max price).

* Update the query and returned value on get_terms_list.

* Update the validation for returning the default counts when no values are filtered.

* Update the query on get_terms_list to use ->prefix

* Update the  variable for the query to rely on the filtered one. Update the min_price and max_price format on get_product_by_metas.

* Ensure the get_product_by_filtered_terms method is triggered for each one of the filtered terms and update the macro query to include those term ids on the WHERE clause.

* Make adjustments for the 'and' condition to work as expected.

* Ensure the queryState.attributes is properly added as a param to the API request to correctly fetch the attribute count data.

* Ensure the get_product_by_metas method is only triggered when at least one of the metas in the request is not empty.

* Join type update: for the 'and' (all) filter condition, items with the count zero are not displayed.

* wpdb prepare the where clauses

* Update the get_product_by_filtered_terms query wpdb prepare params

* update the get_product_by_metas method's where clause preparation.

* Update the where clause preparation for get_attribute_and_meta_counts so we don't rely on interpolated variables anymore.

* Adjust the get_attribute_and_meta_counts method for usage alongside the rating filter.

* Adjust the query for fetching the attribute counts for filtered ratings.

* Add support for the filter by stock.

* Ensure the product attribute counts are correct if the parent product receives a rating.

* Ensure product_or_parent_id is used only when the filter by rating is used, not affecting price or stock filters.

* Add the missing else condition.

* Enable caching.

* Address CR

* Update query for average rating.

* remove file accidentally commited.

* When multiple ratings are selected, make sure the where clause is updated accordingly for each one of them.

* Start updating the stock_status logic to account for when multiple options are selected by the user.

* Ensure the counts are properly updated when more than one stock status is selected.

* Ditch the is_array condition for the average_rating counts as  is always an array.

* Deprecate the second param attributes for the get_attribute_counts method.

* Add the filtered_attribute to the transient_key

* Bypass cache if WP_DEBUG is enabled.

* Update formatting for macro query.

* Fix mixed tabs spaces on query

* Update spacing/formatting for SQL queries.

* Minor: update indentation for the main SQL query

---------

Co-authored-by: roykho <roykho77@gmail.com>
This commit is contained in:
Patricia Hillebrandt 2023-04-11 12:32:24 -03:00 committed by GitHub
parent 53eb5b9219
commit 3c0463ada1
3 changed files with 222 additions and 103 deletions

View File

@ -142,9 +142,6 @@ const AttributeFilterBlock = ( {
shouldSelect: blockAttributes.attributeId > 0,
} );
const filterAvailableTerms =
blockAttributes.displayStyle !== 'dropdown' &&
blockAttributes.queryType === 'and';
const { results: filteredCounts, isLoading: filteredCountsLoading } =
useCollectionData( {
queryAttribute: {
@ -153,7 +150,6 @@ const AttributeFilterBlock = ( {
},
queryState: {
...queryState,
attributes: filterAvailableTerms ? queryState.attributes : null,
},
productIds,
isEditor,

View File

@ -91,49 +91,12 @@ class ProductCollectionData extends AbstractRoute {
}
if ( ! empty( $request['calculate_attribute_counts'] ) ) {
$taxonomy__or_queries = [];
$taxonomy__and_queries = [];
foreach ( $request['calculate_attribute_counts'] as $attributes_to_count ) {
if ( ! empty( $attributes_to_count['taxonomy'] ) ) {
if ( empty( $attributes_to_count['query_type'] ) || 'or' === $attributes_to_count['query_type'] ) {
$taxonomy__or_queries[] = $attributes_to_count['taxonomy'];
} else {
$taxonomy__and_queries[] = $attributes_to_count['taxonomy'];
}
if ( ! isset( $attributes_to_count['taxonomy'] ) ) {
continue;
}
}
$data['attribute_counts'] = [];
// Or type queries need special handling because the attribute, if set, needs removing from the query first otherwise counts would not be correct.
if ( $taxonomy__or_queries ) {
foreach ( $taxonomy__or_queries as $taxonomy ) {
$filter_request = clone $request;
$filter_attributes = $filter_request->get_param( 'attributes' );
if ( ! empty( $filter_attributes ) ) {
$filter_attributes = array_filter(
$filter_attributes,
function( $query ) use ( $taxonomy ) {
return $query['attribute'] !== $taxonomy;
}
);
}
$filter_request->set_param( 'attributes', $filter_attributes );
$counts = $filters->get_attribute_counts( $filter_request, [ $taxonomy ] );
foreach ( $counts as $key => $value ) {
$data['attribute_counts'][] = (object) [
'term' => $key,
'count' => $value,
];
}
}
}
if ( $taxonomy__and_queries ) {
$counts = $filters->get_attribute_counts( $request, $taxonomy__and_queries );
$counts = $filters->get_attribute_counts( $request, $attributes_to_count['taxonomy'] );
foreach ( $counts as $key => $value ) {
$data['attribute_counts'][] = (object) [

View File

@ -2,6 +2,8 @@
namespace Automattic\WooCommerce\StoreApi\Utilities;
use Automattic\WooCommerce\StoreApi\Utilities\ProductQuery;
use Exception;
use WP_REST_Request;
/**
* Product Query filters class.
@ -111,63 +113,194 @@ class ProductQueryFilters {
}
/**
* Get attribute counts for the current products.
* Get terms list for a given taxonomy.
*
* @param \WP_REST_Request $request The request object.
* @param array $attributes Attributes to count, either names or ids.
* @return array termId=>count pairs.
* @param string $taxonomy Taxonomy name.
*
* @return array
*/
public function get_attribute_counts( $request, $attributes = [] ) {
public function get_terms_list( string $taxonomy ) {
global $wpdb;
// Remove paging and sorting params from the request.
$request->set_param( 'page', null );
$request->set_param( 'per_page', null );
$request->set_param( 'order', null );
$request->set_param( 'orderby', null );
return $wpdb->get_results(
$wpdb->prepare(
"SELECT term_id as term_count_id,
count(DISTINCT product_or_parent_id) as term_count
FROM {$wpdb->prefix}wc_product_attributes_lookup
WHERE taxonomy = %s
GROUP BY term_id",
$taxonomy
)
);
}
// Grab the request from the WP Query object, and remove SQL_CALC_FOUND_ROWS and Limits so we get a list of all products.
$product_query = new ProductQuery();
/**
* Get the empty terms list for a given taxonomy.
*
* @param string $taxonomy Taxonomy name.
*
* @return array
*/
public function get_empty_terms_list( string $taxonomy ) {
global $wpdb;
add_filter( 'posts_clauses', array( $product_query, 'add_query_clauses' ), 10, 2 );
add_filter( 'posts_pre_query', '__return_empty_array' );
return $wpdb->get_results(
$wpdb->prepare(
"SELECT DISTINCT term_id as term_count_id,
0 as term_count
FROM {$wpdb->prefix}wc_product_attributes_lookup
WHERE taxonomy = %s",
$taxonomy
)
);
}
$query_args = $product_query->prepare_objects_query( $request );
$query_args['no_found_rows'] = true;
$query_args['posts_per_page'] = -1;
$query = new \WP_Query();
$result = $query->query( $query_args );
$product_query_sql = $query->request;
/**
* Get attribute and meta counts.
*
* @param WP_REST_Request $request Request data.
* @param string $filtered_attribute The attribute to count.
*
* @return array
*/
public function get_attribute_counts( $request, $filtered_attribute ) {
if ( is_array( $filtered_attribute ) ) {
wc_deprecated_argument( 'attributes', 'TBD', 'get_attribute_counts does not require an array of attributes as the second parameter anymore. Provide the filtered attribute as a string instead.' );
remove_filter( 'posts_clauses', array( $product_query, 'add_query_clauses' ), 10 );
remove_filter( 'posts_pre_query', '__return_empty_array' );
$filtered_attribute = ! empty( $filtered_attribute[0] ) ? $filtered_attribute[0] : '';
if ( count( $attributes ) === count( array_filter( $attributes, 'is_numeric' ) ) ) {
$attributes = array_map( 'wc_attribute_taxonomy_name_by_id', wp_parse_id_list( $attributes ) );
if ( empty( $filtered_attribute ) ) {
return array();
}
}
$attributes_to_count = array_map(
function( $attribute ) {
$attribute = wc_sanitize_taxonomy_name( $attribute );
return esc_sql( $attribute );
},
$attributes
$attributes_data = $request->get_param( 'attributes' );
$calculate_attribute_counts = $request->get_param( 'calculate_attribute_counts' );
$min_price = $request->get_param( 'min_price' );
$max_price = $request->get_param( 'max_price' );
$rating = $request->get_param( 'rating' );
$stock_status = $request->get_param( 'stock_status' );
$transient_key = 'wc_get_attribute_and_meta_counts_' . md5(
wp_json_encode(
array(
'attributes_data' => $attributes_data,
'calculate_attribute_counts' => $calculate_attribute_counts,
'min_price' => $min_price,
'max_price' => $max_price,
'rating' => $rating,
'stock_status' => $stock_status,
'filtered_attribute' => $filtered_attribute,
)
)
);
$attributes_to_count_sql = 'AND term_taxonomy.taxonomy IN ("' . implode( '","', $attributes_to_count ) . '")';
$attribute_count_sql = "
SELECT COUNT( DISTINCT posts.ID ) as term_count, terms.term_id as term_count_id
FROM {$wpdb->posts} AS posts
INNER JOIN {$wpdb->term_relationships} AS term_relationships ON 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 )
WHERE posts.ID IN ( {$product_query_sql} )
{$attributes_to_count_sql}
GROUP BY terms.term_id
";
$results = $wpdb->get_results( $attribute_count_sql ); // phpcs:ignore
$cached_results = get_transient( $transient_key );
if ( ! empty( $cached_results ) && defined( 'WP_DEBUG' ) && ! WP_DEBUG ) {
return $cached_results;
}
return array_map( 'absint', wp_list_pluck( $results, 'term_count', 'term_count_id' ) );
if ( empty( $attributes_data ) && empty( $min_price ) && empty( $max_price ) && empty( $rating ) && empty( $stock_status ) ) {
$counts = $this->get_terms_list( $filtered_attribute );
return array_map( 'absint', wp_list_pluck( $counts, 'term_count', 'term_count_id' ) );
}
$where_clause = '';
if ( ! empty( $min_price ) || ! empty( $max_price ) || ! empty( $rating ) || ! empty( $stock_status ) ) {
$product_metas = [
'min_price' => $min_price,
'max_price' => $max_price,
'average_rating' => $rating,
'stock_status' => $stock_status,
];
$filtered_products_by_metas = $this->get_product_by_metas( $product_metas );
$formatted_filtered_products_by_metas = implode( ',', array_map( 'intval', $filtered_products_by_metas ) );
if ( ! empty( $formatted_filtered_products_by_metas ) ) {
if ( ! empty( $rating ) ) {
$where_clause .= sprintf( ' AND product_attribute_lookup.product_or_parent_id IN (%1s)', $formatted_filtered_products_by_metas );
} else {
$where_clause .= sprintf( ' AND product_attribute_lookup.product_id IN (%1s)', $formatted_filtered_products_by_metas );
}
} else {
$counts = $this->get_empty_terms_list( $filtered_attribute );
return array_map( 'absint', wp_list_pluck( $counts, 'term_count', 'term_count_id' ) );
}
}
$join_type = 'LEFT';
foreach ( $attributes_data as $attribute ) {
$filtered_terms = $attribute['slug'] ?? '';
if ( empty( $filtered_terms ) ) {
continue;
}
$taxonomy = $attribute['attribute'] ?? '';
$term_ids = [];
if ( in_array( $taxonomy, wc_get_attribute_taxonomy_names(), true ) ) {
foreach ( $filtered_terms as $filtered_term ) {
$term = get_term_by( 'slug', $filtered_term, $taxonomy );
if ( is_object( $term ) ) {
$term_ids[] = $term->term_id;
}
}
}
if ( empty( $term_ids ) ) {
continue;
}
foreach ( $calculate_attribute_counts as $calculate_attribute_count ) {
if ( ! isset( $calculate_attribute_count['taxonomy'] ) && ! isset( $calculate_attribute_count['query_type'] ) ) {
continue;
}
$query_type = $calculate_attribute_count['query_type'];
$filtered_products_by_terms = $this->get_product_by_filtered_terms( $calculate_attribute_count['taxonomy'], $term_ids, $query_type );
$formatted_filtered_products_by_terms = implode( ',', array_map( 'intval', $filtered_products_by_terms ) );
if ( ! empty( $formatted_filtered_products_by_terms ) ) {
$where_clause .= sprintf( ' AND product_attribute_lookup.product_or_parent_id IN (%1s)', $formatted_filtered_products_by_terms );
}
if ( $calculate_attribute_count['taxonomy'] === $filtered_attribute ) {
$join_type = 'or' === $query_type ? 'LEFT' : 'INNER';
}
}
}
global $wpdb;
$counts = $wpdb->get_results(
$wpdb->prepare(
"
SELECT attributes.term_id as term_count_id, coalesce(term_count, 0) as term_count
FROM (SELECT DISTINCT term_id
FROM {$wpdb->prefix}wc_product_attributes_lookup
WHERE taxonomy = %s) as attributes %1s JOIN (
SELECT COUNT(DISTINCT product_attribute_lookup.product_or_parent_id) as term_count, product_attribute_lookup.term_id
FROM {$wpdb->prefix}wc_product_attributes_lookup product_attribute_lookup
INNER JOIN {$wpdb->posts} posts
ON posts.ID = product_attribute_lookup.product_id
WHERE posts.post_type IN ('product', 'product_variation') AND posts.post_status = 'publish'%1s
GROUP BY product_attribute_lookup.term_id
) summarize
ON attributes.term_id = summarize.term_id
",
$filtered_attribute,
$join_type,
$where_clause
)
);
$results = array_map( 'absint', wp_list_pluck( $counts, 'term_count', 'term_count_id' ) );
set_transient( $transient_key, $results, 24 * HOUR_IN_SECONDS );
return $results;
}
/**
@ -229,30 +362,59 @@ class ProductQueryFilters {
$where = array();
$results = array();
$params = array();
foreach ( $metas as $column => $value ) {
if ( empty( $value ) ) {
continue;
}
if ( 'stock_status' === $column ) {
$stock_product_ids = array();
foreach ( $value as $stock_status ) {
$stock_product_ids[] = $wpdb->get_col(
$wpdb->prepare(
"SELECT DISTINCT product_id FROM {$wpdb->prefix}wc_product_meta_lookup WHERE stock_status = %s",
$stock_status
)
);
}
$where[] = 'product_id IN (' . implode( ',', array_merge( ...$stock_product_ids ) ) . ')';
continue;
}
if ( 'min_price' === $column ) {
$where[] = "{$column} >= %f";
$params[] = (float) $value;
$where[] = "{$column} >= %d";
$params[] = intval( $value ) / 100;
continue;
}
if ( 'max_price' === $column ) {
$where[] = "{$column} <= %f";
$params[] = (float) $value;
$where[] = "{$column} <= %d";
$params[] = intval( $value ) / 100;
continue;
}
$where[] = "{$column} = %s";
if ( 'average_rating' === $column ) {
$where_rating = array();
foreach ( $value as $rating ) {
$where_rating[] = sprintf( '(average_rating >= %f - 0.5 AND average_rating < %f + 0.5)', $rating, $rating );
}
$where[] = '(' . implode( ' OR ', $where_rating ) . ')';
continue;
}
$where[] = sprintf( "%1s = '%s'", $column, $value );
$params[] = $value;
}
if ( ! empty( $where ) ) {
$where_clause = implode( ' AND ', $where );
// Use a parameterized query.
$where_clause = sprintf( $where_clause, ...$params );
$results = $wpdb->get_col(
$wpdb->prepare( "SELECT DISTINCT product_id FROM {$wpdb->prefix}wc_product_meta_lookup WHERE {$where_clause}", // phpcs:ignore
$params
$wpdb->prepare(
"SELECT DISTINCT product_id FROM {$wpdb->prefix}wc_product_meta_lookup WHERE %1s",
$where_clause
)
);
}
@ -277,38 +439,36 @@ class ProductQueryFilters {
$term_ids = implode( ',', array_map( 'intval', $term_ids ) );
if ( 'or' === $query_type ) {
// phpcs:disable
$results = $wpdb->get_col(
$wpdb->prepare(
"
SELECT DISTINCT `product_or_parent_id`
FROM {$wpdb->prefix}wc_product_attributes_lookup
WHERE `taxonomy` = %s
AND `term_id` IN ({$term_ids})
AND `term_id` IN (%1s)
",
$taxonomy
$taxonomy,
$term_ids
)
);
// phpcs:enable
}
if ( 'and' === $query_type ) {
// phpcs:disable
$results = $wpdb->get_col(
$wpdb->prepare(
"
SELECT DISTINCT `product_or_parent_id`
FROM {$wpdb->prefix}wc_product_attributes_lookup
WHERE `taxonomy` = %s
AND `term_id` IN ({$term_ids})
AND `term_id` IN (%1s)
GROUP BY `product_or_parent_id`
HAVING COUNT( DISTINCT `term_id` ) >= %d
",
$taxonomy,
$term_ids,
$term_count
)
);
// phpcs:enable
}
return $results;