Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
subception
/
wp-content
/
plugins
/
woocommerce
/
src
/
Blocks
/
BlockTypes
/
ProductCollection
:
HandlerRegistry.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?php declare(strict_types=1); namespace Automattic\WooCommerce\Blocks\BlockTypes\ProductCollection; use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils; use InvalidArgumentException; /** * HandlerRegistry class. * Manages collection handlers. */ class HandlerRegistry { /** * Associative array of collection handlers. * * @var array */ protected $collection_handler_store = []; /** * Register handlers for a collection. * * @param string $collection_name The name of the collection. * @param callable $build_query The query builder callable. * @param callable|null $frontend_args Optional frontend args callable. * @param callable|null $editor_args Optional editor args callable. * @param callable|null $preview_query Optional preview query callable. * * @throws InvalidArgumentException If handlers are already registered for the collection. */ public 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 ] = [ 'build_query' => $build_query, 'frontend_args' => $frontend_args, 'editor_args' => $editor_args, 'preview_query' => $preview_query, ]; return $this->collection_handler_store[ $collection_name ]; } /** * Register core collection handlers. */ public function register_core_collections() { $this->register_collection_handlers( 'woocommerce/product-collection/hand-picked', function ( $collection_args, $common_query_values, $query ) { // For Hand-Picked collection, if no products are selected, we should return an empty result set. // This ensures that the collection doesn't display any products until the user explicitly chooses them. if ( empty( $query['handpicked_products'] ) ) { return array( 'post__in' => array( -1 ), ); } } ); $this->register_collection_handlers( 'woocommerce/product-collection/by-category', function ( $collection_args, $common_query_values, $query ) { // For Products by Category collection, if no category is selected, we should return an empty result set. if ( empty( $query['taxonomies_query'] ) ) { return array( 'post__in' => array( -1 ), ); } } ); $this->register_collection_handlers( 'woocommerce/product-collection/by-tag', function ( $collection_args, $common_query_values, $query ) { // For Products by Tag collection, if no tag is selected, we should return an empty result set. if ( empty( $query['taxonomies_query'] ) ) { return array( 'post__in' => array( -1 ), ); } } ); $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 ), ); } $category_callback = function () use ( $collection_args ) { return $collection_args['relatedBy']['categories']; }; $tag_callback = function () use ( $collection_args ) { return $collection_args['relatedBy']['tags']; }; add_filter( 'woocommerce_product_related_posts_relate_by_category', $category_callback, PHP_INT_MAX ); add_filter( 'woocommerce_product_related_posts_relate_by_tag', $tag_callback, PHP_INT_MAX ); $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, array(), $collection_args['relatedBy'] ); remove_filter( 'woocommerce_product_related_posts_relate_by_category', $category_callback, PHP_INT_MAX ); remove_filter( 'woocommerce_product_related_posts_relate_by_tag', $tag_callback, PHP_INT_MAX ); 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' => $related_products, ); }, 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; $collection_args['relatedBy'] = ! isset( $query['relatedBy'] ) ? array( 'categories' => true, 'tags' => true, ) : array( 'categories' => isset( $query['relatedBy']['categories'] ) && true === $query['relatedBy']['categories'], 'tags' => isset( $query['relatedBy']['tags'] ) && true === $query['relatedBy']['tags'], ); 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; $related_by = $request->get_param( 'relatedBy' ); $collection_args['relatedBy'] = ! isset( $related_by ) ? array( 'categories' => true, 'tags' => true, ) : array( 'categories' => rest_sanitize_boolean( $related_by['categories'] ?? false ), 'tags' => rest_sanitize_boolean( $related_by['tags'] ?? false ), ); return $collection_args; } ); $this->register_collection_handlers( 'woocommerce/product-collection/upsells', function ( $collection_args ) { $product_reference = $collection_args['upsellsProductReferences'] ?? null; // No products should be shown if no upsells product reference is set. if ( empty( $product_reference ) ) { return array( 'post__in' => array( -1 ), ); } $products = array_map( 'wc_get_product', $product_reference ); if ( empty( $products ) ) { return array( 'post__in' => array( -1 ), ); } $all_upsells = array_reduce( $products, function ( $acc, $product ) { return array_merge( $acc, $product->get_upsell_ids() ); }, array() ); // Remove duplicates and product references. We don't want to display // what's already in cart. $unique_upsells = array_unique( $all_upsells ); $upsells = array_diff( $unique_upsells, $product_reference ); return array( 'post__in' => empty( $upsells ) ? array( -1 ) : $upsells, ); }, function ( $collection_args, $query ) { $product_references = isset( $query['productReference'] ) ? array( $query['productReference'] ) : null; // Infer the product reference from the location if an explicit product is not set. if ( empty( $product_references ) ) { $location = $collection_args['productCollectionLocation']; if ( isset( $location['type'] ) && 'product' === $location['type'] ) { $product_references = array( $location['sourceData']['productId'] ); } if ( isset( $location['type'] ) && 'cart' === $location['type'] ) { $product_references = $location['sourceData']['productIds']; } if ( isset( $location['type'] ) && 'order' === $location['type'] ) { $product_references = $this->get_product_ids_from_order( $location['sourceData']['orderId'] ?? 0 ); } } $collection_args['upsellsProductReferences'] = $product_references; 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['upsellsProductReferences'] = array( $product_reference ); return $collection_args; } ); $this->register_collection_handlers( 'woocommerce/product-collection/cross-sells', function ( $collection_args ) { $product_reference = $collection_args['crossSellsProductReferences'] ?? null; // No products should be shown if no cross-sells product reference is set. if ( empty( $product_reference ) ) { return array( 'post__in' => array( -1 ), ); } $products = array_filter( array_map( 'wc_get_product', $product_reference ) ); if ( empty( $products ) ) { return array( 'post__in' => array( -1 ), ); } $product_ids = array_map( function ( $product ) { return $product->get_id(); }, $products ); $all_cross_sells = array_reduce( $products, function ( $acc, $product ) { return array_merge( $acc, $product->get_cross_sell_ids() ); }, array() ); // Remove duplicates and product references. We don't want to display // what's already in cart. $unique_cross_sells = array_unique( $all_cross_sells ); $cross_sells = array_diff( $unique_cross_sells, $product_ids ); return array( 'post__in' => empty( $cross_sells ) ? array( -1 ) : $cross_sells, ); }, function ( $collection_args, $query ) { $product_references = isset( $query['productReference'] ) ? array( $query['productReference'] ) : null; // Infer the product reference from the location if an explicit product is not set. if ( empty( $product_references ) ) { $location = $collection_args['productCollectionLocation']; if ( isset( $location['type'] ) && 'product' === $location['type'] ) { $product_references = array( $location['sourceData']['productId'] ); } if ( isset( $location['type'] ) && 'cart' === $location['type'] ) { $product_references = $location['sourceData']['productIds']; } if ( isset( $location['type'] ) && 'order' === $location['type'] ) { $product_references = $this->get_product_ids_from_order( $location['sourceData']['orderId'] ?? 0 ); } } $collection_args['crossSellsProductReferences'] = $product_references; 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['crossSellsProductReferences'] = array( $product_reference ); return $collection_args; } ); $this->register_collection_handlers( 'woocommerce/product-collection/cart-contents', function ( $collection_args ) { $cart_product_ids = $collection_args['cartProductIds'] ?? null; if ( empty( $cart_product_ids ) ) { return array( 'post__in' => array( -1 ) ); } return array( 'post__in' => $cart_product_ids ); }, function ( $collection_args, $query ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $collection_args['cartProductIds'] = $this->get_cart_product_ids( $collection_args, null ); return $collection_args; }, function ( $collection_args, $query, $request ) { $collection_args['cartProductIds'] = $this->get_cart_product_ids( $collection_args, $request ); return $collection_args; } ); return $this->collection_handler_store; } /** * Get collection handler by name. * * @param string $name Collection name. * @return array|null Collection handler array or null if not found. */ public function get_collection_handler( $name ) { return $this->collection_handler_store[ $name ] ?? null; } /** * 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_handler_store[ $collection_name ] ); } /** * Get product IDs from an order. * * @param int $order_id The order ID. * @return array<int> The product IDs. */ private function get_product_ids_from_order( $order_id ) { $product_references = array(); if ( empty( $order_id ) ) { return $product_references; } $order = wc_get_order( $order_id ); if ( $order ) { $product_references = array_filter( array_map( function ( $item ) { return $item->get_product_id(); }, $order->get_items( 'line_item' ) ) ); } return $product_references; } /** * Get cart product IDs from various sources. * Handles loading cart products from location context or request params. * * @param array $collection_args Collection arguments with location context. * @param \WP_REST_Request|null $request Optional REST request for editor context. * @return array<int> The product IDs from the cart. Returns recent products for preview in editor context only. */ private function get_cart_product_ids( $collection_args, $request = null ) { $location = $collection_args['productCollectionLocation'] ?? array(); if ( $request ) { $user_id = $request->get_param( 'userId' ) ? absint( $request->get_param( 'userId' ) ) : null; $user_email = $request->get_param( 'userEmail' ) ? sanitize_email( $request->get_param( 'userEmail' ) ) : null; if ( $user_id || $user_email ) { $cart_ids = CartCheckoutUtils::get_cart_product_ids_for_user( $user_id, $user_email ); if ( ! empty( $cart_ids ) ) { return $cart_ids; } } // In editor context (REST request), show sample products for preview when cart is empty. $recent_product_ids = wc_get_products( array( 'status' => 'publish', 'orderby' => 'date', 'order' => 'DESC', 'limit' => 3, 'return' => 'ids', ) ); return ! empty( $recent_product_ids ) ? $recent_product_ids : array(); } if ( isset( $location['type'] ) && 'cart' === $location['type'] ) { $user_id = isset( $location['sourceData']['userId'] ) ? absint( $location['sourceData']['userId'] ) : null; $user_email = isset( $location['sourceData']['userEmail'] ) ? sanitize_email( $location['sourceData']['userEmail'] ) : null; if ( $user_id || $user_email ) { return CartCheckoutUtils::get_cart_product_ids_for_user( $user_id, $user_email ); } } // In frontend/email context, return empty array when no cart is found. return array(); } }