Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Product Collection: Parse front-end context #44145

Open
wants to merge 17 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions plugins/woocommerce/changelog/add-44144-front-end-context
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: enhancement

Provide the location context within the Product Collection block context
111 changes: 111 additions & 0 deletions plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
xristos3490 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Automattic\WooCommerce\Blocks\BlockTypes;

use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils;
use WP_Query;
use WC_Tax;

Expand Down Expand Up @@ -45,6 +46,13 @@ class ProductCollection extends AbstractBlock {
*/
protected $custom_order_opts = array( 'popularity', 'rating' );

/**
* An array of instance's inner block names.
*
* @var array
*/
protected $product_collection_inner_blocks_names = array();


/**
* Initialize this block type.
Expand Down Expand Up @@ -76,13 +84,115 @@ protected function initialize() {
// 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, 'update_context' ), 11, 3 );

// 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 );

add_filter( 'posts_clauses', array( $this, 'add_price_range_filter_posts_clauses' ), 10, 2 );
}

/**
* Provides the location context to each inner block of the product collection block.
*
* The context schema is an array with the following data:
* - type: The context type. Possible values are 'site', 'order', 'cart', 'archive', 'product'.
* - sourceData: The context data.
*
* The sourceData structure depends on the context type as follows:
* - site: []
* - order: ['orderId' => int]
* - cart: ['productIds' => array]
* - archive: ['taxonomy' => string, 'termId' => int]
* - product: ['productId' => int]
*
* @param array $context The block context.
* @param array $block The parsed block.
* @param WP_Block $parent_block The parent block.
*
* @return array The block's context including the location context.
*/
public function update_context( $context, $block, $parent_block ) {
xristos3490 marked this conversation as resolved.
Show resolved Hide resolved
// Run only on frontend.
// This is needed to avoid SSR renders within patterns. @see https://github.com/woocommerce/woocommerce/issues/45181.
if ( is_admin() || \WC()->is_rest_api_request() ) {
return $context;
}

if ( 'woocommerce/product-collection' === $block['blockName']
&& isset( $block['attrs']['query']['isProductCollectionBlock'] ) ) {
$this->product_collection_inner_blocks_names = array_reverse(
$this->extract_inner_block_names( $block )
);
}

$this->provide_location_context_for_inner_blocks( $block, $context );

return $context;
}

/**
* Extract the inner block names for the Product Collection block.
*
* @param array $block The Product Collection block.
* @param array $result Array of inner block names.
*
* @return array Array containing all the inner block names.
*/
protected function extract_inner_block_names( $block, &$result = array() ) {
if ( isset( $block['blockName'] ) ) {
$result[] = $block['blockName'];
}

if ( isset( $block['innerBlocks'] ) ) {
foreach ( $block['innerBlocks'] as $inner_block ) {
$this->extract_inner_block_names( $inner_block, $result );
}
}
return $result;
}

/**
* Add the productCollectionLocation context data into the inner blocks' context.
*
* @param array $block Block attributes.
* @param array $context Block context.
*/
protected function provide_location_context_for_inner_blocks( $block, &$context ) {
if ( empty( $this->product_collection_inner_blocks_names ) ) {
return;
}

$block_name = array_pop( $this->product_collection_inner_blocks_names );
$is_in_single_product = isset( $context['singleProduct'] ) && ! empty( $context['postId'] );

if ( $block_name === $block['blockName'] ) {

$context['productCollectionLocation'] = $is_in_single_product ? array(
'type' => 'product',
'sourceData' => array(
'productId' => absint( $context['postId'] ),
),
) : $this->get_location_context();
}
}

/**
* Get the global location context.
* Serve as a runtime cache for the location context.
*
* @return array The location context as described in self::update_context().
*/
protected function get_location_context() {
static $location_context = null;
xristos3490 marked this conversation as resolved.
Show resolved Hide resolved
if ( null === $location_context ) {
$location_context = ProductCollectionUtils::parse_global_location_context();
}
return $location_context;
}

/**
* Enhances the Product Collection block with client-side pagination.
*
Expand Down Expand Up @@ -324,6 +434,7 @@ public function add_support_for_filter_blocks( $pre_render, $parsed_block ) {
* @return array
*/
public function build_frontend_query( $query, $block, $page ) {

xristos3490 marked this conversation as resolved.
Show resolved Hide resolved
// If not in context of product collection block, return the query as is.
$is_product_collection_block = $block->context['query']['isProductCollectionBlock'] ?? false;
if ( ! $is_product_collection_block ) {
Expand Down
78 changes: 78 additions & 0 deletions plugins/woocommerce/src/Blocks/Utils/ProductCollectionUtils.php
xristos3490 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,84 @@
* {@internal This class and its methods are not intended for public use.}
*/
class ProductCollectionUtils {

/**
* Parse WP Query's global context for the Product Collection block.
*
* The return schema is:
* - type: The context type. Possible values are 'site', 'order', 'cart', 'archive', 'product'.
* - sourceData: The context data.
*
* The sourceData structure depends on the context type as follows:
* - site: []
* - order: ['orderId' => int]
* - cart: ['productIds' => array]
* - archive: ['taxonomy' => string, 'termId' => int]
* - product: ['productId' => int]
*
* @return array Parsed context.
*/
public static function parse_global_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 ) {
$items[] = absint( $cart_item['productId'] );
}
$items = array_unique( array_filter( $items ) );
$source_data = array( 'productIds' => $items );

} elseif ( is_product_taxonomy() ) {

$source = $wp_query->get_queried_object();
$taxonomy = is_a( $source, 'WP_Term' ) ? $source->taxonomy : '';
$term_id = is_a( $source, 'WP_Term' ) ? $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;
}

/**
* Prepare and execute a query for the Product Collection block.
* This method is used by the Product Collection block and the No Results block.
Expand Down