From 2099008a72a6bfac73ce79c16c7b2c4ededdc373 Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Fri, 30 Aug 2024 16:15:17 -0700
Subject: [PATCH 01/26] Fixed PHP 7.4 Compatibility
The splat operator does not support associative arrays until PHP 8.1.
---
.../Blocks/BlockTypes/ProductCollection.php | 18 ++++++++++--------
1 file changed, 10 insertions(+), 8 deletions(-)
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
index f2f9f72328a..09fe61976fa 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
@@ -304,14 +304,16 @@ class ProductCollection extends AbstractBlock {
$p->set_attribute(
'data-wc-context',
wp_json_encode(
- array(
- ...$current_context,
- // The message to be announced by the screen reader when the page is loading or loaded.
- 'accessibilityLoadingMessage' => __( 'Loading page, please wait.', 'woocommerce' ),
- 'accessibilityLoadedMessage' => __( 'Page Loaded.', 'woocommerce' ),
- // We don't prefetch the links if user haven't clicked on pagination links yet.
- // This way we avoid prefetching when the page loads.
- 'isPrefetchNextOrPreviousLink' => false,
+ array_merge(
+ $current_context,
+ array(
+ // The message to be announced by the screen reader when the page is loading or loaded.
+ 'accessibilityLoadingMessage' => __( 'Loading page, please wait.', 'woocommerce' ),
+ 'accessibilityLoadedMessage' => __( 'Page Loaded.', 'woocommerce' ),
+ // We don't prefetch the links if user haven't clicked on pagination links yet.
+ // This way we avoid prefetching when the page loads.
+ 'isPrefetchNextOrPreviousLink' => false,
+ ),
),
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
)
From 674475eaaec22f47bbba996f4d70cf007845046a Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Fri, 30 Aug 2024 16:37:03 -0700
Subject: [PATCH 02/26] Added Related Products Collection Boilerplate
---
.../product-collection/collections/index.tsx | 2 +
.../collections/related.tsx | 64 +++++++++++++++++++
.../js/blocks/product-collection/types.ts | 1 +
3 files changed, 67 insertions(+)
create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related.tsx
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/index.tsx
index 869b7275469..2a24183d705 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/index.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/index.tsx
@@ -20,6 +20,7 @@ import topRated from './top-rated';
import bestSellers from './best-sellers';
import onSale from './on-sale';
import featured from './featured';
+import related from './related';
const collections: BlockVariation[] = [
productCollection,
@@ -28,6 +29,7 @@ const collections: BlockVariation[] = [
onSale,
bestSellers,
newArrivals,
+ related,
];
export const registerCollections = () => {
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related.tsx
new file mode 100644
index 00000000000..114e7e115c2
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related.tsx
@@ -0,0 +1,64 @@
+/**
+ * External dependencies
+ */
+import type { InnerBlockTemplate } from '@wordpress/blocks';
+import { __ } from '@wordpress/i18n';
+import { Icon, loop } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import { INNER_BLOCKS_PRODUCT_TEMPLATE } from '../constants';
+import { CoreCollectionNames, CoreFilterNames, LayoutOptions } from '../types';
+
+const collection = {
+ name: CoreCollectionNames.RELATED,
+ title: __( 'Related Products', 'woocommerce' ),
+ icon: ,
+ description: __( 'Recommend products like this one.', 'woocommerce' ),
+ keywords: [ 'related', 'product collection' ],
+ scope: [],
+ preview: {
+ initialPreviewState: {
+ isPreview: true,
+ previewMessage: __(
+ 'Actual products will vary depending on the product being viewed.',
+ 'woocommerce'
+ ),
+ },
+ },
+};
+
+const attributes = {
+ displayLayout: {
+ type: LayoutOptions.GRID,
+ columns: 4,
+ shrinkColumns: true,
+ },
+ query: {
+ perPage: 4,
+ pages: 1,
+ },
+ hideControls: [ CoreFilterNames.FILTERABLE ],
+};
+
+const heading: InnerBlockTemplate = [
+ 'core/heading',
+ {
+ textAlign: 'center',
+ level: 2,
+ content: __( 'You may also like', 'woocommerce' ),
+ style: { spacing: { margin: { bottom: '1rem' } } },
+ },
+];
+
+const innerBlocks: InnerBlockTemplate[] = [
+ heading,
+ INNER_BLOCKS_PRODUCT_TEMPLATE,
+];
+
+export default {
+ ...collection,
+ attributes,
+ innerBlocks,
+};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts
index 4407c682abe..9e67c249445 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts
@@ -140,6 +140,7 @@ export enum CoreCollectionNames {
NEW_ARRIVALS = 'woocommerce/product-collection/new-arrivals',
ON_SALE = 'woocommerce/product-collection/on-sale',
TOP_RATED = 'woocommerce/product-collection/top-rated',
+ RELATED = 'woocommerce/product-collection/related',
}
export enum CoreFilterNames {
From c5e783606a1dc2e2ea4f261568316d8657d4b827 Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Sat, 31 Aug 2024 11:23:30 -0700
Subject: [PATCH 03/26] Added `woocommerceRelatedTo` Collection Parameter
This parameter allows the block to query for products that are related
to the given product IDs.
---
.../register-product-collection.tsx | 12 ++++
.../product-collection/collections/index.tsx | 4 +-
.../{related.tsx => related-to.tsx} | 2 +-
.../js/blocks/product-collection/constants.ts | 3 +
.../js/blocks/product-collection/types.ts | 6 +-
...s-wc-product-collection-block-tracking.php | 5 ++
.../Blocks/BlockTypes/ProductCollection.php | 71 ++++++++++++++-----
.../Blocks/BlockTypes/ProductCollection.php | 63 ++++++++++++++++
8 files changed, 144 insertions(+), 22 deletions(-)
rename plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/{related.tsx => related-to.tsx} (97%)
diff --git a/plugins/woocommerce-blocks/assets/js/blocks-registry/product-collection/register-product-collection.tsx b/plugins/woocommerce-blocks/assets/js/blocks-registry/product-collection/register-product-collection.tsx
index 516fb64e48d..05b15355c84 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks-registry/product-collection/register-product-collection.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks-registry/product-collection/register-product-collection.tsx
@@ -222,6 +222,15 @@ const isValidCollectionConfig = ( config: ProductCollectionConfig ) => {
'Invalid woocommerceHandPickedProducts: woocommerceHandPickedProducts must be an array.'
);
}
+ // attributes.query.woocommerceRelatedTo
+ if (
+ config.attributes?.query?.woocommerceRelatedTo !== undefined &&
+ ! Array.isArray( config.attributes.query.woocommerceRelatedTo )
+ ) {
+ console.warn(
+ 'Invalid woocommerceRelatedTo: woocommerceRelatedTo must be an array.'
+ );
+ }
// attributes.query.priceRange
if (
config.attributes?.query?.priceRange !== undefined &&
@@ -402,6 +411,9 @@ export const __experimentalRegisterProductCollection = (
woocommerceHandPickedProducts:
query.woocommerceHandPickedProducts,
} ),
+ ...( query.woocommerceRelatedTo !== undefined && {
+ woocommerceRelatedTo: query.woocommerceRelatedTo,
+ } ),
...( query.priceRange !== undefined && {
priceRange: query.priceRange,
} ),
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/index.tsx
index 2a24183d705..708d2b2224b 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/index.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/index.tsx
@@ -20,7 +20,7 @@ import topRated from './top-rated';
import bestSellers from './best-sellers';
import onSale from './on-sale';
import featured from './featured';
-import related from './related';
+import relatedTo from './related-to';
const collections: BlockVariation[] = [
productCollection,
@@ -29,7 +29,7 @@ const collections: BlockVariation[] = [
onSale,
bestSellers,
newArrivals,
- related,
+ relatedTo,
];
export const registerCollections = () => {
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related-to.tsx
similarity index 97%
rename from plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related.tsx
rename to plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related-to.tsx
index 114e7e115c2..a8ca8022023 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related-to.tsx
@@ -12,7 +12,7 @@ import { INNER_BLOCKS_PRODUCT_TEMPLATE } from '../constants';
import { CoreCollectionNames, CoreFilterNames, LayoutOptions } from '../types';
const collection = {
- name: CoreCollectionNames.RELATED,
+ name: CoreCollectionNames.RELATED_TO,
title: __( 'Related Products', 'woocommerce' ),
icon: ,
description: __( 'Recommend products like this one.', 'woocommerce' ),
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/constants.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/constants.ts
index 98a1d9be2f6..96d34f5b949 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/constants.ts
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/constants.ts
@@ -60,6 +60,7 @@ export const DEFAULT_QUERY: ProductCollectionQuery = {
woocommerceStockStatus: getDefaultStockStatuses(),
woocommerceAttributes: [],
woocommerceHandPickedProducts: [],
+ woocommerceRelatedTo: [],
timeFrame: undefined,
priceRange: undefined,
filterable: false,
@@ -90,6 +91,7 @@ export const DEFAULT_FILTERS: Pick<
| 'woocommerceStockStatus'
| 'woocommerceAttributes'
| 'woocommerceHandPickedProducts'
+ | 'woocommerceRelatedTo'
| 'taxQuery'
| 'featured'
| 'timeFrame'
@@ -99,6 +101,7 @@ export const DEFAULT_FILTERS: Pick<
woocommerceStockStatus: DEFAULT_QUERY.woocommerceStockStatus,
woocommerceAttributes: DEFAULT_QUERY.woocommerceAttributes,
woocommerceHandPickedProducts: DEFAULT_QUERY.woocommerceHandPickedProducts,
+ woocommerceRelatedTo: DEFAULT_QUERY.woocommerceRelatedTo,
taxQuery: DEFAULT_QUERY.taxQuery,
featured: DEFAULT_QUERY.featured,
timeFrame: DEFAULT_QUERY.timeFrame,
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts
index 9e67c249445..22f9af2b8d7 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts
@@ -93,6 +93,10 @@ export interface ProductCollectionQuery {
woocommerceAttributes: AttributeMetadata[];
isProductCollectionBlock: boolean;
woocommerceHandPickedProducts: string[];
+ /**
+ * Filter for products related to the given list of product IDs.
+ */
+ woocommerceRelatedTo: string[];
priceRange: undefined | PriceRange;
filterable: boolean;
}
@@ -140,7 +144,7 @@ export enum CoreCollectionNames {
NEW_ARRIVALS = 'woocommerce/product-collection/new-arrivals',
ON_SALE = 'woocommerce/product-collection/on-sale',
TOP_RATED = 'woocommerce/product-collection/top-rated',
- RELATED = 'woocommerce/product-collection/related',
+ RELATED_TO = 'woocommerce/product-collection/related',
}
export enum CoreFilterNames {
diff --git a/plugins/woocommerce/includes/tracks/events/class-wc-product-collection-block-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-product-collection-block-tracking.php
index 092be4f81ee..948c20175ae 100644
--- a/plugins/woocommerce/includes/tracks/events/class-wc-product-collection-block-tracking.php
+++ b/plugins/woocommerce/includes/tracks/events/class-wc-product-collection-block-tracking.php
@@ -247,6 +247,7 @@ class WC_Product_Collection_Block_Tracking {
$filters = array(
'inherit' => 'no',
'order-by' => 'no',
+ 'related-to' => 'no',
'on-sale' => 'no',
'stock-status' => 'no',
'handpicked' => 'no',
@@ -303,6 +304,10 @@ class WC_Product_Collection_Block_Tracking {
$filters['handpicked'] = 'yes';
}
+ if ( ! empty( $query_attrs['woocommerceRelatedTo'] ) ) {
+ $filters['related-to'] = 'yes';
+ }
+
if ( ! empty( $query_attrs['search'] ) ) {
$filters['keyword'] = 'yes';
}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
index 09fe61976fa..fdea9df3cd1 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
@@ -609,6 +609,7 @@ class ProductCollection extends AbstractBlock {
$stock_status = $request->get_param( 'woocommerceStockStatus' );
$product_attributes = $request->get_param( 'woocommerceAttributes' );
$handpicked_products = $request->get_param( 'woocommerceHandPickedProducts' );
+ $related_to = $request->get_param( 'woocommerceRelatedTo' );
$featured = $request->get_param( 'featured' );
$time_frame = $request->get_param( 'timeFrame' );
$price_range = $request->get_param( 'priceRange' );
@@ -624,6 +625,7 @@ class ProductCollection extends AbstractBlock {
'stock_status' => $stock_status,
'product_attributes' => $product_attributes,
'handpicked_products' => $handpicked_products,
+ 'related_to' => $related_to,
'featured' => $featured,
'timeFrame' => $time_frame,
'priceRange' => $price_range,
@@ -749,24 +751,35 @@ class ProductCollection extends AbstractBlock {
* @param bool $is_exclude_applied_filters Whether to exclude the applied filters or not.
*/
private function get_final_query_args( $common_query_values, $query, $is_exclude_applied_filters = false ) {
- $handpicked_products = $query['handpicked_products'] ?? array();
- $orderby_query = $query['orderby'] ? $this->get_custom_orderby_query( $query['orderby'] ) : array();
- $on_sale_query = $this->get_on_sale_products_query( $query['on_sale'] );
- $stock_query = $this->get_stock_status_query( $query['stock_status'] );
- $visibility_query = is_array( $query['stock_status'] ) ? $this->get_product_visibility_query( $stock_query, $query['stock_status'] ) : array();
- $featured_query = $this->get_featured_query( $query['featured'] ?? false );
- $attributes_query = $this->get_product_attributes_query( $query['product_attributes'] );
- $taxonomies_query = $query['taxonomies_query'] ?? array();
- $tax_query = $this->merge_tax_queries( $visibility_query, $attributes_query, $taxonomies_query, $featured_query );
- $date_query = $this->get_date_query( $query['timeFrame'] ?? array() );
- $price_query_args = $this->get_price_range_query_args( $query['priceRange'] ?? array() );
+ $orderby_query = $query['orderby'] ? $this->get_custom_orderby_query( $query['orderby'] ) : array();
+ $on_sale_query = $this->get_on_sale_products_query( $query['on_sale'] );
+ $stock_query = $this->get_stock_status_query( $query['stock_status'] );
+ $visibility_query = is_array( $query['stock_status'] ) ? $this->get_product_visibility_query( $stock_query, $query['stock_status'] ) : array();
+ $featured_query = $this->get_featured_query( $query['featured'] ?? false );
+ $attributes_query = $this->get_product_attributes_query( $query['product_attributes'] );
+ $taxonomies_query = $query['taxonomies_query'] ?? array();
+ $tax_query = $this->merge_tax_queries( $visibility_query, $attributes_query, $taxonomies_query, $featured_query );
+ $date_query = $this->get_date_query( $query['timeFrame'] ?? array() );
+ $price_query_args = $this->get_price_range_query_args( $query['priceRange'] ?? array() );
// We exclude applied filters to generate product ids for the filter blocks.
$applied_filters_query = $is_exclude_applied_filters ? array() : $this->get_queries_by_applied_filters();
- $merged_query = $this->merge_queries( $common_query_values, $orderby_query, $on_sale_query, $stock_query, $tax_query, $applied_filters_query, $date_query, $price_query_args );
+ $merged_query = $this->merge_queries(
+ $common_query_values,
+ $orderby_query,
+ $on_sale_query,
+ $stock_query,
+ $tax_query,
+ $applied_filters_query,
+ $date_query,
+ $price_query_args
+ );
- $result = $this->filter_query_to_only_include_ids( $merged_query, $handpicked_products );
+ $handpicked_products = $query['handpicked_products'] ?? array();
+ $related_to = $this->get_related_to_query( $query['related_to'] );
+
+ $result = $this->filter_query_to_only_include_ids( $merged_query, $handpicked_products, $related_to );
return $result;
}
@@ -1077,6 +1090,20 @@ class ProductCollection extends AbstractBlock {
);
}
+ /**
+ * Returns a query for filtering products that are related to the ones given.
+ *
+ * @param array $related_to The IDs pf products that we're looking for a relationship to.
+ * @return array The query for related products.
+ */
+ private function get_related_to_query( $related_to ) {
+ if ( empty( $related_to ) ) {
+ return array();
+ }
+
+ return array();
+ }
+
/**
* Generates a tax query to filter products based on their "featured" status.
* If the `$featured` parameter is true, the function will return a tax query
@@ -1209,16 +1236,24 @@ class ProductCollection extends AbstractBlock {
* Apply the query only to a subset of products
*
* @param array $query The query.
- * @param array $ids Array of selected product ids.
+ * @param array ...$ids The product IDs to filter.
*
* @return array
*/
- private function filter_query_to_only_include_ids( $query, $ids ) {
- if ( ! empty( $ids ) ) {
- $query['post__in'] = empty( $query['post__in'] ) ?
- $ids : array_intersect( $ids, $query['post__in'] );
+ private function filter_query_to_only_include_ids( $query, ...$ids ) {
+ if ( empty( $ids ) ) {
+ return $query;
}
+ // Since this is an exclusive filter, we will only show products that are present in all of the ID filters.
+ $post_in_filter = ! empty( $query['post__in'] ) ? $query['post__in'] : array();
+ foreach ( $ids as $i ) {
+ if ( is_array( $i ) && ! empty( $i ) ) {
+ $post_in_filter = array_intersect( $i, $post_in_filter );
+ }
+ }
+ $query['post__in'] = $post_in_filter;
+
return $query;
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
index aba17ef74c3..6e2cf07d10f 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
@@ -90,6 +90,7 @@ class ProductCollection extends \WP_UnitTestCase {
'woocommerceOnSale' => false,
'woocommerceAttributes' => array(),
'woocommerceStockStatus' => array(),
+ 'woocommerceRelatedTo' => array(),
'timeFrame' => array(),
'priceRange' => array(),
)
@@ -678,6 +679,7 @@ class ProductCollection extends \WP_UnitTestCase {
),
),
'woocommerceStockStatus' => array( 'instock', 'outofstock' ),
+ 'woocommerceRelatedTo' => array(),
'timeFrame' => array(
'operator' => 'in',
'value' => $time_frame_date,
@@ -937,4 +939,65 @@ class ProductCollection extends \WP_UnitTestCase {
$this->assertStringContainsString( "( wc_product_meta_lookup.tax_class = 'collection-test' AND wc_product_meta_lookup.`min_price` >= 1.", $query->request );
$this->assertStringContainsString( "( wc_product_meta_lookup.tax_class = 'collection-test' AND wc_product_meta_lookup.`max_price` <= 2.", $query->request );
}
+
+ /**
+ * Test handpicked products queries.
+ */
+ public function test_handpicked_producs_queries() {
+ $handpicked_product_ids = array( 1, 2, 3, 4 );
+
+ $parsed_block = $this->get_base_parsed_block();
+ $parsed_block['attrs']['query']['woocommerceHandPickedProducts'] = $handpicked_product_ids;
+
+ $merged_query = $this->initialize_merged_query( $parsed_block );
+
+ foreach ( $handpicked_product_ids as $id ) {
+ $this->assertContainsEquals( $id, $merged_query['post__in'] );
+ }
+
+ $this->assertCount( count( $handpicked_product_ids ), $merged_query['post__in'] );
+ }
+
+ /**
+ * Test related to queries.
+ */
+ public function test_related_to_queries() {
+ $related_to_product_ids = array( 1, 2, 3, 4 );
+
+ $parsed_block = $this->get_base_parsed_block();
+ $parsed_block['attrs']['query']['woocommerceRelatedTo'] = array();
+
+ $merged_query = $this->initialize_merged_query( $parsed_block );
+
+ foreach ( $related_to_product_ids as $id ) {
+ $this->assertContainsEquals( $id, $merged_query['post__in'] );
+ }
+
+ $this->assertCount( 4, $merged_query['post__in'] );
+ }
+
+ /**
+ * Test merging exclusive id filters.
+ */
+ public function test_merges_exclusive_id_filters() {
+ $handpicked_product_ids = array( 3, 4, 5, 6 );
+ $related_to_product_ids = array( 1, 2, 3, 4 );
+
+ $parsed_block = $this->get_base_parsed_block();
+ $parsed_block['attrs']['query']['woocommerceHandPickedProducts'] = $handpicked_product_ids;
+ $parsed_block['attrs']['query']['woocommerceRelatedTo'] = array();
+
+ $merged_query = $this->initialize_merged_query( $parsed_block );
+
+ // Since these are exclusive filters, we expect the insersection of the ID filters.
+ $expected_product_ids = array_intersect(
+ $handpicked_product_ids,
+ $related_to_product_ids
+ );
+ foreach ( $expected_product_ids as $id ) {
+ $this->assertContainsEquals( $id, $merged_query['post__in'] );
+ }
+
+ $this->assertCount( count( $expected_product_ids ), $merged_query['post__in'] );
+ }
}
From 40b167fb419befba416f1b850ff3f36530295ef4 Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Sat, 31 Aug 2024 13:03:07 -0700
Subject: [PATCH 04/26] Added Collection Related Product Filter
Using `woocommerceRelatedTo`, queries may restrict the products returned
to those that are related to the given product ID(s).
---
.../Blocks/BlockTypes/ProductCollection.php | 63 ++++++++++++++-----
.../Blocks/BlockTypes/ProductCollection.php | 46 ++++++++++----
2 files changed, 80 insertions(+), 29 deletions(-)
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
index fdea9df3cd1..e6c1873f2d0 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
@@ -699,8 +699,9 @@ class ProductCollection extends AbstractBlock {
* @param bool $is_exclude_applied_filters Whether to exclude the applied filters or not.
*/
private function get_final_frontend_query( $query, $page = 1, $is_exclude_applied_filters = false ) {
- $offset = $query['offset'] ?? 0;
- $per_page = $query['perPage'] ?? 9;
+ $product_ids = $query['post__in'] ?? array();
+ $offset = $query['offset'] ?? 0;
+ $per_page = $query['perPage'] ?? 9;
$common_query_values = array(
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
@@ -708,7 +709,7 @@ class ProductCollection extends AbstractBlock {
'posts_per_page' => $query['perPage'],
'order' => $query['order'],
'offset' => ( $per_page * ( $page - 1 ) ) + $offset,
- 'post__in' => array(),
+ 'post__in' => $product_ids,
'post_status' => 'publish',
'post_type' => 'product',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
@@ -721,6 +722,7 @@ class ProductCollection extends AbstractBlock {
$product_attributes = $query['woocommerceAttributes'] ?? array();
$taxonomies_query = $this->get_filter_by_taxonomies_query( $query['tax_query'] ?? array() );
$handpicked_products = $query['woocommerceHandPickedProducts'] ?? array();
+ $related_to = $query['woocommerceRelatedTo'] ?? array();
$time_frame = $query['timeFrame'] ?? null;
$price_range = $query['priceRange'] ?? null;
@@ -733,6 +735,7 @@ class ProductCollection extends AbstractBlock {
'product_attributes' => $product_attributes,
'taxonomies_query' => $taxonomies_query,
'handpicked_products' => $handpicked_products,
+ 'related_to' => $related_to,
'featured' => $query['featured'] ?? false,
'timeFrame' => $time_frame,
'priceRange' => $price_range,
@@ -777,9 +780,9 @@ class ProductCollection extends AbstractBlock {
);
$handpicked_products = $query['handpicked_products'] ?? array();
- $related_to = $this->get_related_to_query( $query['related_to'] );
+ $related_products = $this->get_related_products( $query['related_to'], $common_query_values['posts_per_page'] );
- $result = $this->filter_query_to_only_include_ids( $merged_query, $handpicked_products, $related_to );
+ $result = $this->filter_query_to_only_include_ids( $merged_query, $handpicked_products, $related_products );
return $result;
}
@@ -1093,15 +1096,24 @@ class ProductCollection extends AbstractBlock {
/**
* Returns a query for filtering products that are related to the ones given.
*
- * @param array $related_to The IDs pf products that we're looking for a relationship to.
- * @return array The query for related products.
+ * @param array $product_ids The IDs pf products that we're looking for a relationship to.
+ * @param int $per_page The number of related products to fetch.
+ * @return array The IDs of the related products.
*/
- private function get_related_to_query( $related_to ) {
- if ( empty( $related_to ) ) {
+ private function get_related_products( $product_ids, $per_page ) {
+ if ( empty( $product_ids ) ) {
return array();
}
- return array();
+ $related_product_ids = array();
+ foreach ( $product_ids as $id ) {
+ $related_product_ids = array_merge(
+ $related_product_ids,
+ wc_get_related_products( $id, $per_page )
+ );
+ }
+
+ return $related_product_ids;
}
/**
@@ -1245,14 +1257,33 @@ class ProductCollection extends AbstractBlock {
return $query;
}
- // Since this is an exclusive filter, we will only show products that are present in all of the ID filters.
- $post_in_filter = ! empty( $query['post__in'] ) ? $query['post__in'] : array();
- foreach ( $ids as $i ) {
- if ( is_array( $i ) && ! empty( $i ) ) {
- $post_in_filter = array_intersect( $i, $post_in_filter );
+ // Since we're using array_intersect, any array that is empty will result
+ // in an empty array. To avoid this, we need to make sure every
+ // argument is a non-empty array.
+ $i = 0;
+ $len = count( $ids );
+ $post_in_filter = null;
+
+ // Make sure to filter any product IDs that have already been set in the query too.
+ if ( ! empty( $query['post__in'] ) ) {
+ $post_in_filter = $query['post__in'];
+ } else {
+ // Find the first non-empty array to serve as the base for the intersection.
+ while ( empty( $post_in_filter ) && $i < $len ) {
+ $post_in_filter = $ids[ $i ];
+ ++$i;
}
}
- $query['post__in'] = $post_in_filter;
+
+ for ( ; $i < $len; ++$i ) {
+ $arr = $ids[ $i ];
+ if ( is_array( $arr ) && ! empty( $arr ) ) {
+ $post_in_filter = array_intersect( $arr, $post_in_filter );
+ }
+ }
+
+ // Clean up the output since there will be duplicates and possibly some weird keys.
+ $query['post__in'] = array_values( array_unique( $post_in_filter, SORT_NUMERIC ) );
return $query;
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
index 6e2cf07d10f..5add2f42bf7 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
@@ -943,7 +943,7 @@ class ProductCollection extends \WP_UnitTestCase {
/**
* Test handpicked products queries.
*/
- public function test_handpicked_producs_queries() {
+ public function test_handpicked_products_queries() {
$handpicked_product_ids = array( 1, 2, 3, 4 );
$parsed_block = $this->get_base_parsed_block();
@@ -955,49 +955,69 @@ class ProductCollection extends \WP_UnitTestCase {
$this->assertContainsEquals( $id, $merged_query['post__in'] );
}
- $this->assertCount( count( $handpicked_product_ids ), $merged_query['post__in'] );
+ $this->assertCount( 4, $merged_query['post__in'] );
}
/**
* Test related to queries.
*/
public function test_related_to_queries() {
- $related_to_product_ids = array( 1, 2, 3, 4 );
+ $related_to_product_ids = array( 1, 2, 3 );
+ // This filter will turn off the data store so we don't need dummy products.
+ add_filter( 'woocommerce_product_related_posts_force_display', '__return_true', 0 );
+ $related_filter = function ( $related_posts, $product_id ) use ( $related_to_product_ids ) {
+ $this->assertEquals( 4, $product_id );
+ return $related_to_product_ids;
+ };
+ add_filter( 'woocommerce_related_products', $related_filter, 10, 2 );
$parsed_block = $this->get_base_parsed_block();
- $parsed_block['attrs']['query']['woocommerceRelatedTo'] = array();
+ $parsed_block['attrs']['query']['woocommerceRelatedTo'] = array( 4 );
$merged_query = $this->initialize_merged_query( $parsed_block );
+ remove_filter( 'woocommerce_product_related_posts_force_display', '__return_true', 0 );
+ remove_filter( 'woocommerce_related_products', $related_filter );
+
foreach ( $related_to_product_ids as $id ) {
$this->assertContainsEquals( $id, $merged_query['post__in'] );
}
- $this->assertCount( 4, $merged_query['post__in'] );
+ $this->assertCount( 3, $merged_query['post__in'] );
}
/**
* Test merging exclusive id filters.
*/
public function test_merges_exclusive_id_filters() {
+ $existing_id_filter = array( 1, 4 );
$handpicked_product_ids = array( 3, 4, 5, 6 );
$related_to_product_ids = array( 1, 2, 3, 4 );
+ // The only ID present in ALL of the exclusive filters is 4.
+ $expected_product_ids = array( 4 );
- $parsed_block = $this->get_base_parsed_block();
+ // This filter will turn off the data store so we don't need dummy products.
+ add_filter( 'woocommerce_product_related_posts_force_display', '__return_true', 0 );
+ $related_filter = function ( $related_posts, $product_id ) use ( $related_to_product_ids ) {
+ $this->assertEquals( 10, $product_id );
+ return $related_to_product_ids;
+ };
+ add_filter( 'woocommerce_related_products', $related_filter, 10, 2 );
+
+ $parsed_block = $this->get_base_parsed_block();
+ $parsed_block['attrs']['query']['post__in'] = $existing_id_filter;
$parsed_block['attrs']['query']['woocommerceHandPickedProducts'] = $handpicked_product_ids;
- $parsed_block['attrs']['query']['woocommerceRelatedTo'] = array();
+ $parsed_block['attrs']['query']['woocommerceRelatedTo'] = array( 10 );
$merged_query = $this->initialize_merged_query( $parsed_block );
- // Since these are exclusive filters, we expect the insersection of the ID filters.
- $expected_product_ids = array_intersect(
- $handpicked_product_ids,
- $related_to_product_ids
- );
+ remove_filter( 'woocommerce_product_related_posts_force_display', '__return_true', 0 );
+ remove_filter( 'woocommerce_related_products', $related_filter );
+
foreach ( $expected_product_ids as $id ) {
$this->assertContainsEquals( $id, $merged_query['post__in'] );
}
- $this->assertCount( count( $expected_product_ids ), $merged_query['post__in'] );
+ $this->assertCount( 1, $merged_query['post__in'] );
}
}
From 2dff8ec5746e13db58e39e42f26f09c7e62eb0b7 Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Sat, 31 Aug 2024 14:28:32 -0700
Subject: [PATCH 05/26] Removed Unnecessary Tracking
---
.../events/class-wc-product-collection-block-tracking.php | 5 -----
1 file changed, 5 deletions(-)
diff --git a/plugins/woocommerce/includes/tracks/events/class-wc-product-collection-block-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-product-collection-block-tracking.php
index 948c20175ae..092be4f81ee 100644
--- a/plugins/woocommerce/includes/tracks/events/class-wc-product-collection-block-tracking.php
+++ b/plugins/woocommerce/includes/tracks/events/class-wc-product-collection-block-tracking.php
@@ -247,7 +247,6 @@ class WC_Product_Collection_Block_Tracking {
$filters = array(
'inherit' => 'no',
'order-by' => 'no',
- 'related-to' => 'no',
'on-sale' => 'no',
'stock-status' => 'no',
'handpicked' => 'no',
@@ -304,10 +303,6 @@ class WC_Product_Collection_Block_Tracking {
$filters['handpicked'] = 'yes';
}
- if ( ! empty( $query_attrs['woocommerceRelatedTo'] ) ) {
- $filters['related-to'] = 'yes';
- }
-
if ( ! empty( $query_attrs['search'] ) ) {
$filters['keyword'] = 'yes';
}
From 5f370b02aa48cb6953f36252884fae446f3d7ebc Mon Sep 17 00:00:00 2001
From: github-actions
Date: Sat, 31 Aug 2024 21:40:47 +0000
Subject: [PATCH 06/26] Add changefile(s) from automation for the following
project(s): woocommerce-blocks, woocommerce
---
.../changelog/51076-add-49335-related-products-collection | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 plugins/woocommerce/changelog/51076-add-49335-related-products-collection
diff --git a/plugins/woocommerce/changelog/51076-add-49335-related-products-collection b/plugins/woocommerce/changelog/51076-add-49335-related-products-collection
new file mode 100644
index 00000000000..16e94f62a5c
--- /dev/null
+++ b/plugins/woocommerce/changelog/51076-add-49335-related-products-collection
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+New "Related Products" Product Collection type.
\ No newline at end of file
From eed87239166afa91045fc38f292a73bef3697359 Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Sat, 31 Aug 2024 14:56:29 -0700
Subject: [PATCH 07/26] Removing Misunderstood Option
---
.../js/blocks/product-collection/collections/related-to.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related-to.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related-to.tsx
index a8ca8022023..3c38767c81d 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related-to.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related-to.tsx
@@ -39,7 +39,6 @@ const attributes = {
perPage: 4,
pages: 1,
},
- hideControls: [ CoreFilterNames.FILTERABLE ],
};
const heading: InnerBlockTemplate = [
From 0936af5529bf3fe82ac619ee01632c4cdb98c54d Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Sat, 31 Aug 2024 15:14:16 -0700
Subject: [PATCH 08/26] Linting Fix
---
.../js/blocks/product-collection/collections/related-to.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related-to.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related-to.tsx
index 3c38767c81d..aecf3aff62b 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related-to.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related-to.tsx
@@ -9,7 +9,7 @@ import { Icon, loop } from '@wordpress/icons';
* Internal dependencies
*/
import { INNER_BLOCKS_PRODUCT_TEMPLATE } from '../constants';
-import { CoreCollectionNames, CoreFilterNames, LayoutOptions } from '../types';
+import { CoreCollectionNames, LayoutOptions } from '../types';
const collection = {
name: CoreCollectionNames.RELATED_TO,
From 71ed41c2736ea3fefea00609c4bc66d05a5ae06e Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Sat, 31 Aug 2024 15:32:06 -0700
Subject: [PATCH 09/26] Fixed Test
---
.../src/Blocks/BlockTypes/ProductCollection.php | 2 +-
.../tests/php/src/Blocks/BlockTypes/ProductCollection.php | 8 ++++++--
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
index e6c1873f2d0..fa91cd0b82b 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
@@ -706,7 +706,7 @@ class ProductCollection extends AbstractBlock {
$common_query_values = array(
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(),
- 'posts_per_page' => $query['perPage'],
+ 'posts_per_page' => $per_page,
'order' => $query['order'],
'offset' => ( $per_page * ( $page - 1 ) ) + $offset,
'post__in' => $product_ids,
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
index 5add2f42bf7..df08c01476a 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
@@ -633,7 +633,9 @@ class ProductCollection extends \WP_UnitTestCase {
$product_visibility_terms = wc_get_product_visibility_term_ids();
$product_visibility_not_in = array( is_search() ? $product_visibility_terms['exclude-from-search'] : $product_visibility_terms['exclude-from-catalog'] );
- $args = array();
+ $args = array(
+ 'posts_per_page' => 9,
+ );
$request = $this->build_request();
$updated_query = $this->block_instance->update_rest_query_in_editor( $args, $request );
@@ -667,7 +669,9 @@ class ProductCollection extends \WP_UnitTestCase {
$product_visibility_terms = wc_get_product_visibility_term_ids();
$product_visibility_not_in = array( is_search() ? $product_visibility_terms['exclude-from-search'] : $product_visibility_terms['exclude-from-catalog'] );
- $args = array();
+ $args = array(
+ 'posts_per_page' => 9,
+ );
$time_frame_date = gmdate( 'Y-m-d H:i:s' );
$params = array(
'featured' => 'true',
From 7aeb4052d208ffcd3ae810a466692087600e4a7f Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Thu, 5 Sep 2024 09:27:09 -0700
Subject: [PATCH 10/26] Removed Unnecessary Keyword
---
.../js/blocks/product-collection/collections/related-to.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related-to.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related-to.tsx
index aecf3aff62b..62c6b4bfcf0 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related-to.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related-to.tsx
@@ -16,7 +16,7 @@ const collection = {
title: __( 'Related Products', 'woocommerce' ),
icon: ,
description: __( 'Recommend products like this one.', 'woocommerce' ),
- keywords: [ 'related', 'product collection' ],
+ keywords: [ 'product collection' ],
scope: [],
preview: {
initialPreviewState: {
From 41c837202ebad27031b82f4227cba0b307bc7905 Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Thu, 5 Sep 2024 10:28:30 -0700
Subject: [PATCH 11/26] Support Unlimited `wc_get_related_products()`
This changes related product fetching so that `-1` will return all
related products. I also removed a second +10 offset that seems
to have been accidentally added eight years ago.
---
.../class-wc-product-data-store-cpt.php | 24 ++-
.../class-wc-product-data-store-interface.php | 2 +-
.../includes/wc-product-functions.php | 20 ++-
.../Blocks/BlockTypes/ProductCollection.php | 12 +-
.../includes/wc-product-functions-test.php | 147 ++++++++++++++++++
5 files changed, 187 insertions(+), 18 deletions(-)
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
index b5a611d6f8a..68234967684 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
@@ -1453,7 +1453,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
* @param array $cats_array List of categories IDs.
* @param array $tags_array List of tags IDs.
* @param array $exclude_ids Excluded IDs.
- * @param int $limit Limit of results.
+ * @param int $limit Limit of results, -1 for no limit.
* @param int $product_id Product ID.
* @return array
*/
@@ -1464,13 +1464,20 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
'categories' => $cats_array,
'tags' => $tags_array,
'exclude_ids' => $exclude_ids,
- 'limit' => $limit + 10,
+ 'limit' => $limit,
);
- $related_product_query = (array) apply_filters( 'woocommerce_product_related_posts_query', $this->get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit + 10 ), $product_id, $args );
+ $related_product_query = (array) apply_filters(
+ 'woocommerce_product_related_posts_query',
+ $this->get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit ),
+ $product_id,
+ $args
+ );
// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
- return $wpdb->get_col( implode( ' ', $related_product_query ) );
+ $products = $wpdb->get_col( implode( ' ', $related_product_query ) );
+
+ return array_map( 'intval', $products );
}
/**
@@ -1481,7 +1488,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
* @param array $cats_array List of categories IDs.
* @param array $tags_array List of tags IDs.
* @param array $exclude_ids Excluded IDs.
- * @param int $limit Limit of results.
+ * @param int $limit Limit of results, -1 for no limit.
*
* @return array
*/
@@ -1511,11 +1518,12 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
AND p.post_type = 'product'
",
- 'limits' => '
- LIMIT ' . absint( $limit ) . '
- ',
);
+ if ( $limit > 0 ) {
+ $query['limits'] = ' LIMIT ' . absint( $limit );
+ }
+
if ( count( $exclude_term_ids ) ) {
$query['join'] .= " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = p.ID';
$query['where'] .= ' AND exclude_join.object_id IS NULL';
diff --git a/plugins/woocommerce/includes/interfaces/class-wc-product-data-store-interface.php b/plugins/woocommerce/includes/interfaces/class-wc-product-data-store-interface.php
index ed9244ae56a..4199fefa857 100644
--- a/plugins/woocommerce/includes/interfaces/class-wc-product-data-store-interface.php
+++ b/plugins/woocommerce/includes/interfaces/class-wc-product-data-store-interface.php
@@ -86,7 +86,7 @@ interface WC_Product_Data_Store_Interface {
* @param array $cats_array List of categories IDs.
* @param array $tags_array List of tags IDs.
* @param array $exclude_ids Excluded IDs.
- * @param int $limit Limit of results.
+ * @param int $limit Limit of results, -1 for no limit.
* @param int $product_id Product ID.
* @return array
*/
diff --git a/plugins/woocommerce/includes/wc-product-functions.php b/plugins/woocommerce/includes/wc-product-functions.php
index 7232ef7ccc7..7f2450df9f5 100644
--- a/plugins/woocommerce/includes/wc-product-functions.php
+++ b/plugins/woocommerce/includes/wc-product-functions.php
@@ -976,14 +976,15 @@ function wc_get_product_backorder_options() {
*
* @since 3.0.0
* @param int $product_id Product ID.
- * @param int $limit Limit of results.
+ * @param int $limit Limit of results, -1 for no limit.
* @param array $exclude_ids Exclude IDs from the results.
* @return array
*/
function wc_get_related_products( $product_id, $limit = 5, $exclude_ids = array() ) {
$product_id = absint( $product_id );
- $limit = $limit >= -1 ? $limit : 5;
+ $limit = intval( $limit );
+ $limit = $limit < -1 ? -1 : $limit; // Any negative number will default to no limit.
$exclude_ids = array_merge( array( 0, $product_id ), $exclude_ids );
$transient_name = 'wc_related_' . $product_id;
$query_args = http_build_query(
@@ -997,7 +998,7 @@ function wc_get_related_products( $product_id, $limit = 5, $exclude_ids = array(
$related_posts = $transient && is_array( $transient ) && isset( $transient[ $query_args ] ) ? $transient[ $query_args ] : false;
// We want to query related posts if they are not cached, or we don't have enough.
- if ( false === $related_posts || count( $related_posts ) < $limit ) {
+ if ( false === $related_posts || ( $limit > 0 && count( $related_posts ) < $limit ) ) {
$cats_array = apply_filters( 'woocommerce_product_related_posts_relate_by_category', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_cat_terms', wc_get_product_term_ids( $product_id, 'product_cat' ), $product_id ) : array();
$tags_array = apply_filters( 'woocommerce_product_related_posts_relate_by_tag', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_tag_terms', wc_get_product_term_ids( $product_id, 'product_tag' ), $product_id ) : array();
@@ -1006,8 +1007,13 @@ function wc_get_related_products( $product_id, $limit = 5, $exclude_ids = array(
if ( empty( $cats_array ) && empty( $tags_array ) && ! apply_filters( 'woocommerce_product_related_posts_force_display', false, $product_id ) ) {
$related_posts = array();
} else {
+ // For backward compatibility we need to grab 10 extra products. This was done so that when we shuffle the
+ // output below it will appear to be random by including different products rather than the same ones
+ // in a different order.
+ $query_limit = $limit > 0 ? $limit + 10 : -1;
+
$data_store = WC_Data_Store::load( 'product' );
- $related_posts = $data_store->get_related_products( $cats_array, $tags_array, $exclude_ids, $limit + 10, $product_id );
+ $related_posts = $data_store->get_related_products( $cats_array, $tags_array, $exclude_ids, $query_limit, $product_id );
}
if ( $transient && is_array( $transient ) ) {
@@ -1033,7 +1039,11 @@ function wc_get_related_products( $product_id, $limit = 5, $exclude_ids = array(
shuffle( $related_posts );
}
- return array_slice( $related_posts, 0, $limit );
+ if ( $limit > 0 ) {
+ return array_slice( $related_posts, 0, $limit );
+ }
+
+ return $related_posts;
}
/**
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
index fa91cd0b82b..43c13e941f6 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
@@ -780,7 +780,7 @@ class ProductCollection extends AbstractBlock {
);
$handpicked_products = $query['handpicked_products'] ?? array();
- $related_products = $this->get_related_products( $query['related_to'], $common_query_values['posts_per_page'] );
+ $related_products = $this->get_related_products( $query['related_to'] );
$result = $this->filter_query_to_only_include_ids( $merged_query, $handpicked_products, $related_products );
@@ -1097,10 +1097,9 @@ class ProductCollection extends AbstractBlock {
* Returns a query for filtering products that are related to the ones given.
*
* @param array $product_ids The IDs pf products that we're looking for a relationship to.
- * @param int $per_page The number of related products to fetch.
* @return array The IDs of the related products.
*/
- private function get_related_products( $product_ids, $per_page ) {
+ private function get_related_products( $product_ids ) {
if ( empty( $product_ids ) ) {
return array();
}
@@ -1109,7 +1108,12 @@ class ProductCollection extends AbstractBlock {
foreach ( $product_ids as $id ) {
$related_product_ids = array_merge(
$related_product_ids,
- wc_get_related_products( $id, $per_page )
+ // Since this is a secondary query we want to make sure to
+ // grab enough products so that filtering in the primary
+ // query will have a subset to work with. We ALSO need
+ // to make sure that we aren't harming performance by
+ // fetching and caching too many related product IDs.
+ wc_get_related_products( $id, 100 )
);
}
diff --git a/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php
index 247c74e4fe7..30f4b47b980 100644
--- a/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php
@@ -240,4 +240,151 @@ class WC_Product_Functions_Tests extends \WC_Unit_Test_Case {
$this->assertEquals( 100, wc_get_product( $product->get_id() )->get_price() );
}
+
+ /**
+ * @testDox Products related by tag or category should be returned.
+ */
+ public function test_wc_get_related_products() {
+ add_filter( 'woocommerce_product_related_posts_relate_by_category', '__return_true', 100 );
+ add_filter( 'woocommerce_product_related_posts_relate_by_tag', '__return_true', 100 );
+
+ $related_tag = wp_insert_term( 'Related Tag', 'product_tag' );
+ $related_cat = wp_insert_term( 'Related Category', 'product_cat' );
+
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_tag_ids( array( $related_tag['term_id'] ) );
+ $product->set_category_ids( array( $related_cat['term_id'] ) );
+ $product->save();
+
+ $related_product_1 = WC_Helper_Product::create_simple_product();
+ $related_product_1->set_category_ids( array( $related_cat['term_id'] ) );
+ $related_product_1->save();
+
+ $related_product_2 = WC_Helper_Product::create_simple_product();
+ $related_product_2->set_tag_ids( array( $related_tag['term_id'] ) );
+ $related_product_2->save();
+
+ $related = wc_get_related_products( $product->get_id() );
+
+ remove_filter( 'woocommerce_product_related_posts_relate_by_category', '__return_true', 100 );
+ remove_filter( 'woocommerce_product_related_posts_relate_by_tag', '__return_true', 100 );
+
+ $this->assertEquals( 2, count( $related ) );
+ $this->assertContains( $related_product_1->get_id(), $related );
+ $this->assertContains( $related_product_2->get_id(), $related );
+ }
+
+ /**
+ * @testDox Only products related by tag should be returned when the appropriate filters are set.
+ */
+ public function test_wc_get_related_products_by_tag() {
+ add_filter( 'woocommerce_product_related_posts_relate_by_category', '__return_false', 100 );
+ add_filter( 'woocommerce_product_related_posts_relate_by_tag', '__return_true', 100 );
+
+ $related_tag = wp_insert_term( 'Related Tag', 'product_tag' );
+ $related_cat = wp_insert_term( 'Related Category', 'product_cat' );
+
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_tag_ids( array( $related_tag['term_id'] ) );
+ $product->set_category_ids( array( $related_cat['term_id'] ) );
+ $product->save();
+
+ $related_product_1 = WC_Helper_Product::create_simple_product();
+ $related_product_1->set_tag_ids( array( $related_tag['term_id'] ) );
+ $related_product_1->set_category_ids( array( $related_cat['term_id'] ) );
+ $related_product_1->save();
+
+ $related_product_2 = WC_Helper_Product::create_simple_product();
+ $related_product_2->set_tag_ids( array( $related_tag['term_id'] ) );
+ $related_product_2->set_category_ids( array( $related_cat['term_id'] ) );
+ $related_product_2->save();
+
+ $unrelated_product = WC_Helper_Product::create_simple_product();
+ $unrelated_product->set_category_ids( array( $related_cat['term_id'] ) );
+ $unrelated_product->save();
+
+ $related = wc_get_related_products( $product->get_id() );
+
+ remove_filter( 'woocommerce_product_related_posts_relate_by_category', '__return_false', 100 );
+ remove_filter( 'woocommerce_product_related_posts_relate_by_tag', '__return_true', 100 );
+
+ $this->assertEquals( 2, count( $related ) );
+ $this->assertContains( $related_product_1->get_id(), $related );
+ $this->assertContains( $related_product_2->get_id(), $related );
+ }
+
+ /**
+ * @testDox Only products related by category should be returned when the appropriate filters are set.
+ */
+ public function test_wc_get_related_products_by_category() {
+ add_filter( 'woocommerce_product_related_posts_relate_by_category', '__return_true', 100 );
+ add_filter( 'woocommerce_product_related_posts_relate_by_tag', '__return_false', 100 );
+
+ $related_tag = wp_insert_term( 'Related Tag', 'product_tag' );
+ $related_cat = wp_insert_term( 'Related Category', 'product_cat' );
+
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_tag_ids( array( $related_tag['term_id'] ) );
+ $product->set_category_ids( array( $related_cat['term_id'] ) );
+ $product->save();
+
+ $related_product_1 = WC_Helper_Product::create_simple_product();
+ $related_product_1->set_tag_ids( array( $related_tag['term_id'] ) );
+ $related_product_1->set_category_ids( array( $related_cat['term_id'] ) );
+ $related_product_1->save();
+
+ $related_product_2 = WC_Helper_Product::create_simple_product();
+ $related_product_2->set_tag_ids( array( $related_tag['term_id'] ) );
+ $related_product_2->set_category_ids( array( $related_cat['term_id'] ) );
+ $related_product_2->save();
+
+ $unrelated_product = WC_Helper_Product::create_simple_product();
+ $unrelated_product->set_tag_ids( array( $related_tag['term_id'] ) );
+ $unrelated_product->save();
+
+ $related = wc_get_related_products( $product->get_id() );
+
+ remove_filter( 'woocommerce_product_related_posts_relate_by_category', '__return_true', 100 );
+ remove_filter( 'woocommerce_product_related_posts_relate_by_tag', '__return_false', 100 );
+
+ $this->assertEquals( 2, count( $related ) );
+ $this->assertContains( $related_product_1->get_id(), $related );
+ $this->assertContains( $related_product_2->get_id(), $related );
+ }
+
+ /**
+ * @testDox Products related by tag or category should apply a limit to the results.
+ */
+ public function test_wc_get_related_products_limit() {
+ add_filter( 'woocommerce_product_related_posts_relate_by_category', '__return_true', 100 );
+ add_filter( 'woocommerce_product_related_posts_relate_by_tag', '__return_true', 100 );
+
+ $related_tag = wp_insert_term( 'Related Tag', 'product_tag' );
+ $related_cat = wp_insert_term( 'Related Category', 'product_cat' );
+
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_tag_ids( array( $related_tag['term_id'] ) );
+ $product->set_category_ids( array( $related_cat['term_id'] ) );
+ $product->save();
+
+ $related_product_1 = WC_Helper_Product::create_simple_product();
+ $related_product_1->set_category_ids( array( $related_cat['term_id'] ) );
+ $related_product_1->save();
+
+ $related_product_2 = WC_Helper_Product::create_simple_product();
+ $related_product_2->set_tag_ids( array( $related_tag['term_id'] ) );
+ $related_product_2->save();
+
+ $related = wc_get_related_products( $product->get_id(), 1 );
+ $unlimited_related = wc_get_related_products( $product->get_id(), -1 );
+
+ remove_filter( 'woocommerce_product_related_posts_relate_by_category', '__return_true', 100 );
+ remove_filter( 'woocommerce_product_related_posts_relate_by_tag', '__return_true', 100 );
+
+ $this->assertEquals( 1, count( $related ) );
+ $this->assertContains( $related[0], array( $related_product_1->get_id(), $related_product_2->get_id() ) );
+ $this->assertEquals( 2, count( $unlimited_related ) );
+ $this->assertContains( $related_product_1->get_id(), $unlimited_related );
+ $this->assertContains( $related_product_2->get_id(), $unlimited_related );
+ }
}
From aa1ffdcecd9111bfb7326ab987542dae0b3bbc38 Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Tue, 10 Sep 2024 20:49:30 -0700
Subject: [PATCH 12/26] Removed `woocommerceRelatedTo` Param
Since we're going to use the collection name, we don't need
this functionality any longer.
---
.../register-product-collection.tsx | 12 ------
.../js/blocks/product-collection/constants.ts | 3 --
.../js/blocks/product-collection/types.ts | 4 --
.../Blocks/BlockTypes/ProductCollection.php | 7 +--
.../Blocks/BlockTypes/ProductCollection.php | 43 -------------------
5 files changed, 1 insertion(+), 68 deletions(-)
diff --git a/plugins/woocommerce-blocks/assets/js/blocks-registry/product-collection/register-product-collection.tsx b/plugins/woocommerce-blocks/assets/js/blocks-registry/product-collection/register-product-collection.tsx
index 05b15355c84..516fb64e48d 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks-registry/product-collection/register-product-collection.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks-registry/product-collection/register-product-collection.tsx
@@ -222,15 +222,6 @@ const isValidCollectionConfig = ( config: ProductCollectionConfig ) => {
'Invalid woocommerceHandPickedProducts: woocommerceHandPickedProducts must be an array.'
);
}
- // attributes.query.woocommerceRelatedTo
- if (
- config.attributes?.query?.woocommerceRelatedTo !== undefined &&
- ! Array.isArray( config.attributes.query.woocommerceRelatedTo )
- ) {
- console.warn(
- 'Invalid woocommerceRelatedTo: woocommerceRelatedTo must be an array.'
- );
- }
// attributes.query.priceRange
if (
config.attributes?.query?.priceRange !== undefined &&
@@ -411,9 +402,6 @@ export const __experimentalRegisterProductCollection = (
woocommerceHandPickedProducts:
query.woocommerceHandPickedProducts,
} ),
- ...( query.woocommerceRelatedTo !== undefined && {
- woocommerceRelatedTo: query.woocommerceRelatedTo,
- } ),
...( query.priceRange !== undefined && {
priceRange: query.priceRange,
} ),
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/constants.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/constants.ts
index 96d34f5b949..98a1d9be2f6 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/constants.ts
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/constants.ts
@@ -60,7 +60,6 @@ export const DEFAULT_QUERY: ProductCollectionQuery = {
woocommerceStockStatus: getDefaultStockStatuses(),
woocommerceAttributes: [],
woocommerceHandPickedProducts: [],
- woocommerceRelatedTo: [],
timeFrame: undefined,
priceRange: undefined,
filterable: false,
@@ -91,7 +90,6 @@ export const DEFAULT_FILTERS: Pick<
| 'woocommerceStockStatus'
| 'woocommerceAttributes'
| 'woocommerceHandPickedProducts'
- | 'woocommerceRelatedTo'
| 'taxQuery'
| 'featured'
| 'timeFrame'
@@ -101,7 +99,6 @@ export const DEFAULT_FILTERS: Pick<
woocommerceStockStatus: DEFAULT_QUERY.woocommerceStockStatus,
woocommerceAttributes: DEFAULT_QUERY.woocommerceAttributes,
woocommerceHandPickedProducts: DEFAULT_QUERY.woocommerceHandPickedProducts,
- woocommerceRelatedTo: DEFAULT_QUERY.woocommerceRelatedTo,
taxQuery: DEFAULT_QUERY.taxQuery,
featured: DEFAULT_QUERY.featured,
timeFrame: DEFAULT_QUERY.timeFrame,
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts
index 95d55186da7..04bc42f22ca 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts
@@ -103,10 +103,6 @@ export interface ProductCollectionQuery {
woocommerceAttributes: AttributeMetadata[];
isProductCollectionBlock: boolean;
woocommerceHandPickedProducts: string[];
- /**
- * Filter for products related to the given list of product IDs.
- */
- woocommerceRelatedTo: string[];
priceRange: undefined | PriceRange;
filterable: boolean;
productReference?: number;
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
index 43c13e941f6..5f2ff50e4f1 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
@@ -609,7 +609,6 @@ class ProductCollection extends AbstractBlock {
$stock_status = $request->get_param( 'woocommerceStockStatus' );
$product_attributes = $request->get_param( 'woocommerceAttributes' );
$handpicked_products = $request->get_param( 'woocommerceHandPickedProducts' );
- $related_to = $request->get_param( 'woocommerceRelatedTo' );
$featured = $request->get_param( 'featured' );
$time_frame = $request->get_param( 'timeFrame' );
$price_range = $request->get_param( 'priceRange' );
@@ -625,7 +624,6 @@ class ProductCollection extends AbstractBlock {
'stock_status' => $stock_status,
'product_attributes' => $product_attributes,
'handpicked_products' => $handpicked_products,
- 'related_to' => $related_to,
'featured' => $featured,
'timeFrame' => $time_frame,
'priceRange' => $price_range,
@@ -722,7 +720,6 @@ class ProductCollection extends AbstractBlock {
$product_attributes = $query['woocommerceAttributes'] ?? array();
$taxonomies_query = $this->get_filter_by_taxonomies_query( $query['tax_query'] ?? array() );
$handpicked_products = $query['woocommerceHandPickedProducts'] ?? array();
- $related_to = $query['woocommerceRelatedTo'] ?? array();
$time_frame = $query['timeFrame'] ?? null;
$price_range = $query['priceRange'] ?? null;
@@ -735,7 +732,6 @@ class ProductCollection extends AbstractBlock {
'product_attributes' => $product_attributes,
'taxonomies_query' => $taxonomies_query,
'handpicked_products' => $handpicked_products,
- 'related_to' => $related_to,
'featured' => $query['featured'] ?? false,
'timeFrame' => $time_frame,
'priceRange' => $price_range,
@@ -780,9 +776,8 @@ class ProductCollection extends AbstractBlock {
);
$handpicked_products = $query['handpicked_products'] ?? array();
- $related_products = $this->get_related_products( $query['related_to'] );
- $result = $this->filter_query_to_only_include_ids( $merged_query, $handpicked_products, $related_products );
+ $result = $this->filter_query_to_only_include_ids( $merged_query, $handpicked_products );
return $result;
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
index df08c01476a..643b906859c 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
@@ -90,7 +90,6 @@ class ProductCollection extends \WP_UnitTestCase {
'woocommerceOnSale' => false,
'woocommerceAttributes' => array(),
'woocommerceStockStatus' => array(),
- 'woocommerceRelatedTo' => array(),
'timeFrame' => array(),
'priceRange' => array(),
)
@@ -683,7 +682,6 @@ class ProductCollection extends \WP_UnitTestCase {
),
),
'woocommerceStockStatus' => array( 'instock', 'outofstock' ),
- 'woocommerceRelatedTo' => array(),
'timeFrame' => array(
'operator' => 'in',
'value' => $time_frame_date,
@@ -962,62 +960,21 @@ class ProductCollection extends \WP_UnitTestCase {
$this->assertCount( 4, $merged_query['post__in'] );
}
- /**
- * Test related to queries.
- */
- public function test_related_to_queries() {
- $related_to_product_ids = array( 1, 2, 3 );
- // This filter will turn off the data store so we don't need dummy products.
- add_filter( 'woocommerce_product_related_posts_force_display', '__return_true', 0 );
- $related_filter = function ( $related_posts, $product_id ) use ( $related_to_product_ids ) {
- $this->assertEquals( 4, $product_id );
- return $related_to_product_ids;
- };
- add_filter( 'woocommerce_related_products', $related_filter, 10, 2 );
-
- $parsed_block = $this->get_base_parsed_block();
- $parsed_block['attrs']['query']['woocommerceRelatedTo'] = array( 4 );
-
- $merged_query = $this->initialize_merged_query( $parsed_block );
-
- remove_filter( 'woocommerce_product_related_posts_force_display', '__return_true', 0 );
- remove_filter( 'woocommerce_related_products', $related_filter );
-
- foreach ( $related_to_product_ids as $id ) {
- $this->assertContainsEquals( $id, $merged_query['post__in'] );
- }
-
- $this->assertCount( 3, $merged_query['post__in'] );
- }
-
/**
* Test merging exclusive id filters.
*/
public function test_merges_exclusive_id_filters() {
$existing_id_filter = array( 1, 4 );
$handpicked_product_ids = array( 3, 4, 5, 6 );
- $related_to_product_ids = array( 1, 2, 3, 4 );
// The only ID present in ALL of the exclusive filters is 4.
$expected_product_ids = array( 4 );
- // This filter will turn off the data store so we don't need dummy products.
- add_filter( 'woocommerce_product_related_posts_force_display', '__return_true', 0 );
- $related_filter = function ( $related_posts, $product_id ) use ( $related_to_product_ids ) {
- $this->assertEquals( 10, $product_id );
- return $related_to_product_ids;
- };
- add_filter( 'woocommerce_related_products', $related_filter, 10, 2 );
-
$parsed_block = $this->get_base_parsed_block();
$parsed_block['attrs']['query']['post__in'] = $existing_id_filter;
$parsed_block['attrs']['query']['woocommerceHandPickedProducts'] = $handpicked_product_ids;
- $parsed_block['attrs']['query']['woocommerceRelatedTo'] = array( 10 );
$merged_query = $this->initialize_merged_query( $parsed_block );
- remove_filter( 'woocommerce_product_related_posts_force_display', '__return_true', 0 );
- remove_filter( 'woocommerce_related_products', $related_filter );
-
foreach ( $expected_product_ids as $id ) {
$this->assertContainsEquals( $id, $merged_query['post__in'] );
}
From e34a24b195783b99e0592ea5dc36187645bc9f57 Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Tue, 10 Sep 2024 23:52:51 -0700
Subject: [PATCH 13/26] Simplified `post__in` Filtering
Since `merge_queries` was already using an intersection merge for
`post__in`, this second function is unnecessary. I've removed it
as well as refactored the merging logic.
---
.../Blocks/BlockTypes/ProductCollection.php | 147 +++++++++---------
1 file changed, 77 insertions(+), 70 deletions(-)
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
index 5f2ff50e4f1..885a0f14fb1 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
@@ -760,11 +760,12 @@ class ProductCollection extends AbstractBlock {
$tax_query = $this->merge_tax_queries( $visibility_query, $attributes_query, $taxonomies_query, $featured_query );
$date_query = $this->get_date_query( $query['timeFrame'] ?? array() );
$price_query_args = $this->get_price_range_query_args( $query['priceRange'] ?? array() );
+ $handpicked_query = $this->get_handpicked_query( $query['handpicked_products'] ?? false );
// We exclude applied filters to generate product ids for the filter blocks.
$applied_filters_query = $is_exclude_applied_filters ? array() : $this->get_queries_by_applied_filters();
- $merged_query = $this->merge_queries(
+ return $this->merge_queries(
$common_query_values,
$orderby_query,
$on_sale_query,
@@ -772,14 +773,9 @@ class ProductCollection extends AbstractBlock {
$tax_query,
$applied_filters_query,
$date_query,
- $price_query_args
+ $price_query_args,
+ $handpicked_query
);
-
- $handpicked_products = $query['handpicked_products'] ?? array();
-
- $result = $this->filter_query_to_only_include_ids( $merged_query, $handpicked_products );
-
- return $result;
}
/**
@@ -828,42 +824,80 @@ class ProductCollection extends AbstractBlock {
* @return array
*/
private function merge_queries( ...$queries ) {
+ // Rather than a simple merge, some query vars should be held aside and merged differently.
+ $special_query_vars = array(
+ 'post__in' => array(),
+ );
+ $special_query_keys = array_keys( $special_query_vars );
+
$merged_query = array_reduce(
$queries,
- function ( $acc, $query ) {
+ function ( $acc, $query ) use ( $special_query_keys, &$special_query_vars ) {
if ( ! is_array( $query ) ) {
return $acc;
}
+
// If the $query doesn't contain any valid query keys, we unpack/spread it then merge.
if ( empty( array_intersect( $this->get_valid_query_vars(), array_keys( $query ) ) ) ) {
return $this->merge_queries( $acc, ...array_values( $query ) );
}
+
+ // Pull out the special query vars so we can merge them separately.
+ foreach ( $special_query_keys as $query_var ) {
+ if ( isset( $query[ $query_var ] ) ) {
+ $special_query_vars[ $query_var ][] = $query[ $query_var ];
+ unset( $query[ $query_var ] );
+ }
+ }
+
return $this->array_merge_recursive_replace_non_array_properties( $acc, $query );
},
array()
);
- /**
- * If there are duplicated items in post__in, it means that we need to
- * use the intersection of the results, which in this case, are the
- * duplicated items.
- */
- if (
- ! empty( $merged_query['post__in'] ) &&
- is_array( $merged_query['post__in'] ) &&
- count( $merged_query['post__in'] ) > count( array_unique( $merged_query['post__in'] ) )
- ) {
- $merged_query['post__in'] = array_unique(
- array_diff(
- $merged_query['post__in'],
- array_unique( $merged_query['post__in'] )
- )
- );
- }
+ // Perform any necessary special merges.
+ $merged_query['post__in'] = $this->merge_post__in( ...$special_query_vars['post__in'] );
return $merged_query;
}
+ /**
+ * Merge all of the 'post__in' values and return an array containing only values that are present in all arrays.
+ *
+ * @param int[][] ...$post__in The 'post__in' values to be merged.
+ *
+ * @return int[] The merged 'post__in' values.
+ */
+ private function merge_post__in( ...$post__in ) {
+ if ( empty( $post__in ) ) {
+ return array();
+ }
+
+ // Since we're using array_intersect, any array that is empty will result
+ // in an empty output array. To avoid this we need to make sure every
+ // argument is a non-empty array.
+ $post__in = array_filter(
+ $post__in,
+ function ( $val ) {
+ return is_array( $val ) && ! empty( $val );
+ }
+ );
+ if ( empty( $post__in ) ) {
+ return array();
+ }
+
+ // Since the 'post__in' filter is exclusionary we need to use an intersection of
+ // all of the arrays. This ensures one query doesn't add options that another
+ // has otherwise excluded from the results.
+ if ( count( $post__in ) > 1 ) {
+ $post__in = array_intersect( ...$post__in );
+ } else {
+ $post__in = reset( $post__in );
+ }
+
+ return array_values( array_unique( $post__in, SORT_NUMERIC ) );
+ }
+
/**
* Return query params to support custom sort values
*
@@ -1143,6 +1177,23 @@ class ProductCollection extends AbstractBlock {
);
}
+ /**
+ * Generates a post__in query to filter products to the set of provided IDs.
+ *
+ * @param int[]|false $handpicked_products The products to filter.
+ *
+ * @return array The post__in query.
+ */
+ private function get_handpicked_query( $handpicked_products ) {
+ if ( false === $handpicked_products ) {
+ return array();
+ }
+
+ return array(
+ 'post__in' => $handpicked_products,
+ );
+ }
+
/**
* Merge tax_queries from various queries.
@@ -1243,50 +1294,6 @@ class ProductCollection extends AbstractBlock {
return ! empty( $result ) ? array( 'tax_query' => $result ) : array();
}
- /**
- * Apply the query only to a subset of products
- *
- * @param array $query The query.
- * @param array ...$ids The product IDs to filter.
- *
- * @return array
- */
- private function filter_query_to_only_include_ids( $query, ...$ids ) {
- if ( empty( $ids ) ) {
- return $query;
- }
-
- // Since we're using array_intersect, any array that is empty will result
- // in an empty array. To avoid this, we need to make sure every
- // argument is a non-empty array.
- $i = 0;
- $len = count( $ids );
- $post_in_filter = null;
-
- // Make sure to filter any product IDs that have already been set in the query too.
- if ( ! empty( $query['post__in'] ) ) {
- $post_in_filter = $query['post__in'];
- } else {
- // Find the first non-empty array to serve as the base for the intersection.
- while ( empty( $post_in_filter ) && $i < $len ) {
- $post_in_filter = $ids[ $i ];
- ++$i;
- }
- }
-
- for ( ; $i < $len; ++$i ) {
- $arr = $ids[ $i ];
- if ( is_array( $arr ) && ! empty( $arr ) ) {
- $post_in_filter = array_intersect( $arr, $post_in_filter );
- }
- }
-
- // Clean up the output since there will be duplicates and possibly some weird keys.
- $query['post__in'] = array_values( array_unique( $post_in_filter, SORT_NUMERIC ) );
-
- return $query;
- }
-
/**
* Return queries that are generated by query args.
*
From 440cdba53eeb88450246bf368dc28ceac53a556d Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Wed, 11 Sep 2024 01:24:10 -0700
Subject: [PATCH 14/26] Pass Collection Name To Final Query
---
.../product-collection/collections/index.tsx | 4 +-
.../{related-to.tsx => related.tsx} | 12 +---
.../js/blocks/product-collection/types.ts | 2 +-
.../Blocks/BlockTypes/ProductCollection.php | 63 +++++++++----------
4 files changed, 35 insertions(+), 46 deletions(-)
rename plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/{related-to.tsx => related.tsx} (82%)
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/index.tsx
index 708d2b2224b..2a24183d705 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/index.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/index.tsx
@@ -20,7 +20,7 @@ import topRated from './top-rated';
import bestSellers from './best-sellers';
import onSale from './on-sale';
import featured from './featured';
-import relatedTo from './related-to';
+import related from './related';
const collections: BlockVariation[] = [
productCollection,
@@ -29,7 +29,7 @@ const collections: BlockVariation[] = [
onSale,
bestSellers,
newArrivals,
- relatedTo,
+ related,
];
export const registerCollections = () => {
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related-to.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related.tsx
similarity index 82%
rename from plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related-to.tsx
rename to plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related.tsx
index 62c6b4bfcf0..ec530da03dc 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related-to.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related.tsx
@@ -12,21 +12,13 @@ import { INNER_BLOCKS_PRODUCT_TEMPLATE } from '../constants';
import { CoreCollectionNames, LayoutOptions } from '../types';
const collection = {
- name: CoreCollectionNames.RELATED_TO,
+ name: CoreCollectionNames.RELATED,
title: __( 'Related Products', 'woocommerce' ),
icon: ,
description: __( 'Recommend products like this one.', 'woocommerce' ),
keywords: [ 'product collection' ],
scope: [],
- preview: {
- initialPreviewState: {
- isPreview: true,
- previewMessage: __(
- 'Actual products will vary depending on the product being viewed.',
- 'woocommerce'
- ),
- },
- },
+ usesReference: [ 'product' ],
};
const attributes = {
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts
index 04bc42f22ca..5be0fe0a4e7 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts
@@ -153,7 +153,7 @@ export enum CoreCollectionNames {
NEW_ARRIVALS = 'woocommerce/product-collection/new-arrivals',
ON_SALE = 'woocommerce/product-collection/on-sale',
TOP_RATED = 'woocommerce/product-collection/top-rated',
- RELATED_TO = 'woocommerce/product-collection/related',
+ RELATED = 'woocommerce/product-collection/related',
}
export enum CoreFilterNames {
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
index 885a0f14fb1..10e8f1acb89 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
@@ -616,7 +616,8 @@ class ProductCollection extends AbstractBlock {
// Most likely this argument is being accessed in the test environment image.
$args['author'] = '';
- return $this->get_final_query_args(
+ $final_query = $this->get_final_query_args(
+ $product_collection_query_context['collection'] ?? '',
$args,
array(
'orderby' => $orderby,
@@ -629,6 +630,8 @@ class ProductCollection extends AbstractBlock {
'priceRange' => $price_range,
)
);
+
+ return $final_query;
}
/**
@@ -724,6 +727,7 @@ class ProductCollection extends AbstractBlock {
$price_range = $query['priceRange'] ?? null;
$final_query = $this->get_final_query_args(
+ $this->parsed_block['attrs']['collection'] ?? '',
$common_query_values,
array(
'on_sale' => $is_on_sale,
@@ -745,11 +749,12 @@ class ProductCollection extends AbstractBlock {
/**
* Get final query args based on provided values
*
- * @param array $common_query_values Common query values.
- * @param array $query Query from block context.
- * @param bool $is_exclude_applied_filters Whether to exclude the applied filters or not.
+ * @param string $collection The name of the collection.
+ * @param array $common_query_values Common query values.
+ * @param array $query Query from block context.
+ * @param bool $is_exclude_applied_filters Whether to exclude the applied filters or not.
*/
- private function get_final_query_args( $common_query_values, $query, $is_exclude_applied_filters = false ) {
+ private function get_final_query_args( $collection, $common_query_values, $query, $is_exclude_applied_filters = false ) {
$orderby_query = $query['orderby'] ? $this->get_custom_orderby_query( $query['orderby'] ) : array();
$on_sale_query = $this->get_on_sale_products_query( $query['on_sale'] );
$stock_query = $this->get_stock_status_query( $query['stock_status'] );
@@ -761,6 +766,7 @@ class ProductCollection extends AbstractBlock {
$date_query = $this->get_date_query( $query['timeFrame'] ?? array() );
$price_query_args = $this->get_price_range_query_args( $query['priceRange'] ?? array() );
$handpicked_query = $this->get_handpicked_query( $query['handpicked_products'] ?? false );
+ $collection_query = $this->get_core_collection_query( $collection, $query );
// We exclude applied filters to generate product ids for the filter blocks.
$applied_filters_query = $is_exclude_applied_filters ? array() : $this->get_queries_by_applied_filters();
@@ -774,10 +780,28 @@ class ProductCollection extends AbstractBlock {
$applied_filters_query,
$date_query,
$price_query_args,
- $handpicked_query
+ $handpicked_query,
+ $collection_query
);
}
+ /**
+ * Get any collection-specific query args to merge.
+ *
+ * @param string $collection The name of the collection.
+ * @param array $query Query from block context.
+ *
+ * @return array The collection-specific query to merge.
+ */
+ private function get_core_collection_query( $collection, $query ) {
+ $collection = preg_match( '/^woocommerce\/product-collection\/(.*)/', $collection, $matches ) ? $matches[1] : '';
+ if ( '' === $collection ) {
+ return array();
+ }
+
+ return array();
+ }
+
/**
* Get query args for preview mode. These query args will be used with WP_Query to fetch the products.
*
@@ -1122,33 +1146,6 @@ class ProductCollection extends AbstractBlock {
);
}
- /**
- * Returns a query for filtering products that are related to the ones given.
- *
- * @param array $product_ids The IDs pf products that we're looking for a relationship to.
- * @return array The IDs of the related products.
- */
- private function get_related_products( $product_ids ) {
- if ( empty( $product_ids ) ) {
- return array();
- }
-
- $related_product_ids = array();
- foreach ( $product_ids as $id ) {
- $related_product_ids = array_merge(
- $related_product_ids,
- // Since this is a secondary query we want to make sure to
- // grab enough products so that filtering in the primary
- // query will have a subset to work with. We ALSO need
- // to make sure that we aren't harming performance by
- // fetching and caching too many related product IDs.
- wc_get_related_products( $id, 100 )
- );
- }
-
- return $related_product_ids;
- }
-
/**
* Generates a tax query to filter products based on their "featured" status.
* If the `$featured` parameter is true, the function will return a tax query
From b47b0f638f851d769918ed3987a17f7c4a59b670 Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Wed, 11 Sep 2024 14:30:07 -0700
Subject: [PATCH 15/26] Added Custom Collection Handlers
Developers can register collections along with handlers that implement
the custom behavior.
---
.../Blocks/BlockTypes/ProductCollection.php | 143 ++++++++++++------
.../Blocks/BlockTypes/ProductCollection.php | 134 ++++++++++++++++
.../Blocks/Mocks/ProductCollectionMock.php | 24 +++
3 files changed, 254 insertions(+), 47 deletions(-)
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
index 94583e54ec4..652609bf4e9 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
@@ -18,6 +18,13 @@ class ProductCollection extends AbstractBlock {
*/
protected $block_name = 'product-collection';
+ /**
+ * An array keyed by the name of the collection containing handlers for implementing custom collection behavior.
+ *
+ * @var array
+ */
+ protected $collection_handler_store = array();
+
/**
* The Block with its attributes before it gets rendered
*
@@ -617,22 +624,32 @@ class ProductCollection extends AbstractBlock {
/**
* Update the query for the product query block in Editor.
*
- * @param array $args Query args.
+ * @param array $query Query args.
* @param WP_REST_Request $request Request.
*/
- public function update_rest_query_in_editor( $args, $request ): array {
+ public function update_rest_query_in_editor( $query, $request ): array {
// Only update the query if this is a product collection block.
$is_product_collection_block = $request->get_param( 'isProductCollectionBlock' );
if ( ! $is_product_collection_block ) {
- return $args;
+ return $query;
+ }
+
+ $product_collection_query_context = $request->get_param( 'productCollectionQueryContext' );
+ $collection_args = array(
+ 'name' => $product_collection_query_context['collection'] ?? '',
+ );
+
+ // Allow collections to modify the collection arguments passed to the query builder.
+ $handlers = $this->collection_handler_store[ $collection_args['name'] ] ?? null;
+ if ( isset( $handlers['editor_args'] ) ) {
+ $collection_args = call_user_func( $handlers['editor_args'], $collection_args, $query );
}
// Is this a preview mode request?
// If yes, short-circuit the query and return the preview query args.
- $product_collection_query_context = $request->get_param( 'productCollectionQueryContext' );
- $is_preview = $product_collection_query_context['previewState']['isPreview'] ?? false;
+ $is_preview = $product_collection_query_context['previewState']['isPreview'] ?? false;
if ( 'true' === $is_preview ) {
- return $this->get_preview_query_args( $args, $request );
+ return $this->get_preview_query_args( $collection_args, $query, $request );
}
$orderby = $request->get_param( 'orderBy' );
@@ -645,11 +662,11 @@ class ProductCollection extends AbstractBlock {
$price_range = $request->get_param( 'priceRange' );
// This argument is required for the tests to PHP Unit Tests to run correctly.
// Most likely this argument is being accessed in the test environment image.
- $args['author'] = '';
+ $query['author'] = '';
$final_query = $this->get_final_query_args(
- $product_collection_query_context['collection'] ?? '',
- $args,
+ $collection_args,
+ $query,
array(
'orderby' => $orderby,
'on_sale' => $on_sale,
@@ -757,8 +774,18 @@ class ProductCollection extends AbstractBlock {
$time_frame = $query['timeFrame'] ?? null;
$price_range = $query['priceRange'] ?? null;
+ $collection_args = array(
+ 'name' => $this->parsed_block['attrs']['collection'] ?? '',
+ );
+
+ // Allow collections to modify the collection arguments passed to the query builder.
+ $handlers = $this->collection_handler_store[ $collection_args['name'] ] ?? null;
+ if ( isset( $handlers['frontend_args'] ) ) {
+ $collection_args = call_user_func( $handlers['frontend_args'], $collection_args, $query );
+ }
+
$final_query = $this->get_final_query_args(
- $this->parsed_block['attrs']['collection'] ?? '',
+ $collection_args,
$common_query_values,
array(
'on_sale' => $is_on_sale,
@@ -780,12 +807,17 @@ class ProductCollection extends AbstractBlock {
/**
* Get final query args based on provided values
*
- * @param string $collection The name of the collection.
- * @param array $common_query_values Common query values.
- * @param array $query Query from block context.
- * @param bool $is_exclude_applied_filters Whether to exclude the applied filters or not.
+ * @param array $collection_args Any special arguments that should change the behavior of the query.
+ * @param array $common_query_values Common query values.
+ * @param array $query Query from block context.
+ * @param bool $is_exclude_applied_filters Whether to exclude the applied filters or not.
*/
- private function get_final_query_args( $collection, $common_query_values, $query, $is_exclude_applied_filters = false ) {
+ private function get_final_query_args(
+ $collection_args,
+ $common_query_values,
+ $query,
+ $is_exclude_applied_filters = false
+ ) {
$orderby_query = $query['orderby'] ? $this->get_custom_orderby_query( $query['orderby'] ) : array();
$on_sale_query = $this->get_on_sale_products_query( $query['on_sale'] );
$stock_query = $this->get_stock_status_query( $query['stock_status'] );
@@ -797,11 +829,24 @@ class ProductCollection extends AbstractBlock {
$date_query = $this->get_date_query( $query['timeFrame'] ?? array() );
$price_query_args = $this->get_price_range_query_args( $query['priceRange'] ?? array() );
$handpicked_query = $this->get_handpicked_query( $query['handpicked_products'] ?? false );
- $collection_query = $this->get_core_collection_query( $collection, $query );
// We exclude applied filters to generate product ids for the filter blocks.
$applied_filters_query = $is_exclude_applied_filters ? array() : $this->get_queries_by_applied_filters();
+ // Allow collections to provide their own query parameters.
+ $handlers = $this->collection_handler_store[ $collection_args['name'] ] ?? null;
+ if ( isset( $handlers['build_query'] ) ) {
+ $collection_query = call_user_func(
+ $handlers['build_query'],
+ $collection_args,
+ $common_query_values,
+ $query,
+ $is_exclude_applied_filters
+ );
+ } else {
+ $collection_query = array();
+ }
+
return $this->merge_queries(
$common_query_values,
$orderby_query,
@@ -816,41 +861,21 @@ class ProductCollection extends AbstractBlock {
);
}
- /**
- * Get any collection-specific query args to merge.
- *
- * @param string $collection The name of the collection.
- * @param array $query Query from block context.
- *
- * @return array The collection-specific query to merge.
- */
- private function get_core_collection_query( $collection, $query ) {
- $collection = preg_match( '/^woocommerce\/product-collection\/(.*)/', $collection, $matches ) ? $matches[1] : '';
- if ( '' === $collection ) {
- return array();
- }
-
- return array();
- }
-
/**
* Get query args for preview mode. These query args will be used with WP_Query to fetch the products.
*
- * @param array $args Query args.
- * @param WP_REST_Request $request Request.
+ * @param array $collection_args Any collection-specific arguments.
+ * @param array $args Query args.
+ * @param WP_REST_Request $request Request.
*/
- private function get_preview_query_args( $args, $request ) {
+ private function get_preview_query_args( $collection_args, $args, $request ) {
$collection_query = array();
- /**
- * In future, Here we will modify the preview query based on the collection name. For example:
- *
- * $product_collection_query_context = $request->get_param( 'productCollectionQueryContext' );
- * $collection_name = $product_collection_query_context['collection'] ?? '';
- * if ( 'woocommerce/product-collection/on-sale' === $collection_name ) {
- * $collection_query = $this->get_on_sale_products_query( true );
- * }.
- */
+ // Allow collections to override the preview mode behavior.
+ $handlers = $this->collection_handler_store[ $collection_args['name'] ] ?? null;
+ if ( isset( $handlers['preview_query'] ) ) {
+ $collection_query = call_user_func( $handlers['preview_query'], $collection_args, $args, $request );
+ }
$args = $this->merge_queries( $args, $collection_query );
return $args;
@@ -892,8 +917,8 @@ class ProductCollection extends AbstractBlock {
return $acc;
}
- // If the $query doesn't contain any valid query keys, we unpack/spread it then merge.
- if ( empty( array_intersect( $this->get_valid_query_vars(), array_keys( $query ) ) ) ) {
+ // When the $query has keys but doesn't contain any valid query keys, we unpack/spread it then merge.
+ if ( ! empty( $query ) && empty( array_intersect( $this->get_valid_query_vars(), array_keys( $query ) ) ) ) {
return $this->merge_queries( $acc, ...array_values( $query ) );
}
@@ -1750,4 +1775,28 @@ class ProductCollection extends AbstractBlock {
return $price_filter + array_sum( $taxes );
}
+
+ /**
+ * Registers handlers for a collection.
+ *
+ * @param string $collection_name The name of the custom collection.
+ * @param callable $build_query A hook returning any custom query arguments to merge with the collection's query.
+ * @param callable|null $frontend_args An optional hook that returns any frontend collection arguments to pass to the query builder.
+ * @param callable|null $editor_args An optional hook that returns any REST collection arguments to pass to the query builder.
+ * @param callable|null $preview_query An optional hook that returns a query to use in preview mode.
+ *
+ * @throws \InvalidArgumentException If collection handlers are already registered for the given collection name.
+ */
+ protected function register_collection_handlers( $collection_name, $build_query, $frontend_args = null, $editor_args = null, $preview_query = null ) {
+ if ( isset( $this->collection_handler_store[ $collection_name ] ) ) {
+ throw new \InvalidArgumentException( 'Collection handlers already registered for ' . esc_html( $collection_name ) );
+ }
+
+ $this->collection_handler_store[ $collection_name ] = array(
+ 'build_query' => $build_query,
+ 'frontend_args' => $frontend_args,
+ 'editor_args' => $editor_args,
+ 'preview_query' => $preview_query,
+ );
+ }
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
index 643b906859c..49cf1db1a27 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
@@ -981,4 +981,138 @@ class ProductCollection extends \WP_UnitTestCase {
$this->assertCount( 1, $merged_query['post__in'] );
}
+
+ /**
+ * Test for frontend collection handlers.
+ */
+ public function test_frontend_collection_handlers() {
+ $build_query = $this->getMockBuilder( \stdClass::class )
+ ->setMethods( [ '__invoke' ] )
+ ->getMock();
+ $frontend_args = $this->getMockBuilder( \stdClass::class )
+ ->setMethods( [ '__invoke' ] )
+ ->getMock();
+ $this->block_instance->register_collection_handlers( 'test-collection', $build_query, $frontend_args );
+
+ $frontend_args->expects( $this->once() )
+ ->method( '__invoke' )
+ ->willReturnCallback(
+ function ( $collection_args ) {
+ $collection_args['test'] = 'test-arg';
+ return $collection_args;
+ }
+ );
+ $build_query->expects( $this->once() )
+ ->method( '__invoke' )
+ ->willReturnCallback(
+ function ( $collection_args ) {
+ $this->assertArrayHasKey( 'test', $collection_args );
+ $this->assertEquals( 'test-arg', $collection_args['test'] );
+ return array(
+ 'post__in' => array( 111 ),
+ );
+ }
+ );
+
+ $parsed_block = $this->get_base_parsed_block();
+ $parsed_block['attrs']['collection'] = 'test-collection';
+
+ $merged_query = $this->initialize_merged_query( $parsed_block );
+
+ $this->block_instance->unregister_collection_handlers( 'test-collection' );
+
+ $this->assertContains( 111, $merged_query['post__in'] );
+ }
+
+ /**
+ * Test for editor collection handlers.
+ */
+ public function test_editor_collection_handlers() {
+ $build_query = $this->getMockBuilder( \stdClass::class )
+ ->setMethods( [ '__invoke' ] )
+ ->getMock();
+ $editor_args = $this->getMockBuilder( \stdClass::class )
+ ->setMethods( [ '__invoke' ] )
+ ->getMock();
+ $this->block_instance->register_collection_handlers( 'test-collection', $build_query, null, $editor_args );
+
+ $editor_args->expects( $this->once() )
+ ->method( '__invoke' )
+ ->willReturnCallback(
+ function ( $collection_args ) {
+ $collection_args['test'] = 'test-arg';
+ return $collection_args;
+ }
+ );
+ $build_query->expects( $this->once() )
+ ->method( '__invoke' )
+ ->willReturnCallback(
+ function ( $collection_args ) {
+ $this->assertArrayHasKey( 'test', $collection_args );
+ $this->assertEquals( 'test-arg', $collection_args['test'] );
+ return array(
+ 'post__in' => array( 111 ),
+ );
+ }
+ );
+
+ $args = array();
+ $request = $this->build_request();
+ $request->set_param(
+ 'productCollectionQueryContext',
+ array(
+ 'collection' => 'test-collection',
+ )
+ );
+
+ $updated_query = $this->block_instance->update_rest_query_in_editor( $args, $request );
+
+ $this->block_instance->unregister_collection_handlers( 'test-collection' );
+
+ $this->assertContains( 111, $updated_query['post__in'] );
+ }
+
+ /**
+ * Test for the editor preview collection handler.
+ */
+ public function test_editor_preview_collection_handler() {
+ $preview_query = $this->getMockBuilder( \stdClass::class )
+ ->setMethods( [ '__invoke' ] )
+ ->getMock();
+ $this->block_instance->register_collection_handlers(
+ 'test-collection',
+ function () {
+ return array();
+ },
+ null,
+ null,
+ $preview_query
+ );
+
+ $preview_query->expects( $this->once() )
+ ->method( '__invoke' )
+ ->willReturn(
+ array(
+ 'post__in' => array( 123 ),
+ )
+ );
+
+ $args = array();
+ $request = $this->build_request();
+ $request->set_param(
+ 'productCollectionQueryContext',
+ array(
+ 'collection' => 'test-collection',
+ 'previewState' => array(
+ 'isPreview' => 'true',
+ ),
+ )
+ );
+
+ $updated_query = $this->block_instance->update_rest_query_in_editor( $args, $request );
+
+ $this->block_instance->unregister_collection_handlers( 'test-collection' );
+
+ $this->assertContains( 123, $updated_query['post__in'] );
+ }
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Mocks/ProductCollectionMock.php b/plugins/woocommerce/tests/php/src/Blocks/Mocks/ProductCollectionMock.php
index 59e9bae0b4f..71a74e1ca40 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Mocks/ProductCollectionMock.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Mocks/ProductCollectionMock.php
@@ -9,6 +9,8 @@ use Automattic\WooCommerce\Blocks\Assets\Api;
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry;
+// phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod.Found
+
/**
* ProductCollectionMock used to test Product Query block functions.
*/
@@ -49,4 +51,26 @@ class ProductCollectionMock extends ProductCollection {
public function set_attributes_filter_query_args( $data ) {
$this->attributes_filter_query_args = $data;
}
+
+ /**
+ * Makes a protected method public so that it can be used in tests.
+ *
+ * @param string $collection_name The name of the custom collection.
+ * @param callable $build_query A hook returning any custom query arguments to merge with the collection's query.
+ * @param callable|null $frontend_args An optional hook that returns any frontend collection arguments to pass to the query builder.
+ * @param callable|null $editor_args An optional hook that returns any REST collection arguments to pass to the query builder.
+ * @param callable|null $preview_query An optional hook that returns a query to use in preview mode.
+ */
+ public function register_collection_handlers( $collection_name, $build_query, $frontend_args = null, $editor_args = null, $preview_query = null ) {
+ parent::register_collection_handlers( $collection_name, $build_query, $frontend_args, $editor_args, $preview_query );
+ }
+
+ /**
+ * Removes any custom collection handlers for the given collection.
+ *
+ * @param string $collection_name The name of the collection to unregister.
+ */
+ public function unregister_collection_handlers( $collection_name ) {
+ unset( $this->collection_handlers[ $collection_name ] );
+ }
}
From bd29f481c5d54e65587dd95c255e355f4e83e93b Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Wed, 11 Sep 2024 23:01:49 -0700
Subject: [PATCH 16/26] Removed Second Test Query Construction
As it was, `build_query_vars_from_query_block()` was triggering the
ProductCollection instance hooked into WordPress. After that, we
called `build_frontend_query()` on our test instance. This caused some
weird behavior in tests. As a result, however, the tax_query merge
test couldn't rely on the tax_query transformation done by WordPress.
---
.../Blocks/BlockTypes/ProductCollection.php | 30 +++++++++++++------
1 file changed, 21 insertions(+), 9 deletions(-)
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
index 49cf1db1a27..84a2cf12a4a 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
@@ -60,8 +60,9 @@ class ProductCollection extends \WP_UnitTestCase {
* Build the merged_query for testing
*
* @param array $parsed_block Parsed block data.
+ * @param array $query Query data.
*/
- private function initialize_merged_query( $parsed_block = array() ) {
+ private function initialize_merged_query( $parsed_block = array(), $query = array() ) {
if ( empty( $parsed_block ) ) {
$parsed_block = $this->get_base_parsed_block();
}
@@ -71,8 +72,6 @@ class ProductCollection extends \WP_UnitTestCase {
$block = new \stdClass();
$block->context = $parsed_block['attrs'];
- $query = build_query_vars_from_query_block( $block, 1 );
-
return $this->block_instance->build_frontend_query( $query, $block, 1 );
}
@@ -598,14 +597,27 @@ class ProductCollection extends \WP_UnitTestCase {
* - Product tags
*/
public function test_merging_taxonomies_query() {
- $parsed_block = $this->get_base_parsed_block();
- $parsed_block['attrs']['query']['taxQuery'] = array(
- 'product_cat' => array( 1, 2 ),
- 'product_tag' => array( 3, 4 ),
+ $merged_query = $this->initialize_merged_query(
+ null,
+ // Since we aren't calling the Query Loop build function, we need to provide
+ // a tax_query rather than relying on it generating one from the input.
+ array(
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
+ 'tax_query' => array(
+ array(
+ 'taxonomy' => 'product_cat',
+ 'terms' => array( 1, 2 ),
+ 'include_children' => false,
+ ),
+ array(
+ 'taxonomy' => 'product_tag',
+ 'terms' => array( 3, 4 ),
+ 'include_children' => false,
+ ),
+ ),
+ )
);
- $merged_query = $this->initialize_merged_query( $parsed_block );
-
$this->assertContains(
array(
'taxonomy' => 'product_cat',
From 9926d9d20b1efe98c47d260a6e8cabd9ffdddf1c Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Wed, 11 Sep 2024 23:02:19 -0700
Subject: [PATCH 17/26] Added Related Product Collection Handlers
---
.../Blocks/BlockTypes/ProductCollection.php | 89 +++++++++++++++---
.../Blocks/BlockTypes/ProductCollection.php | 91 +++++++++++++++++++
.../Blocks/Mocks/ProductCollectionMock.php | 4 +-
3 files changed, 171 insertions(+), 13 deletions(-)
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
index 652609bf4e9..1efe9ceecd7 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
@@ -3,6 +3,7 @@
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils;
+use InvalidArgumentException;
use WP_Query;
use WC_Tax;
@@ -130,6 +131,8 @@ class ProductCollection extends AbstractBlock {
// Disable client-side-navigation if incompatible blocks are detected.
add_filter( 'render_block_data', array( $this, 'disable_enhanced_pagination' ), 10, 1 );
+
+ $this->register_core_collections();
}
/**
@@ -636,13 +639,16 @@ class ProductCollection extends AbstractBlock {
$product_collection_query_context = $request->get_param( 'productCollectionQueryContext' );
$collection_args = array(
- 'name' => $product_collection_query_context['collection'] ?? '',
+ 'name' => $product_collection_query_context['collection'] ?? '',
+ // The editor uses a REST query to grab product post types. This means we don't have a block
+ // instance to work with and the client needs to provide the location context.
+ 'productCollectionLocation' => $request->get_param( 'productCollectionLocation' ),
);
// Allow collections to modify the collection arguments passed to the query builder.
$handlers = $this->collection_handler_store[ $collection_args['name'] ] ?? null;
if ( isset( $handlers['editor_args'] ) ) {
- $collection_args = call_user_func( $handlers['editor_args'], $collection_args, $query );
+ $collection_args = call_user_func( $handlers['editor_args'], $collection_args, $query, $request );
}
// Is this a preview mode request?
@@ -736,18 +742,29 @@ class ProductCollection extends AbstractBlock {
$is_exclude_applied_filters = ! ( $inherit || $filterable );
- return $this->get_final_frontend_query( $block_context_query, $page, $is_exclude_applied_filters );
+ $collection_args = array(
+ 'name' => $block->context['collection'] ?? '',
+ 'productCollectionLocation' => $block->context['productCollectionLocation'] ?? null,
+ );
+
+ return $this->get_final_frontend_query(
+ $collection_args,
+ $block_context_query,
+ $page,
+ $is_exclude_applied_filters
+ );
}
/**
* Get the final query arguments for the frontend.
*
- * @param array $query The query arguments.
- * @param int $page The page number.
+ * @param array $collection_args Any special arguments that should change the behavior of the query.
+ * @param array $query The query arguments.
+ * @param int $page The page number.
* @param bool $is_exclude_applied_filters Whether to exclude the applied filters or not.
*/
- private function get_final_frontend_query( $query, $page = 1, $is_exclude_applied_filters = false ) {
+ private function get_final_frontend_query( $collection_args, $query, $page = 1, $is_exclude_applied_filters = false ) {
$product_ids = $query['post__in'] ?? array();
$offset = $query['offset'] ?? 0;
$per_page = $query['perPage'] ?? 9;
@@ -774,10 +791,6 @@ class ProductCollection extends AbstractBlock {
$time_frame = $query['timeFrame'] ?? null;
$price_range = $query['priceRange'] ?? null;
- $collection_args = array(
- 'name' => $this->parsed_block['attrs']['collection'] ?? '',
- );
-
// Allow collections to modify the collection arguments passed to the query builder.
$handlers = $this->collection_handler_store[ $collection_args['name'] ] ?? null;
if ( isset( $handlers['frontend_args'] ) ) {
@@ -847,7 +860,7 @@ class ProductCollection extends AbstractBlock {
$collection_query = array();
}
- return $this->merge_queries(
+ $merged = $this->merge_queries(
$common_query_values,
$orderby_query,
$on_sale_query,
@@ -859,6 +872,8 @@ class ProductCollection extends AbstractBlock {
$handpicked_query,
$collection_query
);
+
+ return $merged;
}
/**
@@ -1799,4 +1814,56 @@ class ProductCollection extends AbstractBlock {
'preview_query' => $preview_query,
);
}
+
+ /**
+ * Registers any handlers for the core collections.
+ */
+ protected function register_core_collections() {
+ $this->register_collection_handlers(
+ 'woocommerce/product-collection/related',
+ function ( $collection_args ) {
+ // No products should be shown if no related product reference is set.
+ if ( empty( $collection_args['relatedProductReference'] ) ) {
+ return array(
+ 'post__in' => array( -1 ),
+ );
+ }
+
+ // Have it filter the results to products related to the one provided.
+ return array(
+ 'post__in' => wc_get_related_products(
+ $collection_args['relatedProductReference'],
+ // Use a higher limit so that the result set contains enough products for the collection to subsequently filter.
+ 100
+ ),
+ );
+ },
+ function ( $collection_args, $query ) {
+ $product_reference = $query['productReference'] ?? null;
+ // Infer the product reference from the location if an explicit product is not set.
+ if ( empty( $product_reference ) ) {
+ $location = $collection_args['productCollectionLocation'];
+ if ( isset( $location['type'] ) && 'product' === $location['type'] ) {
+ $product_reference = $location['sourceData']['productId'];
+ }
+ }
+
+ $collection_args['relatedProductReference'] = $product_reference;
+ return $collection_args;
+ },
+ function ( $collection_args, $query, $request ) {
+ $product_reference = $request->get_param( 'productReference' );
+ // In some cases the editor will send along block location context that we can infer the product reference from.
+ if ( empty( $product_reference ) ) {
+ $location = $collection_args['productCollectionLocation'];
+ if ( isset( $location['type'] ) && 'product' === $location['type'] ) {
+ $product_reference = $location['sourceData']['productId'];
+ }
+ }
+
+ $collection_args['relatedProductReference'] = $product_reference;
+ return $collection_args;
+ }
+ );
+ }
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
index 84a2cf12a4a..77946863ff1 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
@@ -1127,4 +1127,95 @@ class ProductCollection extends \WP_UnitTestCase {
$this->assertContains( 123, $updated_query['post__in'] );
}
+
+ /**
+ * Provides the test input and expected output for the collection handler tests.
+ */
+ public function core_collection_handler_test_provider() {
+ $related_filter = $this->getMockBuilder( \stdClass::class )
+ ->setMethods( [ '__invoke' ] )
+ ->getMock();
+
+ return array(
+ array(
+ 'woocommerce/product-collection/related',
+ function () use ( $related_filter ) {
+ // This filter will turn off the data store so we don't need dummy products.
+ add_filter( 'woocommerce_product_related_posts_force_display', '__return_true', 0 );
+ $related_filter->expects( $this->exactly( 2 ) )
+ ->method( '__invoke' )
+ ->with( array(), 1 )
+ ->willReturn( array( 2, 3, 4 ) );
+ add_filter( 'woocommerce_related_products', array( $related_filter, '__invoke' ), 10, 2 );
+ },
+ function () use ( $related_filter ) {
+ remove_filter( 'woocommerce_product_related_posts_force_display', '__return_true', 0 );
+ remove_filter( 'woocommerce_related_products', array( $related_filter, '__invoke' ) );
+ },
+ array(
+ 'productReference' => 1,
+ ),
+ array(
+ 'productReference' => 1,
+ ),
+ array(
+ 'post__in' => array( 2, 3, 4 ),
+ ),
+ ),
+ );
+ }
+
+ /**
+ * Tests that the core collection handlers behave correctly.
+ *
+ * @dataProvider core_collection_handler_test_provider
+ *
+ * @param string $collection_name The name of the collection under test.
+ * @param callable|null $set_up An optional function to call before the test.
+ * @param callable|null $tear_down An optional function to call after the test.
+ * @param array $frontend_query The query arguments to use for the frontend queries.
+ * @param array $editor_query The query arguments to use for the editor queries.
+ * @param array $expected_query Any query arguments we expect to be present.
+ */
+ public function test_core_collection_handlers(
+ $collection_name,
+ $set_up,
+ $tear_down,
+ $frontend_query,
+ $editor_query,
+ $expected_query
+ ) {
+ if ( isset( $set_up ) ) {
+ $set_up();
+ }
+
+ // Frontend.
+ $parsed_block = $this->get_base_parsed_block();
+ $parsed_block['attrs']['collection'] = $collection_name;
+ foreach ( $frontend_query as $key => $value ) {
+ $parsed_block['attrs']['query'][ $key ] = $value;
+ }
+ $result_frontend = $this->initialize_merged_query( $parsed_block );
+
+ // Editor.
+ $request = $this->build_request( $editor_query );
+ $request->set_param(
+ 'productCollectionQueryContext',
+ array(
+ 'collection' => $collection_name,
+ )
+ );
+ $result_editor = $this->block_instance->update_rest_query_in_editor( array(), $request );
+
+ if ( isset( $tear_down ) ) {
+ $tear_down();
+ }
+
+ foreach ( $expected_query as $key => $value ) {
+ $this->assertEqualsCanonicalizing( $value, $result_frontend[ $key ] );
+ }
+ foreach ( $expected_query as $key => $value ) {
+ $this->assertEqualsCanonicalizing( $value, $result_editor[ $key ] );
+ }
+ }
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Mocks/ProductCollectionMock.php b/plugins/woocommerce/tests/php/src/Blocks/Mocks/ProductCollectionMock.php
index 71a74e1ca40..1f9c95582b6 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Mocks/ProductCollectionMock.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Mocks/ProductCollectionMock.php
@@ -28,10 +28,10 @@ class ProductCollectionMock extends ProductCollection {
}
/**
- * For now don't need to initialize anything in tests so let's
- * just override the default behaviour.
+ * Override the normal initialization behavior to prevent registering the block with WordPress filters.
*/
protected function initialize() {
+ $this->register_core_collections();
}
/**
From 3c5b47e2b7ee11fec4fbc745c44f660461d4dec9 Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Wed, 11 Sep 2024 23:15:03 -0700
Subject: [PATCH 18/26] Revert "Support Unlimited `wc_get_related_products()`"
This reverts commit 41c837202ebad27031b82f4227cba0b307bc7905.
---
.../class-wc-product-data-store-cpt.php | 24 +--
.../class-wc-product-data-store-interface.php | 2 +-
.../includes/wc-product-functions.php | 20 +--
.../includes/wc-product-functions-test.php | 147 ------------------
4 files changed, 14 insertions(+), 179 deletions(-)
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
index 68234967684..b5a611d6f8a 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
@@ -1453,7 +1453,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
* @param array $cats_array List of categories IDs.
* @param array $tags_array List of tags IDs.
* @param array $exclude_ids Excluded IDs.
- * @param int $limit Limit of results, -1 for no limit.
+ * @param int $limit Limit of results.
* @param int $product_id Product ID.
* @return array
*/
@@ -1464,20 +1464,13 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
'categories' => $cats_array,
'tags' => $tags_array,
'exclude_ids' => $exclude_ids,
- 'limit' => $limit,
+ 'limit' => $limit + 10,
);
- $related_product_query = (array) apply_filters(
- 'woocommerce_product_related_posts_query',
- $this->get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit ),
- $product_id,
- $args
- );
+ $related_product_query = (array) apply_filters( 'woocommerce_product_related_posts_query', $this->get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit + 10 ), $product_id, $args );
// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
- $products = $wpdb->get_col( implode( ' ', $related_product_query ) );
-
- return array_map( 'intval', $products );
+ return $wpdb->get_col( implode( ' ', $related_product_query ) );
}
/**
@@ -1488,7 +1481,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
* @param array $cats_array List of categories IDs.
* @param array $tags_array List of tags IDs.
* @param array $exclude_ids Excluded IDs.
- * @param int $limit Limit of results, -1 for no limit.
+ * @param int $limit Limit of results.
*
* @return array
*/
@@ -1518,12 +1511,11 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
AND p.post_type = 'product'
",
+ 'limits' => '
+ LIMIT ' . absint( $limit ) . '
+ ',
);
- if ( $limit > 0 ) {
- $query['limits'] = ' LIMIT ' . absint( $limit );
- }
-
if ( count( $exclude_term_ids ) ) {
$query['join'] .= " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = p.ID';
$query['where'] .= ' AND exclude_join.object_id IS NULL';
diff --git a/plugins/woocommerce/includes/interfaces/class-wc-product-data-store-interface.php b/plugins/woocommerce/includes/interfaces/class-wc-product-data-store-interface.php
index 4199fefa857..ed9244ae56a 100644
--- a/plugins/woocommerce/includes/interfaces/class-wc-product-data-store-interface.php
+++ b/plugins/woocommerce/includes/interfaces/class-wc-product-data-store-interface.php
@@ -86,7 +86,7 @@ interface WC_Product_Data_Store_Interface {
* @param array $cats_array List of categories IDs.
* @param array $tags_array List of tags IDs.
* @param array $exclude_ids Excluded IDs.
- * @param int $limit Limit of results, -1 for no limit.
+ * @param int $limit Limit of results.
* @param int $product_id Product ID.
* @return array
*/
diff --git a/plugins/woocommerce/includes/wc-product-functions.php b/plugins/woocommerce/includes/wc-product-functions.php
index 7f2450df9f5..7232ef7ccc7 100644
--- a/plugins/woocommerce/includes/wc-product-functions.php
+++ b/plugins/woocommerce/includes/wc-product-functions.php
@@ -976,15 +976,14 @@ function wc_get_product_backorder_options() {
*
* @since 3.0.0
* @param int $product_id Product ID.
- * @param int $limit Limit of results, -1 for no limit.
+ * @param int $limit Limit of results.
* @param array $exclude_ids Exclude IDs from the results.
* @return array
*/
function wc_get_related_products( $product_id, $limit = 5, $exclude_ids = array() ) {
$product_id = absint( $product_id );
- $limit = intval( $limit );
- $limit = $limit < -1 ? -1 : $limit; // Any negative number will default to no limit.
+ $limit = $limit >= -1 ? $limit : 5;
$exclude_ids = array_merge( array( 0, $product_id ), $exclude_ids );
$transient_name = 'wc_related_' . $product_id;
$query_args = http_build_query(
@@ -998,7 +997,7 @@ function wc_get_related_products( $product_id, $limit = 5, $exclude_ids = array(
$related_posts = $transient && is_array( $transient ) && isset( $transient[ $query_args ] ) ? $transient[ $query_args ] : false;
// We want to query related posts if they are not cached, or we don't have enough.
- if ( false === $related_posts || ( $limit > 0 && count( $related_posts ) < $limit ) ) {
+ if ( false === $related_posts || count( $related_posts ) < $limit ) {
$cats_array = apply_filters( 'woocommerce_product_related_posts_relate_by_category', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_cat_terms', wc_get_product_term_ids( $product_id, 'product_cat' ), $product_id ) : array();
$tags_array = apply_filters( 'woocommerce_product_related_posts_relate_by_tag', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_tag_terms', wc_get_product_term_ids( $product_id, 'product_tag' ), $product_id ) : array();
@@ -1007,13 +1006,8 @@ function wc_get_related_products( $product_id, $limit = 5, $exclude_ids = array(
if ( empty( $cats_array ) && empty( $tags_array ) && ! apply_filters( 'woocommerce_product_related_posts_force_display', false, $product_id ) ) {
$related_posts = array();
} else {
- // For backward compatibility we need to grab 10 extra products. This was done so that when we shuffle the
- // output below it will appear to be random by including different products rather than the same ones
- // in a different order.
- $query_limit = $limit > 0 ? $limit + 10 : -1;
-
$data_store = WC_Data_Store::load( 'product' );
- $related_posts = $data_store->get_related_products( $cats_array, $tags_array, $exclude_ids, $query_limit, $product_id );
+ $related_posts = $data_store->get_related_products( $cats_array, $tags_array, $exclude_ids, $limit + 10, $product_id );
}
if ( $transient && is_array( $transient ) ) {
@@ -1039,11 +1033,7 @@ function wc_get_related_products( $product_id, $limit = 5, $exclude_ids = array(
shuffle( $related_posts );
}
- if ( $limit > 0 ) {
- return array_slice( $related_posts, 0, $limit );
- }
-
- return $related_posts;
+ return array_slice( $related_posts, 0, $limit );
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php
index 30f4b47b980..247c74e4fe7 100644
--- a/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php
@@ -240,151 +240,4 @@ class WC_Product_Functions_Tests extends \WC_Unit_Test_Case {
$this->assertEquals( 100, wc_get_product( $product->get_id() )->get_price() );
}
-
- /**
- * @testDox Products related by tag or category should be returned.
- */
- public function test_wc_get_related_products() {
- add_filter( 'woocommerce_product_related_posts_relate_by_category', '__return_true', 100 );
- add_filter( 'woocommerce_product_related_posts_relate_by_tag', '__return_true', 100 );
-
- $related_tag = wp_insert_term( 'Related Tag', 'product_tag' );
- $related_cat = wp_insert_term( 'Related Category', 'product_cat' );
-
- $product = WC_Helper_Product::create_simple_product();
- $product->set_tag_ids( array( $related_tag['term_id'] ) );
- $product->set_category_ids( array( $related_cat['term_id'] ) );
- $product->save();
-
- $related_product_1 = WC_Helper_Product::create_simple_product();
- $related_product_1->set_category_ids( array( $related_cat['term_id'] ) );
- $related_product_1->save();
-
- $related_product_2 = WC_Helper_Product::create_simple_product();
- $related_product_2->set_tag_ids( array( $related_tag['term_id'] ) );
- $related_product_2->save();
-
- $related = wc_get_related_products( $product->get_id() );
-
- remove_filter( 'woocommerce_product_related_posts_relate_by_category', '__return_true', 100 );
- remove_filter( 'woocommerce_product_related_posts_relate_by_tag', '__return_true', 100 );
-
- $this->assertEquals( 2, count( $related ) );
- $this->assertContains( $related_product_1->get_id(), $related );
- $this->assertContains( $related_product_2->get_id(), $related );
- }
-
- /**
- * @testDox Only products related by tag should be returned when the appropriate filters are set.
- */
- public function test_wc_get_related_products_by_tag() {
- add_filter( 'woocommerce_product_related_posts_relate_by_category', '__return_false', 100 );
- add_filter( 'woocommerce_product_related_posts_relate_by_tag', '__return_true', 100 );
-
- $related_tag = wp_insert_term( 'Related Tag', 'product_tag' );
- $related_cat = wp_insert_term( 'Related Category', 'product_cat' );
-
- $product = WC_Helper_Product::create_simple_product();
- $product->set_tag_ids( array( $related_tag['term_id'] ) );
- $product->set_category_ids( array( $related_cat['term_id'] ) );
- $product->save();
-
- $related_product_1 = WC_Helper_Product::create_simple_product();
- $related_product_1->set_tag_ids( array( $related_tag['term_id'] ) );
- $related_product_1->set_category_ids( array( $related_cat['term_id'] ) );
- $related_product_1->save();
-
- $related_product_2 = WC_Helper_Product::create_simple_product();
- $related_product_2->set_tag_ids( array( $related_tag['term_id'] ) );
- $related_product_2->set_category_ids( array( $related_cat['term_id'] ) );
- $related_product_2->save();
-
- $unrelated_product = WC_Helper_Product::create_simple_product();
- $unrelated_product->set_category_ids( array( $related_cat['term_id'] ) );
- $unrelated_product->save();
-
- $related = wc_get_related_products( $product->get_id() );
-
- remove_filter( 'woocommerce_product_related_posts_relate_by_category', '__return_false', 100 );
- remove_filter( 'woocommerce_product_related_posts_relate_by_tag', '__return_true', 100 );
-
- $this->assertEquals( 2, count( $related ) );
- $this->assertContains( $related_product_1->get_id(), $related );
- $this->assertContains( $related_product_2->get_id(), $related );
- }
-
- /**
- * @testDox Only products related by category should be returned when the appropriate filters are set.
- */
- public function test_wc_get_related_products_by_category() {
- add_filter( 'woocommerce_product_related_posts_relate_by_category', '__return_true', 100 );
- add_filter( 'woocommerce_product_related_posts_relate_by_tag', '__return_false', 100 );
-
- $related_tag = wp_insert_term( 'Related Tag', 'product_tag' );
- $related_cat = wp_insert_term( 'Related Category', 'product_cat' );
-
- $product = WC_Helper_Product::create_simple_product();
- $product->set_tag_ids( array( $related_tag['term_id'] ) );
- $product->set_category_ids( array( $related_cat['term_id'] ) );
- $product->save();
-
- $related_product_1 = WC_Helper_Product::create_simple_product();
- $related_product_1->set_tag_ids( array( $related_tag['term_id'] ) );
- $related_product_1->set_category_ids( array( $related_cat['term_id'] ) );
- $related_product_1->save();
-
- $related_product_2 = WC_Helper_Product::create_simple_product();
- $related_product_2->set_tag_ids( array( $related_tag['term_id'] ) );
- $related_product_2->set_category_ids( array( $related_cat['term_id'] ) );
- $related_product_2->save();
-
- $unrelated_product = WC_Helper_Product::create_simple_product();
- $unrelated_product->set_tag_ids( array( $related_tag['term_id'] ) );
- $unrelated_product->save();
-
- $related = wc_get_related_products( $product->get_id() );
-
- remove_filter( 'woocommerce_product_related_posts_relate_by_category', '__return_true', 100 );
- remove_filter( 'woocommerce_product_related_posts_relate_by_tag', '__return_false', 100 );
-
- $this->assertEquals( 2, count( $related ) );
- $this->assertContains( $related_product_1->get_id(), $related );
- $this->assertContains( $related_product_2->get_id(), $related );
- }
-
- /**
- * @testDox Products related by tag or category should apply a limit to the results.
- */
- public function test_wc_get_related_products_limit() {
- add_filter( 'woocommerce_product_related_posts_relate_by_category', '__return_true', 100 );
- add_filter( 'woocommerce_product_related_posts_relate_by_tag', '__return_true', 100 );
-
- $related_tag = wp_insert_term( 'Related Tag', 'product_tag' );
- $related_cat = wp_insert_term( 'Related Category', 'product_cat' );
-
- $product = WC_Helper_Product::create_simple_product();
- $product->set_tag_ids( array( $related_tag['term_id'] ) );
- $product->set_category_ids( array( $related_cat['term_id'] ) );
- $product->save();
-
- $related_product_1 = WC_Helper_Product::create_simple_product();
- $related_product_1->set_category_ids( array( $related_cat['term_id'] ) );
- $related_product_1->save();
-
- $related_product_2 = WC_Helper_Product::create_simple_product();
- $related_product_2->set_tag_ids( array( $related_tag['term_id'] ) );
- $related_product_2->save();
-
- $related = wc_get_related_products( $product->get_id(), 1 );
- $unlimited_related = wc_get_related_products( $product->get_id(), -1 );
-
- remove_filter( 'woocommerce_product_related_posts_relate_by_category', '__return_true', 100 );
- remove_filter( 'woocommerce_product_related_posts_relate_by_tag', '__return_true', 100 );
-
- $this->assertEquals( 1, count( $related ) );
- $this->assertContains( $related[0], array( $related_product_1->get_id(), $related_product_2->get_id() ) );
- $this->assertEquals( 2, count( $unlimited_related ) );
- $this->assertContains( $related_product_1->get_id(), $unlimited_related );
- $this->assertContains( $related_product_2->get_id(), $unlimited_related );
- }
}
From cea25124123b9bb99f011717376e862977fbc89a Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Wed, 11 Sep 2024 23:47:14 -0700
Subject: [PATCH 19/26] Fixed Preview Mode Query
It looks like we were checking the wrong place for the preview
state.
---
.../src/Blocks/BlockTypes/ProductCollection.php | 11 ++++-------
1 file changed, 4 insertions(+), 7 deletions(-)
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
index 1efe9ceecd7..2a9b7660fe2 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
@@ -651,10 +651,9 @@ class ProductCollection extends AbstractBlock {
$collection_args = call_user_func( $handlers['editor_args'], $collection_args, $query, $request );
}
- // Is this a preview mode request?
- // If yes, short-circuit the query and return the preview query args.
- $is_preview = $product_collection_query_context['previewState']['isPreview'] ?? false;
- if ( 'true' === $is_preview ) {
+ // When requested, short-circuit the query and return the preview query args.
+ $preview_state = $request->get_param( 'previewState' );
+ if ( isset( $preview_state['isPreview'] ) && 'true' === $preview_state['isPreview'] ) {
return $this->get_preview_query_args( $collection_args, $query, $request );
}
@@ -860,7 +859,7 @@ class ProductCollection extends AbstractBlock {
$collection_query = array();
}
- $merged = $this->merge_queries(
+ return $this->merge_queries(
$common_query_values,
$orderby_query,
$on_sale_query,
@@ -872,8 +871,6 @@ class ProductCollection extends AbstractBlock {
$handpicked_query,
$collection_query
);
-
- return $merged;
}
/**
From c35a64721e2c79dcdb17ba060da5a5a0ea09c8bb Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Thu, 12 Sep 2024 06:54:54 -0700
Subject: [PATCH 20/26] Fixed Test
---
.../php/src/Blocks/BlockTypes/ProductCollection.php | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
index 77946863ff1..6430a680b1f 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
@@ -1114,10 +1114,13 @@ class ProductCollection extends \WP_UnitTestCase {
$request->set_param(
'productCollectionQueryContext',
array(
- 'collection' => 'test-collection',
- 'previewState' => array(
- 'isPreview' => 'true',
- ),
+ 'collection' => 'test-collection',
+ )
+ );
+ $request->set_param(
+ 'previewState',
+ array(
+ 'isPreview' => 'true',
)
);
From 750e4723a101776f2a48329d534eb61c37531189 Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Thu, 12 Sep 2024 09:49:26 -0700
Subject: [PATCH 21/26] Updated Collection Heading
---
.../assets/js/blocks/product-collection/collections/related.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related.tsx
index ec530da03dc..3de8cd441e5 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/related.tsx
@@ -38,7 +38,7 @@ const heading: InnerBlockTemplate = [
{
textAlign: 'center',
level: 2,
- content: __( 'You may also like', 'woocommerce' ),
+ content: __( 'Related Products', 'woocommerce' ),
style: { spacing: { margin: { bottom: '1rem' } } },
},
];
From 32449f5fd0d3c2b896e69024c5c599725918ccc6 Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Mon, 16 Sep 2024 13:06:16 -0700
Subject: [PATCH 22/26] Better Collection Test Teardown
Passing a callable back means that we can use variables in the
setUp and then the tearDown without any confusion.
---
.../Blocks/BlockTypes/ProductCollection.php | 30 +++++++++----------
1 file changed, 15 insertions(+), 15 deletions(-)
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
index 6430a680b1f..a1386defe53 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
@@ -1135,14 +1135,14 @@ class ProductCollection extends \WP_UnitTestCase {
* Provides the test input and expected output for the collection handler tests.
*/
public function core_collection_handler_test_provider() {
- $related_filter = $this->getMockBuilder( \stdClass::class )
- ->setMethods( [ '__invoke' ] )
- ->getMock();
-
return array(
array(
'woocommerce/product-collection/related',
- function () use ( $related_filter ) {
+ function () {
+ $related_filter = $this->getMockBuilder( \stdClass::class )
+ ->setMethods( [ '__invoke' ] )
+ ->getMock();
+
// This filter will turn off the data store so we don't need dummy products.
add_filter( 'woocommerce_product_related_posts_force_display', '__return_true', 0 );
$related_filter->expects( $this->exactly( 2 ) )
@@ -1150,10 +1150,11 @@ class ProductCollection extends \WP_UnitTestCase {
->with( array(), 1 )
->willReturn( array( 2, 3, 4 ) );
add_filter( 'woocommerce_related_products', array( $related_filter, '__invoke' ), 10, 2 );
- },
- function () use ( $related_filter ) {
- remove_filter( 'woocommerce_product_related_posts_force_display', '__return_true', 0 );
- remove_filter( 'woocommerce_related_products', array( $related_filter, '__invoke' ) );
+
+ return function () use ( $related_filter ) {
+ remove_filter( 'woocommerce_product_related_posts_force_display', '__return_true', 0 );
+ remove_filter( 'woocommerce_related_products', array( $related_filter, '__invoke' ) );
+ };
},
array(
'productReference' => 1,
@@ -1174,8 +1175,7 @@ class ProductCollection extends \WP_UnitTestCase {
* @dataProvider core_collection_handler_test_provider
*
* @param string $collection_name The name of the collection under test.
- * @param callable|null $set_up An optional function to call before the test.
- * @param callable|null $tear_down An optional function to call after the test.
+ * @param callable|null $set_up An optional function to call before the test. This function can return a callable to clean up after the test.
* @param array $frontend_query The query arguments to use for the frontend queries.
* @param array $editor_query The query arguments to use for the editor queries.
* @param array $expected_query Any query arguments we expect to be present.
@@ -1183,13 +1183,13 @@ class ProductCollection extends \WP_UnitTestCase {
public function test_core_collection_handlers(
$collection_name,
$set_up,
- $tear_down,
$frontend_query,
$editor_query,
$expected_query
) {
+ $tear_down = null;
if ( isset( $set_up ) ) {
- $set_up();
+ $tear_down = call_user_func( $set_up );
}
// Frontend.
@@ -1210,8 +1210,8 @@ class ProductCollection extends \WP_UnitTestCase {
);
$result_editor = $this->block_instance->update_rest_query_in_editor( array(), $request );
- if ( isset( $tear_down ) ) {
- $tear_down();
+ if ( is_callable( $tear_down ) ) {
+ call_user_func( $tear_down );
}
foreach ( $expected_query as $key => $value ) {
From 1c055f4d282bd80af44a489945e6cbfdc7a76981 Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Mon, 16 Sep 2024 13:24:56 -0700
Subject: [PATCH 23/26] Removed Unnecessary Test Provider
---
.../Blocks/BlockTypes/ProductCollection.php | 98 +++++--------------
1 file changed, 25 insertions(+), 73 deletions(-)
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
index a1386defe53..3f0bf143ed5 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
@@ -1132,93 +1132,45 @@ class ProductCollection extends \WP_UnitTestCase {
}
/**
- * Provides the test input and expected output for the collection handler tests.
+ * Tests that the related products collection handler works as expected.
*/
- public function core_collection_handler_test_provider() {
- return array(
- array(
- 'woocommerce/product-collection/related',
- function () {
- $related_filter = $this->getMockBuilder( \stdClass::class )
- ->setMethods( [ '__invoke' ] )
- ->getMock();
+ public function test_collection_related_products() {
+ $related_filter = $this->getMockBuilder( \stdClass::class )
+ ->setMethods( [ '__invoke' ] )
+ ->getMock();
- // This filter will turn off the data store so we don't need dummy products.
- add_filter( 'woocommerce_product_related_posts_force_display', '__return_true', 0 );
- $related_filter->expects( $this->exactly( 2 ) )
- ->method( '__invoke' )
- ->with( array(), 1 )
- ->willReturn( array( 2, 3, 4 ) );
- add_filter( 'woocommerce_related_products', array( $related_filter, '__invoke' ), 10, 2 );
+ $expected_product_ids = array( 2, 3, 4 );
- return function () use ( $related_filter ) {
- remove_filter( 'woocommerce_product_related_posts_force_display', '__return_true', 0 );
- remove_filter( 'woocommerce_related_products', array( $related_filter, '__invoke' ) );
- };
- },
- array(
- 'productReference' => 1,
- ),
- array(
- 'productReference' => 1,
- ),
- array(
- 'post__in' => array( 2, 3, 4 ),
- ),
- ),
- );
- }
-
- /**
- * Tests that the core collection handlers behave correctly.
- *
- * @dataProvider core_collection_handler_test_provider
- *
- * @param string $collection_name The name of the collection under test.
- * @param callable|null $set_up An optional function to call before the test. This function can return a callable to clean up after the test.
- * @param array $frontend_query The query arguments to use for the frontend queries.
- * @param array $editor_query The query arguments to use for the editor queries.
- * @param array $expected_query Any query arguments we expect to be present.
- */
- public function test_core_collection_handlers(
- $collection_name,
- $set_up,
- $frontend_query,
- $editor_query,
- $expected_query
- ) {
- $tear_down = null;
- if ( isset( $set_up ) ) {
- $tear_down = call_user_func( $set_up );
- }
+ // This filter will turn off the data store so we don't need dummy products.
+ add_filter( 'woocommerce_product_related_posts_force_display', '__return_true', 0 );
+ $related_filter->expects( $this->exactly( 2 ) )
+ ->method( '__invoke' )
+ ->with( array(), 1 )
+ ->willReturn( $expected_product_ids );
+ add_filter( 'woocommerce_related_products', array( $related_filter, '__invoke' ), 10, 2 );
// Frontend.
- $parsed_block = $this->get_base_parsed_block();
- $parsed_block['attrs']['collection'] = $collection_name;
- foreach ( $frontend_query as $key => $value ) {
- $parsed_block['attrs']['query'][ $key ] = $value;
- }
- $result_frontend = $this->initialize_merged_query( $parsed_block );
+ $parsed_block = $this->get_base_parsed_block();
+ $parsed_block['attrs']['collection'] = 'woocommerce/product-collection/related';
+ $parsed_block['attrs']['query']['productReference'] = 1;
+ $result_frontend = $this->initialize_merged_query( $parsed_block );
// Editor.
- $request = $this->build_request( $editor_query );
+ $request = $this->build_request(
+ array( 'productReference' => 1 )
+ );
$request->set_param(
'productCollectionQueryContext',
array(
- 'collection' => $collection_name,
+ 'collection' => 'woocommerce/product-collection/related',
)
);
$result_editor = $this->block_instance->update_rest_query_in_editor( array(), $request );
- if ( is_callable( $tear_down ) ) {
- call_user_func( $tear_down );
- }
+ remove_filter( 'woocommerce_product_related_posts_force_display', '__return_true', 0 );
+ remove_filter( 'woocommerce_related_products', array( $related_filter, '__invoke' ) );
- foreach ( $expected_query as $key => $value ) {
- $this->assertEqualsCanonicalizing( $value, $result_frontend[ $key ] );
- }
- foreach ( $expected_query as $key => $value ) {
- $this->assertEqualsCanonicalizing( $value, $result_editor[ $key ] );
- }
+ $this->assertEqualsCanonicalizing( $expected_product_ids, $result_frontend['post__in'] );
+ $this->assertEqualsCanonicalizing( $expected_product_ids, $result_editor['post__in'] );
}
}
From 1fa3ea32d585af84c7ec60cb15381eb53bcda1fd Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Mon, 16 Sep 2024 22:12:16 -0700
Subject: [PATCH 24/26] Fixed `$post__in` Merging
When the intersection is empty it was returning all products. This
makes it so that it returns nothing when there's no results.
---
.../Blocks/BlockTypes/ProductCollection.php | 5 +++++
.../Blocks/BlockTypes/ProductCollection.php | 18 +++++++++++++++++-
2 files changed, 22 insertions(+), 1 deletion(-)
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
index 72285fd79d1..66618f333f3 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
@@ -990,6 +990,11 @@ class ProductCollection extends AbstractBlock {
// has otherwise excluded from the results.
if ( count( $post__in ) > 1 ) {
$post__in = array_intersect( ...$post__in );
+ // An empty array means that there was no overlap between the filters and so
+ // the query should return no results.
+ if ( empty( $post__in ) ) {
+ return array( -1 );
+ }
} else {
$post__in = reset( $post__in );
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
index 3f0bf143ed5..f3ee91ab08a 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection.php
@@ -975,7 +975,7 @@ class ProductCollection extends \WP_UnitTestCase {
/**
* Test merging exclusive id filters.
*/
- public function test_merges_exclusive_id_filters() {
+ public function test_merges_post__in() {
$existing_id_filter = array( 1, 4 );
$handpicked_product_ids = array( 3, 4, 5, 6 );
// The only ID present in ALL of the exclusive filters is 4.
@@ -994,6 +994,22 @@ class ProductCollection extends \WP_UnitTestCase {
$this->assertCount( 1, $merged_query['post__in'] );
}
+ /**
+ * Test merging exclusive id filters with no intersection.
+ */
+ public function test_merges_post__in_empty_result_without_intersection() {
+ $existing_id_filter = array( 1, 4 );
+ $handpicked_product_ids = array( 2, 3 );
+
+ $parsed_block = $this->get_base_parsed_block();
+ $parsed_block['attrs']['query']['post__in'] = $existing_id_filter;
+ $parsed_block['attrs']['query']['woocommerceHandPickedProducts'] = $handpicked_product_ids;
+
+ $merged_query = $this->initialize_merged_query( $parsed_block );
+
+ $this->assertEquals( array( -1 ), $merged_query['post__in'] );
+ }
+
/**
* Test for frontend collection handlers.
*/
From f7a83ee05360a098b5b0cccbf491dc655d403c2c Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Wed, 18 Sep 2024 07:12:32 -0700
Subject: [PATCH 25/26] Fixed PHPDoc
Co-authored-by: Manish Menaria
---
.../src/Blocks/BlockTypes/ProductCollection.php | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
index 66618f333f3..502178a051d 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
@@ -19,10 +19,11 @@ class ProductCollection extends AbstractBlock {
*/
protected $block_name = 'product-collection';
- /**
- * An array keyed by the name of the collection containing handlers for implementing custom collection behavior.
+ /**
+ * An associative array of collection handlers.
*
- * @var array
+ * @var array $collection_handler_store
+ * Keys are collection names, values are callable handlers for custom collection behavior.
*/
protected $collection_handler_store = array();
From 376a1b5223899c808702f424e156e03d7b6a9a16 Mon Sep 17 00:00:00 2001
From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com>
Date: Wed, 18 Sep 2024 08:11:07 -0700
Subject: [PATCH 26/26] Hide Empty Related Product Collections
---
.../src/Blocks/BlockTypes/ProductCollection.php | 17 ++++++++++++-----
1 file changed, 12 insertions(+), 5 deletions(-)
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
index 502178a051d..83eb74be35d 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
@@ -1839,13 +1839,20 @@ class ProductCollection extends AbstractBlock {
);
}
+ $related_products = wc_get_related_products(
+ $collection_args['relatedProductReference'],
+ // Use a higher limit so that the result set contains enough products for the collection to subsequently filter.
+ 100
+ );
+ if ( empty( $related_products ) ) {
+ return array(
+ 'post__in' => array( -1 ),
+ );
+ }
+
// Have it filter the results to products related to the one provided.
return array(
- 'post__in' => wc_get_related_products(
- $collection_args['relatedProductReference'],
- // Use a higher limit so that the result set contains enough products for the collection to subsequently filter.
- 100
- ),
+ 'post__in' => $related_products,
);
},
function ( $collection_args, $query ) {