JezK
Edit File: SingleProductTemplate.php
<?php namespace Automattic\WooCommerce\Blocks\Templates; use Automattic\WooCommerce\Blocks\SharedStores\ProductsStore; use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplateCompatibility; use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils; /** * SingleProductTemplate class. * * @internal */ class SingleProductTemplate extends AbstractTemplate { /** * The slug of the template. * * @var string */ const SLUG = 'single-product'; /** * Initialization method. */ public function init() { add_action( 'template_redirect', array( $this, 'render_block_template' ) ); add_filter( 'get_block_templates', array( $this, 'update_single_product_content' ), 11, 1 ); } /** * Returns the title of the template. * * @return string */ public function get_template_title() { return _x( 'Single Product', 'Template name', 'woocommerce' ); } /** * Returns the description of the template. * * @return string */ public function get_template_description() { return __( 'Displays a single product.', 'woocommerce' ); } /** * Run template-specific logic when the query matches this template. */ public function render_block_template() { if ( ! is_embed() && is_singular( 'product' ) ) { global $post; $compatibility_layer = new SingleProductTemplateCompatibility(); $compatibility_layer->init(); $product = wc_get_product( $post->ID ); if ( $product ) { $consent = 'I acknowledge that using experimental APIs means my theme or plugin will inevitably break in the next version of WooCommerce'; // Load the product data into the products store so derived // state closures can resolve it during server-side rendering. ProductsStore::load_product( $consent, $product->get_id() ); // Set the current product context. The derived state // closures (mainProductInContext, productVariationInContext, productInContext) // are registered by ProductsStore::register_state(). wp_interactivity_state( 'woocommerce/products', array( 'productId' => $product->get_id(), 'variationId' => null, ) ); } } } /** * Add the block template objects to be used. * * @param array $query_result Array of template objects. * @return array */ public function update_single_product_content( $query_result ) { $query_result = array_map( function ( $template ) { if ( str_contains( $template->slug, self::SLUG ) ) { // We don't want to add the compatibility layer on the Editor Side. // The second condition is necessary to not apply the compatibility layer on the REST API. Gutenberg uses the REST API to clone the template. // More details: https://github.com/woocommerce/woocommerce-blocks/issues/9662. if ( ( ! is_admin() && ! ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) && ! BlockTemplateUtils::template_has_legacy_template_block( $template ) ) { // Add the product class to the body. We should move this to a more appropriate place. add_filter( 'body_class', function ( $classes ) { return array_merge( $classes, wc_get_product_class() ); } ); global $product; if ( ! $product instanceof \WC_Product ) { $product_id = get_the_ID(); if ( $product_id ) { wc_setup_product_data( $product_id ); } } if ( post_password_required() ) { $template->content = $this->add_password_form( $template->content ); } else { $template->content = SingleProductTemplateCompatibility::add_compatibility_layer( $template->content ); } } } return $template; }, $query_result ); return $query_result; } /** * Replace the first single product template block with the password form. Remove all other single product template blocks. * * @param array $parsed_blocks Array of parsed block objects. * @return array Parsed blocks. */ private static function replace_first_single_product_template_block_with_password_form( $parsed_blocks ) { $blocks = array(); $is_already_replaced = false; foreach ( $parsed_blocks as $block ) { $processed = self::process_block_for_password_form( $block, $is_already_replaced ); $is_already_replaced = $processed['is_already_replaced']; $blocks = array_merge( $blocks, $processed['blocks'] ); } return $blocks; } /** * Replace or remove a parsed block when rendering a password-protected product. * * @param array $block Parsed block. * @param bool $is_already_replaced If the password form has already been added. * @return array{blocks: array, is_already_replaced: bool} Replacement blocks (empty when removed). */ private static function process_block_for_password_form( $block, $is_already_replaced ) { if ( self::is_password_protected_replacement_block( $block ) ) { if ( $is_already_replaced ) { return array( 'blocks' => array(), 'is_already_replaced' => true, ); } return array( 'blocks' => array( parse_blocks( '<!-- wp:html -->' . get_the_password_form() . '<!-- /wp:html -->' )[0] ), 'is_already_replaced' => true, ); } if ( empty( $block['innerBlocks'] ) || ! isset( $block['innerContent'] ) || ! is_array( $block['innerContent'] ) ) { return array( 'blocks' => array( $block ), 'is_already_replaced' => $is_already_replaced, ); } $new_inner_blocks = array(); $new_inner_content = array(); $inner_block_index = 0; foreach ( $block['innerContent'] as $chunk ) { if ( is_string( $chunk ) ) { $new_inner_content[] = $chunk; continue; } if ( ! isset( $block['innerBlocks'][ $inner_block_index ] ) ) { continue; } $processed = self::process_block_for_password_form( $block['innerBlocks'][ $inner_block_index ], $is_already_replaced ); $is_already_replaced = $processed['is_already_replaced']; ++$inner_block_index; foreach ( $processed['blocks'] as $processed_block ) { $new_inner_blocks[] = $processed_block; $new_inner_content[] = null; } } if ( count( $new_inner_blocks ) === 0 ) { return array( 'blocks' => array(), 'is_already_replaced' => $is_already_replaced, ); } $block['innerBlocks'] = $new_inner_blocks; $block['innerContent'] = $new_inner_content; return array( 'blocks' => array( $block ), 'is_already_replaced' => $is_already_replaced, ); } /** * Whether this block should be replaced with the password form (or removed after the first replacement). * * This list is not every block that can appear on a single product template. Blocks such as breadcrumbs * stay visible on password-protected products. * * @param array $block Parsed block. * @return bool */ private static function is_password_protected_replacement_block( $block ) { $block_name = $block['blockName'] ?? ''; $single_product_template_blocks = array( 'woocommerce/product-image-gallery', 'woocommerce/product-details', 'woocommerce/add-to-cart-form', 'woocommerce/product-meta', 'woocommerce/product-rating', 'woocommerce/product-price', 'woocommerce/product-summary', 'woocommerce/related-products', 'woocommerce/add-to-cart-with-options', 'woocommerce/product-gallery', 'woocommerce/product-collection', 'core/post-title', 'core/post-excerpt', ); if ( in_array( $block_name, $single_product_template_blocks, true ) ) { return true; } if ( 'core/pattern' !== $block_name || ! isset( $block['attrs']['slug'] ) ) { return false; } $pattern = \WP_Block_Patterns_Registry::get_instance()->get_registered( $block['attrs']['slug'] ); if ( empty( $pattern['content'] ) ) { return false; } return BlockTemplateUtils::has_block_including_patterns( $single_product_template_blocks, parse_blocks( $pattern['content'] ) ); } /** * Add password form to the Single Product Template. * * @param string $content The content of the template. * @return string */ public static function add_password_form( $content ) { $parsed_blocks = parse_blocks( $content ); $serialized_blocks = serialize_blocks( self::replace_first_single_product_template_block_with_password_form( $parsed_blocks ) ); return $serialized_blocks; } }