[Product Collection] Parse front-end context (#44145)

* Parse global context

* Refactor the util to only parse global context, provide the location context to collection children

* cleanup

* Cleanup

* Add changelog

* Make linters happy

* provide context to each inner block recursively

* Fix linters

* Remove debug

* Update plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php

Co-authored-by: Manish Menaria <the.manish.menaria@gmail.com>

* Rename the provider method

* Fix typo on cart item products

* Use the query context instead

* Lint

* Lint again

* Minor change on docs

* Polish

* Cleanup

---------

Co-authored-by: Manish Menaria <the.manish.menaria@gmail.com>
This commit is contained in:
Chris Lilitsas 2024-07-05 13:49:24 +03:00 committed by GitHub
parent be849473e0
commit d3df046f97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 156 additions and 0 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: enhancement
Provide the location context within the Product Collection block context

View File

@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils;
use WP_Query;
use WC_Tax;
@ -76,6 +77,9 @@ class ProductCollection extends AbstractBlock {
// Extend allowed `collection_params` for the REST API.
add_filter( 'rest_product_collection_params', array( $this, 'extend_rest_query_allowed_params' ), 10, 1 );
// Provide location context into block's context.
add_filter( 'render_block_context', array( $this, 'provide_location_context_for_inner_blocks' ), 11, 1 );
// Interactivity API: Add navigation directives to the product collection block.
add_filter( 'render_block_woocommerce/product-collection', array( $this, 'enhance_product_collection_with_interactivity' ), 10, 2 );
add_filter( 'render_block_core/query-pagination', array( $this, 'add_navigation_link_directives' ), 10, 3 );
@ -86,6 +90,71 @@ class ProductCollection extends AbstractBlock {
add_filter( 'render_block_data', array( $this, 'disable_enhanced_pagination' ), 10, 1 );
}
/**
* Provides the location context to each inner block of the product collection block.
* Hint: Only blocks using the 'query' context will be affected.
*
* The sourceData structure depends on the context type as follows:
* - site: [ ]
* - order: [ 'orderId' => int ]
* - cart: [ 'productIds' => int[] ]
* - archive: [ 'taxonomy' => string, 'termId' => int ]
* - product: [ 'productId' => int ]
*
* @example array(
* 'type' => 'product',
* 'sourceData' => array( 'productId' => 123 ),
* )
*
* @param array $context The block context.
* @return array $context {
* The block context including the product collection location context.
*
* @type array $productCollectionLocation {
* @type string $type The context type. Possible values are 'site', 'order', 'cart', 'archive', 'product'.
* @type array $sourceData The context source data. Can be the product ID of the viewed product, the order ID of the current order viewed, etc. See structure above for more details.
* }
* }
*/
public function provide_location_context_for_inner_blocks( $context ) {
// Run only on frontend.
// This is needed to avoid SSR renders while in editor. @see https://github.com/woocommerce/woocommerce/issues/45181.
if ( is_admin() || \WC()->is_rest_api_request() ) {
return $context;
}
// Target only product collection's inner blocks that use the 'query' context.
if ( ! isset( $context['query'] ) || ! isset( $context['query']['isProductCollectionBlock'] ) || ! $context['query']['isProductCollectionBlock'] ) {
return $context;
}
$is_in_single_product = isset( $context['singleProduct'] ) && ! empty( $context['postId'] );
$context['productCollectionLocation'] = $is_in_single_product ? array(
'type' => 'product',
'sourceData' => array(
'productId' => absint( $context['postId'] ),
),
) : $this->get_location_context();
return $context;
}
/**
* Get the global location context.
* Serve as a runtime cache for the location context.
*
* @see ProductCollectionUtils::parse_frontend_location_context()
*
* @return array The location context.
*/
private function get_location_context() {
static $location_context = null;
if ( null === $location_context ) {
$location_context = ProductCollectionUtils::parse_frontend_location_context();
}
return $location_context;
}
/**
* Enhances the Product Collection block with client-side pagination.
*
@ -451,6 +520,7 @@ class ProductCollection extends AbstractBlock {
}
$block_context_query = $block->context['query'];
// phpcs:ignore WordPress.DB.SlowDBQuery
$block_context_query['tax_query'] = ! empty( $query['tax_query'] ) ? $query['tax_query'] : array();

View File

@ -8,6 +8,7 @@ use WP_Query;
* {@internal This class and its methods are not intended for public use.}
*/
class ProductCollectionUtils {
/**
* Prepare and execute a query for the Product Collection block.
* This method is used by the Product Collection block and the No Results block.
@ -78,6 +79,87 @@ class ProductCollectionUtils {
return self::remove_empty_array_recursive( $queries );
}
/**
* Parse WP Query's front-end context for the Product Collection block.
*
* The sourceData structure depends on the context type as follows:
* - site: [ ]
* - order: [ 'orderId' => int ]
* - cart: [ 'productIds' => int[] ]
* - archive: [ 'taxonomy' => string, 'termId' => int ]
* - product: [ 'productId' => int ]
*
* @return array $context {
* @type string $type The context type. Possible values are 'site', 'order', 'cart', 'archive', 'product'.
* @type array $sourceData The context source data. Can be the product ID of the viewed product, the order ID of the current order, etc.
* }
*/
public static function parse_frontend_location_context() {
global $wp_query;
// Default context.
// Hint: The Shop page uses the default context.
$type = 'site';
$source_data = array();
if ( ! ( $wp_query instanceof WP_Query ) ) {
return array(
'type' => $type,
'sourceData' => $source_data,
);
}
// As more areas are blockified, expected future contexts include:
// - is_checkout_pay_page().
// - is_view_order_page().
if ( is_order_received_page() ) {
$type = 'order';
$source_data = array( 'orderId' => absint( $wp_query->query_vars['order-received'] ) );
} elseif ( ( is_cart() || is_checkout() ) && isset( WC()->cart ) && is_a( WC()->cart, 'WC_Cart' ) ) {
$type = 'cart';
$items = array();
foreach ( WC()->cart->get_cart() as $cart_item ) {
if ( ! isset( $cart_item['product_id'] ) ) {
continue;
}
$items[] = absint( $cart_item['product_id'] );
}
$items = array_unique( array_filter( $items ) );
$source_data = array( 'productIds' => $items );
} elseif ( is_product_taxonomy() ) {
$source = $wp_query->get_queried_object();
$is_valid = is_a( $source, 'WP_Term' );
$taxonomy = $is_valid ? $source->taxonomy : '';
$term_id = $is_valid ? $source->term_id : '';
$type = 'archive';
$source_data = array(
'taxonomy' => wc_clean( $taxonomy ),
'termId' => absint( $term_id ),
);
} elseif ( is_product() ) {
$source = $wp_query->get_queried_object();
$product_id = is_a( $source, 'WP_Post' ) ? absint( $source->ID ) : 0;
$type = 'product';
$source_data = array( 'productId' => $product_id );
}
$context = array(
'type' => $type,
'sourceData' => $source_data,
);
return $context;
}
/**
* Remove falsy item from array, recursively.
*