lt_catalog_orderby', array( 'type' => 'object', 'description' => __( 'How should products be sorted in the catalog by default?', 'woocommerce' ), 'label' => __( 'Default product sorting', 'woocommerce' ), 'show_in_rest' => array( 'name' => 'woocommerce_default_catalog_orderby', 'schema' => array( 'type' => 'string', 'enum' => array( 'menu_order', 'popularity', 'rating', 'date', 'price', 'price-desc' ), ), ), 'default' => 'menu_order', ) ); } /** * Update the query for the product query block in Editor. * * @param array $query Query args. * @param WP_REST_Request $request Request. */ 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 $query; } $product_collection_query_context = $request->get_param( 'productCollectionQueryContext' ); $collection_args = array( '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_registry->get_collection_handler( $collection_args['name'] ); if ( isset( $handlers['editor_args'] ) ) { $collection_args = call_user_func( $handlers['editor_args'], $collection_args, $query, $request ); } $orderby = $request->get_param( 'orderby' ); // 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->query_builder->get_preview_query_args( $collection_args, array_merge( $query, array( 'orderby' => $orderby ) ), $request ); } $on_sale = $request->get_param( 'woocommerceOnSale' ) === 'true'; $stock_status = $request->get_param( 'woocommerceStockStatus' ); $product_attributes = $request->get_param( 'woocommerceAttributes' ); $handpicked_products = $request->get_param( 'woocommerceHandPickedProducts' ); $featured = $request->get_param( 'featured' ); $time_frame = $request->get_param( 'timeFrame' ); $price_range = $request->get_param( 'priceRange' ); $raw_tax_query_from_rest_params = $query['tax_query'] ?? array(); // 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. $query['author'] = ''; // Use QueryBuilder to get the final query args. return $this->query_builder->get_final_query_args( $collection_args, $query, array( 'orderby' => $orderby, 'on_sale' => $on_sale, 'stock_status' => $stock_status, 'product_attributes' => $product_attributes, 'handpicked_products' => $handpicked_products, 'featured' => $featured, 'timeFrame' => $time_frame, 'priceRange' => $price_range, 'taxonomies_query' => $raw_tax_query_from_rest_params, ) ); } /** * Add support for filter blocks: * - Price filter block * - Attributes filter block * - Rating filter block * - In stock filter block etc. * * @param array $pre_render The pre-rendered block. * @param array $parsed_block The parsed block. */ public function add_support_for_filter_blocks( $pre_render, $parsed_block ) { $is_product_collection_block = $parsed_block['attrs']['query']['isProductCollectionBlock'] ?? false; if ( ! $is_product_collection_block ) { return $pre_render; } $this->renderer->set_parsed_block( $parsed_block ); $this->asset_data_registry->add( 'hasFilterableProducts', true ); /** * It enables the page to refresh when a filter is applied, ensuring that the product collection block, * which is a server-side rendered (SSR) block, retrieves the products that match the filters. */ $this->asset_data_registry->add( 'isRenderingPhpTemplate', true ); /* * When forcePageReload is enabled, the product collection has no data-wp-router-region, * so the Interactivity Router cannot update it client-side. Signal the product-filters * block so its navigate action falls back to a full page reload instead of using the * router, without affecting other blocks on the page. * * This is only needed when the query is inherited from the template, as that's the * only case where the Product Filters block can be a sibling rather than a descendant * of the Product Collection. When it's a descendant, forcePageReload is passed through * the block context instead. */ if ( ( $parsed_block['attrs']['forcePageReload'] ?? false ) && ( $parsed_block['attrs']['query']['inherit'] ?? false ) ) { wp_interactivity_config( 'woocommerce/product-filters', array( 'forcePageReload' => true ) ); } return $pre_render; } /** * Return a custom query based on attributes, filters and global WP_Query. * * @param WP_Query $query The WordPress Query. * @param WP_Block $block The block being rendered. * @param int $page The page number. * * @return array */ public function build_frontend_query( $query, $block, $page ) { // 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 ) { return $query; } $block_context_query = $block->context['query']; // phpcs:ignore WordPress.DB.SlowDBQuery $block_context_query['tax_query'] = ! empty( $query['tax_query'] ) ? $query['tax_query'] : array(); $inherit = $block->context['query']['inherit'] ?? false; $filterable = $block->context['query']['filterable'] ?? false; $is_exclude_applied_filters = ! ( $inherit || $filterable ); $collection_args = array( 'name' => $block->context['collection'] ?? '', 'productCollectionLocation' => $block->context['productCollectionLocation'] ?? null, ); // Use QueryBuilder to construct the query. return $this->query_builder->get_final_frontend_query( $collection_args, $block_context_query, $page, $is_exclude_applied_filters ); } /** * Extends allowed `collection_params` for the REST API * * By itself, the REST API doesn't accept custom `orderby` values, * even if they are supported by a custom post type. * * @param array $params A list of allowed `orderby` values. * * @return array */ public function extend_rest_query_allowed_params( $params ) { $original_enum = isset( $params['orderby']['enum'] ) ? $params['orderby']['enum'] : array(); $params['orderby']['enum'] = array_unique( array_merge( $original_enum, $this->query_builder->get_custom_order_opts() ) ); return $params; } /** * Registers core collections and sets the handler store. */ protected function register_core_collections_and_set_handler_store() { // Use HandlerRegistry to register collections. $collection_handler_store = $this->collection_handler_registry->register_core_collections(); $this->query_builder->set_collection_handler_store( $collection_handler_store ); } }