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 all 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
70 changes: 69 additions & 1 deletion 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,7 +46,6 @@ class ProductCollection extends AbstractBlock {
*/
protected $custom_order_opts = array( 'popularity', 'rating' );


/**
* Initialize this block type.
*
Expand Down Expand Up @@ -76,13 +76,81 @@ 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, '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 );

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.
* 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 ]
*
* @see ProductCollectionUtils::parse_global_location_context()
*
* @example array(
* 'type' => 'product',
* 'sourceData' => array( 'productId' => 123 ),
* )
*
* @param array $context The block context.
* @return array $context {
* The 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, etc.
* }
* }
*/
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.
*
* @return array The location context as described in self::update_context().
*/
private 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
77 changes: 77 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,83 @@
* {@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 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_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['product_id'] );
}
$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