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:
parent
53eb5b9219
commit
3c0463ada1
|
@ -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,
|
||||
|
|
|
@ -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) [
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue