From 16ea028b0378f30d1fe9113503071ae44cc97590 Mon Sep 17 00:00:00 2001 From: Luigi Date: Mon, 12 Sep 2022 12:02:04 +0200 Subject: [PATCH 01/12] Product Query: Fix pagination issue --- src/BlockTypes/ProductQuery.php | 43 +++++++++------------------------ 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index 2b39c231988..8fc72d91657 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -37,27 +37,6 @@ protected function initialize() { } - /** - * Remove the query block filter and parse the custom query - * - * This function is supposed to be called by the `query_loop_block_query_vars` - * filter. It de-registers the filter to make sure it runs only once and doesn't end - * up hi-jacking future Query Loop blocks. - * - * It needs unfortunately to be `public` or otherwise the filter can't call it. - * - * @param WP_Query $query The WordPress Query. - * @return array - */ - public function get_query_by_attributes_once( $query ) { - remove_filter( - 'query_loop_block_query_vars', - array( $this, 'get_query_by_attributes_once' ), - 10 - ); - - return $this->get_query_by_attributes( $query, $this->parsed_block ); - } /** * Update the query for the product query block. @@ -66,28 +45,30 @@ public function get_query_by_attributes_once( $query ) { * @param array $parsed_block The block being rendered. */ public function update_query( $pre_render, $parsed_block ) { - if ( 'core/query' !== $parsed_block['blockName'] || ! isset( $parsed_block['attrs']['__woocommerceVariationProps'] ) ) { + if ( 'core/query' !== $parsed_block['blockName'] ) { return; } $this->parsed_block = $parsed_block; - add_filter( - 'query_loop_block_query_vars', - array( $this, 'get_query_by_attributes_once' ), - 10, - 1 - ); + if ( isset( $parsed_block['attrs']['__woocommerceVariationProps'] ) ) { + add_filter( + 'query_loop_block_query_vars', + array( $this, 'get_query_by_attributes' ), + 10, + 1 + ); + } } /** * Return a custom query based on the attributes. * - * @param WP_Query $query The WordPress Query. - * @param WP_Block $parsed_block The block being rendered. + * @param WP_Query $query The WordPress Query. * @return array */ - public function get_query_by_attributes( $query, $parsed_block ) { + public function get_query_by_attributes( $query ) { + $parsed_block = $this->parsed_block; if ( ! isset( $parsed_block['attrs']['__woocommerceVariationProps'] ) ) { return $query; } From 6e93dd02c77f91dfe99f6459cc66825b7782550f Mon Sep 17 00:00:00 2001 From: Luigi Date: Thu, 15 Sep 2022 14:10:47 +0200 Subject: [PATCH 02/12] Product Query - Add support for the Filter By Price Block #6790 Product Query - Add support for the Filter By Price Block --- src/BlockTypes/PriceFilter.php | 2 + src/BlockTypes/ProductQuery.php | 109 ++++++++++++++++++++++++++++---- 2 files changed, 99 insertions(+), 12 deletions(-) diff --git a/src/BlockTypes/PriceFilter.php b/src/BlockTypes/PriceFilter.php index 0c9ab501c8e..6274d326375 100644 --- a/src/BlockTypes/PriceFilter.php +++ b/src/BlockTypes/PriceFilter.php @@ -12,4 +12,6 @@ class PriceFilter extends AbstractBlock { * @var string */ protected $block_name = 'price-filter'; + const MIN_PRICE_QUERY_VAR = 'min_price'; + const MAX_PRICE_QUERY_VAR = 'max_price'; } diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index 8fc72d91657..b03b4ec347a 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -27,6 +27,10 @@ class ProductQuery extends AbstractBlock { * - Hook into pre_render_block to update the query. */ protected function initialize() { + add_filter( 'query_vars', array( $this, 'set_query_vars' ) ); + // Set this so that our product filters can detect if it's a PHP template. + $this->asset_data_registry->add( 'has_filterable_products', true, true ); + $this->asset_data_registry->add( 'is_rendering_php_template', true, true ); parent::initialize(); add_filter( 'pre_render_block', @@ -37,7 +41,6 @@ protected function initialize() { } - /** * Update the query for the product query block. * @@ -54,7 +57,7 @@ public function update_query( $pre_render, $parsed_block ) { if ( isset( $parsed_block['attrs']['__woocommerceVariationProps'] ) ) { add_filter( 'query_loop_block_query_vars', - array( $this, 'get_query_by_attributes' ), + array( $this, 'build_query' ), 10, 1 ); @@ -62,12 +65,12 @@ public function update_query( $pre_render, $parsed_block ) { } /** - * Return a custom query based on the attributes. + * Return a custom query based on attributes, filters and global WP_Query. * * @param WP_Query $query The WordPress Query. * @return array */ - public function get_query_by_attributes( $query ) { + public function build_query( $query ) { $parsed_block = $this->parsed_block; if ( ! isset( $parsed_block['attrs']['__woocommerceVariationProps'] ) ) { return $query; @@ -79,24 +82,39 @@ public function get_query_by_attributes( $query ) { 'post_status' => 'publish', 'posts_per_page' => $query['posts_per_page'], 'orderby' => $query['orderby'], + 'orderby' => $query['orderby'], 'order' => $query['order'], + // Ignoring the warning of not using meta queries. + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array(), + ); + + $queries_attributes = $this->get_queries_by_attributes( $variation_props ); + $queries_filters = $this->get_queries_by_applied_filters(); + + return array_reduce( + array_merge( + $queries_attributes, + $queries_filters + ), + function( $acc, $query ) { + // Ignoring the warning of not using meta queries. + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + $acc['meta_query'] = isset( $query['meta_query'] ) ? array_merge( $acc['meta_query'], array( $query['meta_query'] ) ) : $acc['meta_query']; + return $acc; + }, + $common_query_values ); - $on_sale_query = $this->get_on_sale_products_query( $variation_props ); - return array_merge( $query, $common_query_values, $on_sale_query ); } + /** * Return a query for on sale products. * - * @param array $variation_props Dedicated attributes for the variation. * @return array */ - private function get_on_sale_products_query( $variation_props ) { - if ( ! isset( $variation_props['attributes']['query']['onSale'] ) || true !== $variation_props['attributes']['query']['onSale'] ) { - return array(); - } - + private function get_on_sale_products_query() { return array( // Ignoring the warning of not using meta queries. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query @@ -117,4 +135,71 @@ private function get_on_sale_products_query( $variation_props ) { ), ); } + + /** + * Set the query vars that are used by filter blocks. + * + * @param array $qvars Public query vars. + * @return array + */ + public function set_query_vars( $qvars ) { + $filter_query_args = array( PriceFilter::MIN_PRICE_QUERY_VAR, PriceFilter::MAX_PRICE_QUERY_VAR ); + return array_merge( $qvars, $filter_query_args ); + } + + /** + * Return queries that are generated by query args + * + * @return array + */ + private function get_queries_by_applied_filters() { + return array( 'price_filter' => $this->get_filter_by_price_query() ); + } + + /** + * Return queries that are generated by attributes + * + * @param array $variation_props Dedicated attributes for the variation. + * @return array + */ + private function get_queries_by_attributes( $variation_props ) { + return array( + 'on_sale' => ( ! isset( $variation_props['attributes']['query']['onSale'] ) || true !== $variation_props['attributes']['query']['onSale'] ) ? array() : $this->get_on_sale_products_query(), + ); + } + + /** + * Return a query that filters products by price. + * + * @return array + */ + private function get_filter_by_price_query() { + $min_price = get_query_var( PriceFilter::MIN_PRICE_QUERY_VAR ); + $max_price = get_query_var( PriceFilter::MAX_PRICE_QUERY_VAR ); + + $max_price_query = empty( $max_price ) ? array() : [ + 'key' => '_price', + 'value' => $max_price, + 'compare' => '<=', + 'type' => 'numeric', + ]; + + $min_price_query = empty( $min_price ) ? array() : [ + 'key' => '_price', + 'value' => $min_price, + 'compare' => '>=', + 'type' => 'numeric', + ]; + + return array( + // Ignoring the warning of not using meta queries. + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + 'relation' => 'OR', + $max_price_query, + $min_price_query, + ), + ); + } + } From b243528f2dc703917063a352e1bfe4cb8c8a2545 Mon Sep 17 00:00:00 2001 From: Luigi Date: Fri, 16 Sep 2022 17:49:18 +0200 Subject: [PATCH 03/12] fix query relation --- src/BlockTypes/ProductQuery.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index b03b4ec347a..ac89c03a6da 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -82,7 +82,6 @@ public function build_query( $query ) { 'post_status' => 'publish', 'posts_per_page' => $query['posts_per_page'], 'orderby' => $query['orderby'], - 'orderby' => $query['orderby'], 'order' => $query['order'], // Ignoring the warning of not using meta queries. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query @@ -163,8 +162,9 @@ private function get_queries_by_applied_filters() { * @return array */ private function get_queries_by_attributes( $variation_props ) { + $on_sale_enabled = isset( $variation_props['attributes']['query']['onSale'] ) && true === $variation_props['attributes']['query']['onSale']; return array( - 'on_sale' => ( ! isset( $variation_props['attributes']['query']['onSale'] ) || true !== $variation_props['attributes']['query']['onSale'] ) ? array() : $this->get_on_sale_products_query(), + 'on_sale' => ( $on_sale_enabled ? $this->get_on_sale_products_query() : array() ), ); } @@ -195,7 +195,7 @@ private function get_filter_by_price_query() { // Ignoring the warning of not using meta queries. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( - 'relation' => 'OR', + 'relation' => 'AND', $max_price_query, $min_price_query, ), From e55a41dcf2f3bd366edb7837b1d8d8eca0c39c1b Mon Sep 17 00:00:00 2001 From: Luigi Date: Fri, 16 Sep 2022 18:52:33 +0200 Subject: [PATCH 04/12] fix on sale query --- src/BlockTypes/ProductQuery.php | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index ac89c03a6da..cdf4e15b78c 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -115,23 +115,7 @@ function( $acc, $query ) { */ private function get_on_sale_products_query() { return array( - // Ignoring the warning of not using meta queries. - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - 'meta_query' => array( - 'relation' => 'OR', - array( - 'key' => '_sale_price', - 'value' => 0, - 'compare' => '>', - 'type' => 'numeric', - ), - array( - 'key' => '_min_variation_sale_price', - 'value' => 0, - 'compare' => '>', - 'type' => 'numeric', - ), - ), + 'post_in' => wc_get_product_ids_on_sale(), ); } From 926f62c56f92778780eb49313307769d9c6f73d8 Mon Sep 17 00:00:00 2001 From: Luigi Date: Tue, 20 Sep 2022 15:30:28 +0200 Subject: [PATCH 05/12] Product Query - Add support for the Filter By Attributes block #6790 Product Query - Add support for the Filter By Attributes block --- src/BlockTypes/AttributeFilter.php | 4 +- src/BlockTypes/ProductQuery.php | 112 +++++++++++++++++++++++++++-- 2 files changed, 110 insertions(+), 6 deletions(-) diff --git a/src/BlockTypes/AttributeFilter.php b/src/BlockTypes/AttributeFilter.php index ec2d0bf3441..4b9d06ee7e9 100644 --- a/src/BlockTypes/AttributeFilter.php +++ b/src/BlockTypes/AttributeFilter.php @@ -10,7 +10,9 @@ class AttributeFilter extends AbstractBlock { * * @var string */ - protected $block_name = 'attribute-filter'; + protected $block_name = 'attribute-filter'; + const FILTER_QUERY_VAR = 'filter_'; + const QUERY_TYPE_QUERY_VAR = 'query_type_'; /** * Extra data passed through from server to client for block. diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index cdf4e15b78c..a34d8045765 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -19,6 +19,13 @@ class ProductQuery extends AbstractBlock { */ protected $parsed_block; + /** + * All the query args related to the filter by attributes block. + * + * @var array + */ + protected $attributes_filter_query_args = array(); + /** * Initialize this block type. * @@ -86,6 +93,8 @@ public function build_query( $query ) { // Ignoring the warning of not using meta queries. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array(), + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + 'tax_query' => array(), ); $queries_attributes = $this->get_queries_by_attributes( $variation_props ); @@ -100,6 +109,9 @@ function( $acc, $query ) { // Ignoring the warning of not using meta queries. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query $acc['meta_query'] = isset( $query['meta_query'] ) ? array_merge( $acc['meta_query'], array( $query['meta_query'] ) ) : $acc['meta_query']; + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + $acc['tax_query'] = isset( $query['tax_query'] ) ? array_merge( $acc['tax_query'], array( $query['tax_query'] ) ) : $acc['tax_query']; + return $acc; }, $common_query_values @@ -126,21 +138,58 @@ private function get_on_sale_products_query() { * @return array */ public function set_query_vars( $qvars ) { - $filter_query_args = array( PriceFilter::MIN_PRICE_QUERY_VAR, PriceFilter::MAX_PRICE_QUERY_VAR ); - return array_merge( $qvars, $filter_query_args ); + $attributes_filter_query_args = array_reduce( + array_values( $this->get_filter_by_attributes_query_vars() ), + function( $acc, $array ) { + return array_merge( array_values( $array ), $acc ); + }, + array() + ); + + $price_filter_query_args = array( PriceFilter::MIN_PRICE_QUERY_VAR, PriceFilter::MAX_PRICE_QUERY_VAR ); + return array_merge( $qvars, $price_filter_query_args, $attributes_filter_query_args ); + } + + /** + * Get all the query args related to the filter by attributes block. + * + * @return array + */ + private function get_filter_by_attributes_query_vars() { + + if ( ! empty( $this->attributes_filter_query_args ) ) { + return $this->attributes_filter_query_args; + } + + $this->attributes_filter_query_args = array_reduce( + wc_get_attribute_taxonomies(), + function( $acc, $attribute ) { + $acc[ $attribute->attribute_name ] = array( + 'filter' => AttributeFilter::FILTER_QUERY_VAR . $attribute->attribute_name, + 'query_type' => AttributeFilter::QUERY_TYPE_QUERY_VAR . $attribute->attribute_name, + ); + return $acc; + }, + array() + ); + + return $this->attributes_filter_query_args; } /** - * Return queries that are generated by query args + * Return queries that are generated by query args. * * @return array */ private function get_queries_by_applied_filters() { - return array( 'price_filter' => $this->get_filter_by_price_query() ); + return array( + 'price_filter' => $this->get_filter_by_price_query(), + 'attributes_filter' => $this->get_filter_by_attributes_query(), + ); } /** - * Return queries that are generated by attributes + * Return queries that are generated by block attributes. * * @param array $variation_props Dedicated attributes for the variation. * @return array @@ -175,6 +224,10 @@ private function get_filter_by_price_query() { 'type' => 'numeric', ]; + if ( empty( $min_price_query ) && empty( $max_price_query ) ) { + return array(); + } + return array( // Ignoring the warning of not using meta queries. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query @@ -186,4 +239,53 @@ private function get_filter_by_price_query() { ); } + /** + * Return a query that filters products by attributes. + * + * @return array + */ + private function get_filter_by_attributes_query() { + $attributes_filter_query_args = $this->get_filter_by_attributes_query_vars(); + + $queries = array_reduce( + $attributes_filter_query_args, + function( $acc, $query_args ) { + $attribute_name = $query_args['filter']; + $attribute_query_type = $query_args['query_type']; + + $attribute_name_value = get_query_var( $attribute_name ); + $attribute_query_type_value = get_query_var( $attribute_query_type ); + + if ( empty( $attribute_name_value ) ) { + return $acc; + } + + $attribute_name_value = explode( ',', $attribute_name_value ); + + $acc[] = array( + 'taxonomy' => str_replace( AttributeFilter::FILTER_QUERY_VAR, 'pa_', $attribute_name ), + 'field' => 'slug', + 'terms' => $attribute_name_value, + 'operator' => 'and' === $attribute_query_type_value ? 'AND' : 'IN', + ); + + return $acc; + }, + array() + ); + + if ( empty( $filters ) ) { + return array(); + } + + return array( + // Ignoring the warning of not using meta queries. + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + 'tax_query' => array( + 'relation' => 'AND', + $queries, + ), + ); + } + } From 1d56a2e90d0ef49330eef6b0133f117237d70e22 Mon Sep 17 00:00:00 2001 From: Luigi Date: Wed, 21 Sep 2022 18:56:49 +0200 Subject: [PATCH 06/12] fix bugged pagination and on-sale filter after refactor --- src/BlockTypes/ProductQuery.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index cdf4e15b78c..1ba8a4ad281 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -83,6 +83,7 @@ public function build_query( $query ) { 'posts_per_page' => $query['posts_per_page'], 'orderby' => $query['orderby'], 'order' => $query['order'], + 'offset' => $query['offset'], // Ignoring the warning of not using meta queries. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array(), @@ -97,10 +98,13 @@ public function build_query( $query ) { $queries_filters ), function( $acc, $query ) { - // Ignoring the warning of not using meta queries. - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - $acc['meta_query'] = isset( $query['meta_query'] ) ? array_merge( $acc['meta_query'], array( $query['meta_query'] ) ) : $acc['meta_query']; - return $acc; + if ( isset( $query['post__in'] ) ) { + $acc['post__in'] = isset( $acc['post__in'] ) ? array_merge( $acc['post__in'], $query['post__in'] ) : $query['post__in']; + } + // Ignoring the warning of not using meta queries. + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + $acc['meta_query'] = isset( $query['meta_query'] ) ? array_merge( $acc['meta_query'], array( $query['meta_query'] ) ) : $acc['meta_query']; + return $acc; }, $common_query_values ); @@ -115,7 +119,7 @@ function( $acc, $query ) { */ private function get_on_sale_products_query() { return array( - 'post_in' => wc_get_product_ids_on_sale(), + 'post__in' => wc_get_product_ids_on_sale(), ); } From 80a0dbf441352825567b1df11fa14c52df9e30b5 Mon Sep 17 00:00:00 2001 From: Luigi Date: Wed, 28 Sep 2022 19:30:56 +0200 Subject: [PATCH 07/12] address feedback --- src/BlockTypes/AttributeFilter.php | 6 +-- src/BlockTypes/ProductQuery.php | 75 ++++++++++++++++++++---------- 2 files changed, 54 insertions(+), 27 deletions(-) diff --git a/src/BlockTypes/AttributeFilter.php b/src/BlockTypes/AttributeFilter.php index 4b9d06ee7e9..0979f6d2353 100644 --- a/src/BlockTypes/AttributeFilter.php +++ b/src/BlockTypes/AttributeFilter.php @@ -10,9 +10,9 @@ class AttributeFilter extends AbstractBlock { * * @var string */ - protected $block_name = 'attribute-filter'; - const FILTER_QUERY_VAR = 'filter_'; - const QUERY_TYPE_QUERY_VAR = 'query_type_'; + protected $block_name = 'attribute-filter'; + const FILTER_QUERY_VAR_PREFIX = 'filter_'; + const QUERY_TYPE_QUERY_VAR_PREFIX = 'query_type_'; /** * Extra data passed through from server to client for block. diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index 8e86d8009d5..c6dd674f98d 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -97,6 +97,7 @@ public function build_query( $query ) { $common_query_values = array( 'post_type' => 'product', + 'post__in' => array(), 'post_status' => 'publish', 'posts_per_page' => $query['posts_per_page'], 'orderby' => $query['orderby'], @@ -109,18 +110,17 @@ public function build_query( $query ) { 'tax_query' => array(), ); - $queries_attributes = $this->get_queries_by_attributes( $parsed_block ); - $queries_filters = $this->get_queries_by_applied_filters(); + $queries_by_attributes = $this->get_queries_by_attributes( $parsed_block ); + $queries_by_filters = $this->get_queries_by_applied_filters(); return array_reduce( array_merge( - $queries_attributes, - $queries_filters + $queries_by_attributes, + $queries_by_filters ), function( $acc, $query ) { - if ( isset( $query['post__in'] ) ) { - $acc['post__in'] = isset( $acc['post__in'] ) ? array_intersect( $acc['post__in'], $query['post__in'] ) : $query['post__in']; - } + $acc['post__in'] = isset( $query['post__in'] ) ? $this->intersect_arrays_when_not_empty( $acc['post__in'], $query['post__in'] ) : $acc['post__in']; + // Ignoring the warning of not using meta queries. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query $acc['meta_query'] = isset( $query['meta_query'] ) ? array_merge( $acc['meta_query'], array( $query['meta_query'] ) ) : $acc['meta_query']; @@ -148,10 +148,11 @@ private function get_on_sale_products_query() { /** * Set the query vars that are used by filter blocks. * - * @param array $qvars Public query vars. + * @param array $public_query_vars Public query vars. * @return array */ - public function set_query_vars( $qvars ) { + public function set_query_vars( $public_query_vars ) { + $attributes_filter_query_args = array_reduce( array_values( $this->get_filter_by_attributes_query_vars() ), function( $acc, $array ) { @@ -161,16 +162,27 @@ function( $acc, $array ) { ); $price_filter_query_args = array( PriceFilter::MIN_PRICE_QUERY_VAR, PriceFilter::MAX_PRICE_QUERY_VAR ); - return array_merge( $qvars, $price_filter_query_args, $attributes_filter_query_args ); + return array_merge( $public_query_vars, $price_filter_query_args, $attributes_filter_query_args ); } /** * Get all the query args related to the filter by attributes block. * * @return array + * [color] => Array + * ( + * [filter] => filter_color + * [query_type] => query_type_color + * ) + * + * [size] => Array + * ( + * [filter] => filter_size + * [query_type] => query_type_size + * ) + * ) */ private function get_filter_by_attributes_query_vars() { - if ( ! empty( $this->attributes_filter_query_args ) ) { return $this->attributes_filter_query_args; } @@ -179,8 +191,8 @@ private function get_filter_by_attributes_query_vars() { wc_get_attribute_taxonomies(), function( $acc, $attribute ) { $acc[ $attribute->attribute_name ] = array( - 'filter' => AttributeFilter::FILTER_QUERY_VAR . $attribute->attribute_name, - 'query_type' => AttributeFilter::QUERY_TYPE_QUERY_VAR . $attribute->attribute_name, + 'filter' => AttributeFilter::FILTER_QUERY_VAR_PREFIX . $attribute->attribute_name, + 'query_type' => AttributeFilter::QUERY_TYPE_QUERY_VAR_PREFIX . $attribute->attribute_name, ); return $acc; }, @@ -203,13 +215,13 @@ private function get_queries_by_applied_filters() { } /** - * Return queries that are generated by block attributes. + * Return queries that are generated by attributes * - * @param array $variation_props Dedicated attributes for the variation. + * @param array $parsed_block The Product Query that being rendered. * @return array */ - private function get_queries_by_attributes( $variation_props ) { - $on_sale_enabled = isset( $variation_props['attributes']['query']['onSale'] ) && true === $variation_props['attributes']['query']['onSale']; + private function get_queries_by_attributes( $parsed_block ) { + $on_sale_enabled = isset( $parsed_block['attrs']['query']['__woocommerceOnSale'] ) && true === $parsed_block['attrs']['query']['__woocommerceOnSale']; return array( 'on_sale' => ( $on_sale_enabled ? $this->get_on_sale_products_query() : array() ), ); @@ -267,20 +279,21 @@ function( $acc, $query_args ) { $attribute_name = $query_args['filter']; $attribute_query_type = $query_args['query_type']; - $attribute_name_value = get_query_var( $attribute_name ); - $attribute_query_type_value = get_query_var( $attribute_query_type ); + $attribute_value = get_query_var( $attribute_name ); + $attribute_query = get_query_var( $attribute_query_type ); - if ( empty( $attribute_name_value ) ) { + if ( empty( $attribute_value ) ) { return $acc; } - $attribute_name_value = explode( ',', $attribute_name_value ); + // It is necessary explode the value because $attribute_value can be a string with multiple values (e.g. "red,blue"). + $attribute_value = explode( ',', $attribute_value ); $acc[] = array( - 'taxonomy' => str_replace( AttributeFilter::FILTER_QUERY_VAR, 'pa_', $attribute_name ), + 'taxonomy' => str_replace( AttributeFilter::FILTER_QUERY_VAR_PREFIX, 'pa_', $attribute_name ), 'field' => 'slug', - 'terms' => $attribute_name_value, - 'operator' => 'and' === $attribute_query_type_value ? 'AND' : 'IN', + 'terms' => $attribute_value, + 'operator' => 'and' === $attribute_query ? 'AND' : 'IN', ); return $acc; @@ -302,4 +315,18 @@ function( $acc, $query_args ) { ); } + /** + * Intersect arrays when both are not empty, otherwise merge them. + * + * @param array $array1 Array. + * @param array $array2 Array. + * @return array + */ + private function intersect_arrays_when_not_empty( $array1, $array2 ) { + if ( empty( $array1 ) || empty( $array2 ) ) { + return array_merge( $array1, $array2 ); + } + return array_intersect( $array1, $array2 ); + } + } From 5be22b9c5e6c7a29c0116d9af1622c5e17bf2a0d Mon Sep 17 00:00:00 2001 From: Luigi Date: Thu, 29 Sep 2022 14:32:34 +0200 Subject: [PATCH 08/12] Product Query - Add support for the Filter By Stock Block #6790 Product Query - Add support for the Filter By Stock Block --- src/BlockTypes/ProductQuery.php | 76 +++++++++++++++++++++++++++++---- src/BlockTypes/StockFilter.php | 4 +- 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index c6dd674f98d..edbbe217cd3 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -126,6 +126,7 @@ function( $acc, $query ) { $acc['meta_query'] = isset( $query['meta_query'] ) ? array_merge( $acc['meta_query'], array( $query['meta_query'] ) ) : $acc['meta_query']; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query $acc['tax_query'] = isset( $query['tax_query'] ) ? array_merge( $acc['tax_query'], array( $query['tax_query'] ) ) : $acc['tax_query']; + return $acc; }, $common_query_values @@ -146,13 +147,11 @@ private function get_on_sale_products_query() { } /** - * Set the query vars that are used by filter blocks. + * Return all the query vars that are used by filter blocks. * - * @param array $public_query_vars Public query vars. * @return array */ - public function set_query_vars( $public_query_vars ) { - + private function get_query_vars_from_filter_blocks() { $attributes_filter_query_args = array_reduce( array_values( $this->get_filter_by_attributes_query_vars() ), function( $acc, $array ) { @@ -161,8 +160,30 @@ function( $acc, $array ) { array() ); - $price_filter_query_args = array( PriceFilter::MIN_PRICE_QUERY_VAR, PriceFilter::MAX_PRICE_QUERY_VAR ); - return array_merge( $public_query_vars, $price_filter_query_args, $attributes_filter_query_args ); + return array( + 'price_filter_query_args' => array( PriceFilter::MIN_PRICE_QUERY_VAR, PriceFilter::MAX_PRICE_QUERY_VAR ), + 'stock_filter_query_args' => array( StockFilter::STOCK_STATUS_QUERY_VAR ), + 'attributes_filter_query_args' => $attributes_filter_query_args, + ); + + } + + /** + * Set the query vars that are used by filter blocks. + * + * @param array $public_query_vars Public query vars. + * @return array + */ + public function set_query_vars( $public_query_vars ) { + $query_vars = $this->get_query_vars_from_filter_blocks(); + + return array_reduce( + array_values( $query_vars ), + function( $acc, $query_vars_filter_block ) { + return array_merge( $query_vars_filter_block, $acc ); + }, + $public_query_vars + ); } /** @@ -209,8 +230,9 @@ function( $acc, $attribute ) { */ private function get_queries_by_applied_filters() { return array( - 'price_filter' => $this->get_filter_by_price_query(), - 'attributes_filter' => $this->get_filter_by_attributes_query(), + 'price_filter' => $this->get_filter_by_price_query(), + 'attributes_filter' => $this->get_filter_by_attributes_query(), + 'stock_status_filter' => $this->get_filter_by_stock_status_query(), ); } @@ -315,6 +337,44 @@ function( $acc, $query_args ) { ); } + /** + * Return a query that filters products by stock status. + * + * @return array + */ + private function get_filter_by_stock_status_query() { + + $filter_stock_status_values = get_query_var( StockFilter::STOCK_STATUS_QUERY_VAR ); + + if ( empty( $filter_stock_status_values ) ) { + return array(); + } + + $filtered_stock_status_values = array_filter( + explode( ',', $filter_stock_status_values ), + function( $stock_status ) { + return in_array( $stock_status, StockFilter::STOCK_STATUS_QUERY_VAR_VALUES, true ); + } + ); + + if ( empty( $filtered_stock_status_values ) ) { + return array(); + } + + return array( + // Ignoring the warning of not using meta queries. + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + array( + 'key' => '_stock_status', + 'value' => $filtered_stock_status_values, + 'operator' => 'IN', + + ), + ), + ); + } + /** * Intersect arrays when both are not empty, otherwise merge them. * diff --git a/src/BlockTypes/StockFilter.php b/src/BlockTypes/StockFilter.php index 8fc29e12452..0e6aed8f2f7 100644 --- a/src/BlockTypes/StockFilter.php +++ b/src/BlockTypes/StockFilter.php @@ -10,7 +10,9 @@ class StockFilter extends AbstractBlock { * * @var string */ - protected $block_name = 'stock-filter'; + protected $block_name = 'stock-filter'; + const STOCK_STATUS_QUERY_VAR = 'filter_stock_status'; + const STOCK_STATUS_QUERY_VAR_VALUES = array( 'instock', 'outofstock', 'onbackorder' ); /** * Extra data passed through from server to client for block. From b8bf17efb32caf70c484e45d2a1e90a90c1bec90 Mon Sep 17 00:00:00 2001 From: Luigi Date: Thu, 29 Sep 2022 18:32:26 +0200 Subject: [PATCH 09/12] Fix filter blocks: the data (e.g: max price or stock-status) match the variation #7245 fix filter blocks: the data (e.g: max price or stock-status) match the variation --- .../hooks/collections/use-collection-data.ts | 6 +- assets/js/blocks/attribute-filter/block.tsx | 5 ++ assets/js/blocks/price-filter/block.tsx | 5 ++ assets/js/blocks/stock-filter/block.tsx | 5 ++ src/BlockTypes/ProductQuery.php | 58 ++++++++++++++++--- 5 files changed, 71 insertions(+), 8 deletions(-) diff --git a/assets/js/base/context/hooks/collections/use-collection-data.ts b/assets/js/base/context/hooks/collections/use-collection-data.ts index fdce755b002..97c0ebecec2 100644 --- a/assets/js/base/context/hooks/collections/use-collection-data.ts +++ b/assets/js/base/context/hooks/collections/use-collection-data.ts @@ -3,8 +3,9 @@ */ import { useState, useEffect, useMemo } from '@wordpress/element'; import { useDebounce } from 'use-debounce'; -import { sortBy } from 'lodash'; +import { isEmpty, sortBy } from 'lodash'; import { useShallowEqual } from '@woocommerce/base-hooks'; +import { getSettingWithCoercion } from '@woocommerce/settings'; import { objectHasProp } from '@woocommerce/types'; /** @@ -46,6 +47,7 @@ interface UseCollectionDataProps { queryPrices?: boolean; queryStock?: boolean; queryState: Record< string, unknown >; + productIds?: number[]; } export const useCollectionData = ( { @@ -53,6 +55,7 @@ export const useCollectionData = ( { queryPrices, queryStock, queryState, + productIds, }: UseCollectionDataProps ) => { let context = useQueryStateContext(); context = `${ context }-collection-data`; @@ -145,6 +148,7 @@ export const useCollectionData = ( { per_page: undefined, orderby: undefined, order: undefined, + ...( ! isEmpty( productIds ) && { include: productIds } ), ...collectionDataQueryVars, }, shouldSelect: debouncedShouldSelect, diff --git a/assets/js/blocks/attribute-filter/block.tsx b/assets/js/blocks/attribute-filter/block.tsx index a7b2a13d266..45546f42e5a 100644 --- a/assets/js/blocks/attribute-filter/block.tsx +++ b/assets/js/blocks/attribute-filter/block.tsx @@ -100,6 +100,10 @@ const AttributeFilterBlock = ( { isString ); + const productIds = isEditor + ? [] + : getSettingWithCoercion( 'product_ids', [], Array.isArray ); + const [ hasSetFilterDefaultsFromUrl, setHasSetFilterDefaultsFromUrl ] = useState( false ); @@ -157,6 +161,7 @@ const AttributeFilterBlock = ( { ...queryState, attributes: filterAvailableTerms ? queryState.attributes : null, }, + productIds, } ); /** diff --git a/assets/js/blocks/price-filter/block.tsx b/assets/js/blocks/price-filter/block.tsx index 4e191b55658..3efafaaefe3 100644 --- a/assets/js/blocks/price-filter/block.tsx +++ b/assets/js/blocks/price-filter/block.tsx @@ -97,6 +97,10 @@ const PriceFilterBlock = ( { isBoolean ); + const productIds = isEditor + ? [] + : getSettingWithCoercion( 'product_ids', [], Array.isArray ); + const [ hasSetFilterDefaultsFromUrl, setHasSetFilterDefaultsFromUrl ] = useState( false ); @@ -106,6 +110,7 @@ const PriceFilterBlock = ( { const { results, isLoading } = useCollectionData( { queryPrices: true, queryState, + productIds, } ); const currency = getCurrencyFromPriceResponse( diff --git a/assets/js/blocks/stock-filter/block.tsx b/assets/js/blocks/stock-filter/block.tsx index 869f0f0bf4a..5fcd1e2b446 100644 --- a/assets/js/blocks/stock-filter/block.tsx +++ b/assets/js/blocks/stock-filter/block.tsx @@ -67,6 +67,10 @@ const StockStatusFilterBlock = ( { {} ); + const productIds = isEditor + ? [] + : getSettingWithCoercion( 'product_ids', [], Array.isArray ); + const STOCK_STATUS_OPTIONS = useRef( getSetting( 'hideOutOfStockItems', false ) ? otherStockStatusOptions @@ -98,6 +102,7 @@ const StockStatusFilterBlock = ( { useCollectionData( { queryStock: true, queryState, + productIds, } ); /** diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index edbbe217cd3..b3bd28e19f0 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -74,6 +74,7 @@ public function update_query( $pre_render, $parsed_block ) { // Set this so that our product filters can detect if it's a PHP template. $this->asset_data_registry->add( 'has_filterable_products', true, true ); $this->asset_data_registry->add( 'is_rendering_php_template', true, true ); + $this->asset_data_registry->add( 'product_ids', $this->get_products_ids_by_attributes( $parsed_block ), true ); add_filter( 'query_loop_block_query_vars', array( $this, 'build_query' ), @@ -119,19 +120,62 @@ public function build_query( $query ) { $queries_by_filters ), function( $acc, $query ) { - $acc['post__in'] = isset( $query['post__in'] ) ? $this->intersect_arrays_when_not_empty( $acc['post__in'], $query['post__in'] ) : $acc['post__in']; + return $this->merge_queries( $acc, $query ); + }, + $common_query_values + ); + } + /** + * Return the product ids based on the attributes. + * + * @param array $parsed_block The block being rendered. + * @return array + */ + private function get_products_ids_by_attributes( $parsed_block ) { + $queries_by_attributes = $this->get_queries_by_attributes( $parsed_block ); + + $query = array_reduce( + $queries_by_attributes, + function( $acc, $query ) { + return $this->merge_queries( $acc, $query ); + }, + array( + 'post_type' => 'product', + 'post__in' => array(), + 'post_status' => 'publish', + 'posts_per_page' => -1, // Ignoring the warning of not using meta queries. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - $acc['meta_query'] = isset( $query['meta_query'] ) ? array_merge( $acc['meta_query'], array( $query['meta_query'] ) ) : $acc['meta_query']; + 'meta_query' => array(), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query - $acc['tax_query'] = isset( $query['tax_query'] ) ? array_merge( $acc['tax_query'], array( $query['tax_query'] ) ) : $acc['tax_query']; - - return $acc; - }, - $common_query_values + 'tax_query' => array(), + ) ); + $products = new \WP_Query( $query ); + $post_ids = wp_list_pluck( $products->posts, 'ID' ); + + return $post_ids; + } + + /** + * Merge in the first parameter the keys "post_in", "meta_query" and "tax_query" of the second parameter. + * + * @param array $query1 The first query. + * @param array $query2 The second query. + * @return array + */ + private function merge_queries( $query1, $query2 ) { + $query1['post__in'] = isset( $query2['post__in'] ) ? $this->intersect_arrays_when_not_empty( $query1['post__in'], $query2['post__in'] ) : $query1['post__in']; + // Ignoring the warning of not using meta queries. + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + $query1['meta_query'] = isset( $query2['meta_query'] ) ? array_merge( $query1['meta_query'], array( $query2['meta_query'] ) ) : $query1['meta_query']; + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + $query1['tax_query'] = isset( $query2['tax_query'] ) ? array_merge( $query1['tax_query'], array( $query2['tax_query'] ) ) : $query1['tax_query']; + + return $query1; + } From c4a46c304e4b9494afc6d29c45dc93f272ae5fd5 Mon Sep 17 00:00:00 2001 From: Luigi Date: Thu, 27 Oct 2022 12:09:05 +0200 Subject: [PATCH 10/12] disable phcs rules on top of the file --- src/BlockTypes/ProductQuery.php | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index 52238f0d587..ead5692be57 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -1,6 +1,9 @@ $query['orderby'], 'order' => $query['order'], 'offset' => $query['offset'], - // Ignoring the warning of not using meta queries. - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array(), - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query 'tax_query' => array(), ); @@ -156,10 +156,7 @@ function( $acc, $query ) { 'post__in' => array(), 'post_status' => 'publish', 'posts_per_page' => -1, - // Ignoring the warning of not using meta queries. - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array(), - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query 'tax_query' => array(), ) ); @@ -178,12 +175,9 @@ function( $acc, $query ) { * @return array */ private function merge_queries( $query1, $query2 ) { - $query1['post__in'] = isset( $query2['post__in'] ) ? $this->intersect_arrays_when_not_empty( $query1['post__in'], $query2['post__in'] ) : $query1['post__in']; - // Ignoring the warning of not using meta queries. - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - $query1['meta_query'] = isset( $query2['meta_query'] ) ? array_merge( $query1['meta_query'], array( $query2['meta_query'] ) ) : $query1['meta_query']; - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query - $query1['tax_query'] = isset( $query2['tax_query'] ) ? array_merge( $query1['tax_query'], array( $query2['tax_query'] ) ) : $query1['tax_query']; + $query1['post__in'] = ( isset( $query2['post__in'] ) && ! empty( $query2['post__in'] ) ) ? $this->intersect_arrays_when_not_empty( $query1['post__in'], $query2['post__in'] ) : $query1['post__in']; + $query1['meta_query'] = ( isset( $query2['meta_query'] ) && ! empty( $query2['meta_query'] ) ) ? array_merge( $query1['meta_query'], array( $query2['meta_query'] ) ) : $query1['meta_query']; + $query1['tax_query'] = ( isset( $query2['tax_query'] ) && ! empty( $query2['tax_query'] ) ) ? array_merge( $query1['tax_query'], array( $query2['tax_query'] ) ) : $query1['tax_query']; return $query1; @@ -331,8 +325,6 @@ private function get_filter_by_price_query() { } return array( - // Ignoring the warning of not using meta queries. - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( 'relation' => 'AND', $max_price_query, @@ -382,8 +374,6 @@ function( $acc, $query_args ) { } return array( - // Ignoring the warning of not using meta queries. - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query 'tax_query' => array( 'relation' => 'AND', $queries, @@ -415,8 +405,6 @@ function( $stock_status ) { } return array( - // Ignoring the warning of not using meta queries. - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( array( 'key' => '_stock_status', From e95122d19765a17a988d3bd509fa103b0867c2a8 Mon Sep 17 00:00:00 2001 From: Luigi Date: Thu, 27 Oct 2022 12:11:42 +0200 Subject: [PATCH 11/12] replace parameter name --- src/BlockTypes/ProductQuery.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php index ead5692be57..027e0522785 100644 --- a/src/BlockTypes/ProductQuery.php +++ b/src/BlockTypes/ProductQuery.php @@ -170,17 +170,16 @@ function( $acc, $query ) { /** * Merge in the first parameter the keys "post_in", "meta_query" and "tax_query" of the second parameter. * - * @param array $query1 The first query. - * @param array $query2 The second query. + * @param array $a The first query. + * @param array $b The second query. * @return array */ - private function merge_queries( $query1, $query2 ) { - $query1['post__in'] = ( isset( $query2['post__in'] ) && ! empty( $query2['post__in'] ) ) ? $this->intersect_arrays_when_not_empty( $query1['post__in'], $query2['post__in'] ) : $query1['post__in']; - $query1['meta_query'] = ( isset( $query2['meta_query'] ) && ! empty( $query2['meta_query'] ) ) ? array_merge( $query1['meta_query'], array( $query2['meta_query'] ) ) : $query1['meta_query']; - $query1['tax_query'] = ( isset( $query2['tax_query'] ) && ! empty( $query2['tax_query'] ) ) ? array_merge( $query1['tax_query'], array( $query2['tax_query'] ) ) : $query1['tax_query']; - - return $query1; + private function merge_queries( $a, $b ) { + $a['post__in'] = ( isset( $b['post__in'] ) && ! empty( $b['post__in'] ) ) ? $this->intersect_arrays_when_not_empty( $a['post__in'], $b['post__in'] ) : $a['post__in']; + $a['meta_query'] = ( isset( $b['meta_query'] ) && ! empty( $b['meta_query'] ) ) ? array_merge( $a['meta_query'], array( $b['meta_query'] ) ) : $a['meta_query']; + $a['tax_query'] = ( isset( $b['tax_query'] ) && ! empty( $b['tax_query'] ) ) ? array_merge( $a['tax_query'], array( $b['tax_query'] ) ) : $a['tax_query']; + return $a; } /** From 987d4a40e2d4c79df0200d29d0a519fd48734529 Mon Sep 17 00:00:00 2001 From: Luigi Date: Thu, 27 Oct 2022 12:15:52 +0200 Subject: [PATCH 12/12] fix eslint error --- assets/js/base/context/hooks/collections/use-collection-data.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/assets/js/base/context/hooks/collections/use-collection-data.ts b/assets/js/base/context/hooks/collections/use-collection-data.ts index 2b0ec830a85..49ad266bf2a 100644 --- a/assets/js/base/context/hooks/collections/use-collection-data.ts +++ b/assets/js/base/context/hooks/collections/use-collection-data.ts @@ -5,7 +5,6 @@ import { useState, useEffect, useMemo } from '@wordpress/element'; import { useDebounce } from 'use-debounce'; import { isEmpty, sortBy } from 'lodash'; import { useShallowEqual } from '@woocommerce/base-hooks'; -import { getSettingWithCoercion } from '@woocommerce/settings'; import { objectHasProp } from '@woocommerce/types'; /**