Commit 20c8388e6ab for woocommerce

commit 20c8388e6ab13aabc7bba647590b7bd034e446d6
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date:   Wed Jun 3 18:51:48 2026 +0200

    Simplify block templates compatibility layer checks (#64504)

    * Simplify compatibility layer checks

    * Add changelog

    * Linting and PHPStan

    * Add a wc_doing_it_wrong() for extensions that were hooking into the filter too early

    * Improve message

    * Improve backwards compatibility

    * Update changelog

    * Simplify logic to run previous woocommerce_disable_compatibility_layer filters

    * Add tests

diff --git a/plugins/woocommerce/changelog/fix-simplify-compatibility-layer-checks b/plugins/woocommerce/changelog/fix-simplify-compatibility-layer-checks
new file mode 100644
index 00000000000..15ffd625786
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-simplify-compatibility-layer-checks
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Move logic to enable or disable the compatibility layer in block templates from 'template_redirect' to 'template_include' to be more accurate with the resolved template
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 37ba3481e29..7badf4992b4 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -55539,24 +55539,6 @@ parameters:
 			count: 1
 			path: src/Blocks/Templates/AbstractTemplateCompatibility.php

-		-
-			message: '#^One or more @param tags has an invalid name or invalid syntax\.$#'
-			identifier: phpDoc.parseError
-			count: 2
-			path: src/Blocks/Templates/AbstractTemplateCompatibility.php
-
-		-
-			message: '#^PHPDoc tag @param has invalid value \(boolean\.\)\: Unexpected token "\.", expected variable at offset 211 on line 7$#'
-			identifier: phpDoc.parseError
-			count: 1
-			path: src/Blocks/Templates/AbstractTemplateCompatibility.php
-
-		-
-			message: '#^PHPDoc tag @param has invalid value \(boolean\.\)\: Unexpected token "\.", expected variable at offset 221 on line 7$#'
-			identifier: phpDoc.parseError
-			count: 1
-			path: src/Blocks/Templates/AbstractTemplateCompatibility.php
-
 		-
 			message: '#^Parameter \$parent_block of method Automattic\\WooCommerce\\Blocks\\Templates\\AbstractTemplateCompatibility\:\:update_render_block_data\(\) has invalid type Automattic\\WooCommerce\\Blocks\\Templates\\WP_Block\.$#'
 			identifier: class.notFound
diff --git a/plugins/woocommerce/src/Blocks/Templates/AbstractTemplateCompatibility.php b/plugins/woocommerce/src/Blocks/Templates/AbstractTemplateCompatibility.php
index aa574e0e29e..41dc4b179c3 100644
--- a/plugins/woocommerce/src/Blocks/Templates/AbstractTemplateCompatibility.php
+++ b/plugins/woocommerce/src/Blocks/Templates/AbstractTemplateCompatibility.php
@@ -1,6 +1,8 @@
 <?php
 namespace Automattic\WooCommerce\Blocks\Templates;

+use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
+
 /**
  * AbstractTemplateCompatibility class.
  *
@@ -24,49 +26,60 @@ abstract class AbstractTemplateCompatibility {
 		$this->set_hook_data();

 		add_filter(
-			'render_block_data',
-			function ( $parsed_block, $source_block, $parent_block ) {
-				/**
-				* Filter to disable the compatibility layer for the blockified templates.
-				*
-				* This hook allows to disable the compatibility layer for the blockified templates.
-				*
-				* @since 7.6.0
-				* @param boolean.
-				*/
-				$is_disabled_compatility_layer = apply_filters( 'woocommerce_disable_compatibility_layer', false );
-
-				if ( $is_disabled_compatility_layer ) {
-					return $parsed_block;
-				}
-
-				return $this->update_render_block_data( $parsed_block, $source_block, $parent_block );
-			},
-			10,
-			3
-		);
+			'template_include',
+			function ( $template ) {
+				$this->set_compatibility_layer_flag();

-		add_filter(
-			'render_block',
-			function ( $block_content, $block ) {
-				/**
-				* Filter to disable the compatibility layer for the blockified templates.
-				*
-				* This hook allows to disable the compatibility layer for the blockified.
-				*
-				* @since 7.6.0
-				* @param boolean.
-				*/
-				$is_disabled_compatibility_layer = apply_filters( 'woocommerce_disable_compatibility_layer', false );
-
-				if ( $is_disabled_compatibility_layer ) {
-					return $block_content;
-				}
-
-				return $this->inject_hooks( $block_content, $block );
+				add_filter(
+					'render_block_data',
+					function ( $parsed_block, $source_block, $parent_block ) {
+						/**
+						* Filter to disable the compatibility layer for the blockified templates.
+						*
+						* This hook allows to disable the compatibility layer for the blockified templates.
+						*
+						* @since 7.6.0
+						* @param bool $is_disabled_compatibility_layer Whether the compatibility layer should be disabled.
+						*/
+						$is_disabled_compatibility_layer = apply_filters( 'woocommerce_disable_compatibility_layer', false );
+
+						if ( $is_disabled_compatibility_layer ) {
+							return $parsed_block;
+						}
+
+						return $this->update_render_block_data( $parsed_block, $source_block, $parent_block );
+					},
+					10,
+					3
+				);
+
+				add_filter(
+					'render_block',
+					function ( $block_content, $block ) {
+						/**
+						* Filter to disable the compatibility layer for the blockified templates.
+						*
+						* This hook allows to disable the compatibility layer for the blockified.
+						*
+						* @since 7.6.0
+						* @param bool $is_disabled_compatibility_layer Whether the compatibility layer should be disabled.
+						*/
+						$is_disabled_compatibility_layer = apply_filters( 'woocommerce_disable_compatibility_layer', false );
+
+						if ( $is_disabled_compatibility_layer ) {
+							return $block_content;
+						}
+
+						return $this->inject_hooks( $block_content, $block );
+					},
+					10,
+					2
+				);
+
+				return $template;
 			},
 			10,
-			2
+			1
 		);
 	}

@@ -195,4 +208,50 @@ abstract class AbstractTemplateCompatibility {
 		}
 		return ob_get_clean();
 	}
+
+	/**
+	 * Check if the current template has a legacy template block.
+	 *
+	 * @return bool True if the current template has a legacy template block, false otherwise.
+	 *
+	 * @internal
+	 */
+	public function current_template_has_legacy_template_block() {
+		global $_wp_current_template_id;
+
+		if ( empty( $_wp_current_template_id ) ) {
+			return false;
+		}
+
+		$current_template = get_block_template( $_wp_current_template_id, 'wp_template' );
+
+		if ( isset( $current_template ) && BlockTemplateUtils::template_has_legacy_template_block( $current_template ) ) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Check if the current template has a legacy template block and disable the compatibility layer if it does.
+	 *
+	 * @return void
+	 *
+	 * @internal
+	 */
+	public function set_compatibility_layer_flag() {
+		$current_template_has_legacy_template_block = $this->current_template_has_legacy_template_block();
+
+		/**
+		 * Filter to determine whether the compatibility layer should be disabled.
+		 *
+		 * @since 11.0.0
+		 * @param bool $should_disable_compatibility_layer Whether the compatibility layer should be disabled.
+		 */
+		$should_disable_compatibility_layer = apply_filters( 'woocommerce_disable_compatibility_layer', $current_template_has_legacy_template_block );
+
+		if ( $should_disable_compatibility_layer ) {
+			add_filter( 'woocommerce_disable_compatibility_layer', '__return_true' );
+		}
+	}
 }
diff --git a/plugins/woocommerce/src/Blocks/Templates/ProductAttributeTemplate.php b/plugins/woocommerce/src/Blocks/Templates/ProductAttributeTemplate.php
index 953314d97ca..24d023283ac 100644
--- a/plugins/woocommerce/src/Blocks/Templates/ProductAttributeTemplate.php
+++ b/plugins/woocommerce/src/Blocks/Templates/ProductAttributeTemplate.php
@@ -56,12 +56,6 @@ class ProductAttributeTemplate extends AbstractTemplateWithFallback {
 		if ( isset( $queried_object->taxonomy ) && taxonomy_is_product_attribute( $queried_object->taxonomy ) ) {
 			$compatibility_layer = new ArchiveProductTemplatesCompatibility();
 			$compatibility_layer->init();
-
-			$templates = get_block_templates( array( 'slug__in' => array( self::SLUG ) ) );
-
-			if ( isset( $templates[0] ) && BlockTemplateUtils::template_has_legacy_template_block( $templates[0] ) ) {
-				add_filter( 'woocommerce_disable_compatibility_layer', '__return_true' );
-			}
 		}
 	}

diff --git a/plugins/woocommerce/src/Blocks/Templates/ProductBrandTemplate.php b/plugins/woocommerce/src/Blocks/Templates/ProductBrandTemplate.php
index 572930052a3..476c329dfbd 100644
--- a/plugins/woocommerce/src/Blocks/Templates/ProductBrandTemplate.php
+++ b/plugins/woocommerce/src/Blocks/Templates/ProductBrandTemplate.php
@@ -59,12 +59,6 @@ class ProductBrandTemplate extends AbstractTemplateWithFallback {
 			$compatibility_layer = new ArchiveProductTemplatesCompatibility();
 			$compatibility_layer->init();

-			$templates = get_block_templates( array( 'slug__in' => array( self::SLUG ) ) );
-
-			if ( isset( $templates[0] ) && BlockTemplateUtils::template_has_legacy_template_block( $templates[0] ) ) {
-				add_filter( 'woocommerce_disable_compatibility_layer', '__return_true' );
-			}
-
 			add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 );
 		}
 	}
diff --git a/plugins/woocommerce/src/Blocks/Templates/ProductCatalogTemplate.php b/plugins/woocommerce/src/Blocks/Templates/ProductCatalogTemplate.php
index 6f2d94b1017..149eec9e4e6 100644
--- a/plugins/woocommerce/src/Blocks/Templates/ProductCatalogTemplate.php
+++ b/plugins/woocommerce/src/Blocks/Templates/ProductCatalogTemplate.php
@@ -52,12 +52,6 @@ class ProductCatalogTemplate extends AbstractTemplate {
 		if ( ! is_embed() && ( is_post_type_archive( 'product' ) || is_page( wc_get_page_id( 'shop' ) ) ) && ! is_search() ) {
 			$compatibility_layer = new ArchiveProductTemplatesCompatibility();
 			$compatibility_layer->init();
-
-			$templates = get_block_templates( array( 'slug__in' => array( self::SLUG ) ) );
-
-			if ( isset( $templates[0] ) && BlockTemplateUtils::template_has_legacy_template_block( $templates[0] ) ) {
-				add_filter( 'woocommerce_disable_compatibility_layer', '__return_true' );
-			}
 		}
 	}

diff --git a/plugins/woocommerce/src/Blocks/Templates/ProductCategoryTemplate.php b/plugins/woocommerce/src/Blocks/Templates/ProductCategoryTemplate.php
index f7a08ad571a..560fd77ce00 100644
--- a/plugins/woocommerce/src/Blocks/Templates/ProductCategoryTemplate.php
+++ b/plugins/woocommerce/src/Blocks/Templates/ProductCategoryTemplate.php
@@ -59,12 +59,6 @@ class ProductCategoryTemplate extends AbstractTemplateWithFallback {
 			$compatibility_layer = new ArchiveProductTemplatesCompatibility();
 			$compatibility_layer->init();

-			$templates = get_block_templates( array( 'slug__in' => array( self::SLUG ) ) );
-
-			if ( isset( $templates[0] ) && BlockTemplateUtils::template_has_legacy_template_block( $templates[0] ) ) {
-				add_filter( 'woocommerce_disable_compatibility_layer', '__return_true' );
-			}
-
 			add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 );
 		}
 	}
diff --git a/plugins/woocommerce/src/Blocks/Templates/ProductSearchResultsTemplate.php b/plugins/woocommerce/src/Blocks/Templates/ProductSearchResultsTemplate.php
index 4f4183abc45..a26d6a7a794 100644
--- a/plugins/woocommerce/src/Blocks/Templates/ProductSearchResultsTemplate.php
+++ b/plugins/woocommerce/src/Blocks/Templates/ProductSearchResultsTemplate.php
@@ -51,12 +51,6 @@ class ProductSearchResultsTemplate extends AbstractTemplate {
 		if ( ! is_embed() && is_post_type_archive( 'product' ) && is_search() ) {
 			$compatibility_layer = new ArchiveProductTemplatesCompatibility();
 			$compatibility_layer->init();
-
-			$templates = get_block_templates( array( 'slug__in' => array( self::SLUG ) ) );
-
-			if ( isset( $templates[0] ) && BlockTemplateUtils::template_has_legacy_template_block( $templates[0] ) ) {
-				add_filter( 'woocommerce_disable_compatibility_layer', '__return_true' );
-			}
 		}
 	}

diff --git a/plugins/woocommerce/src/Blocks/Templates/ProductTagTemplate.php b/plugins/woocommerce/src/Blocks/Templates/ProductTagTemplate.php
index 8718b8a52c8..66525dec03e 100644
--- a/plugins/woocommerce/src/Blocks/Templates/ProductTagTemplate.php
+++ b/plugins/woocommerce/src/Blocks/Templates/ProductTagTemplate.php
@@ -59,12 +59,6 @@ class ProductTagTemplate extends AbstractTemplateWithFallback {
 			$compatibility_layer = new ArchiveProductTemplatesCompatibility();
 			$compatibility_layer->init();

-			$templates = get_block_templates( array( 'slug__in' => array( self::SLUG ) ) );
-
-			if ( isset( $templates[0] ) && BlockTemplateUtils::template_has_legacy_template_block( $templates[0] ) ) {
-				add_filter( 'woocommerce_disable_compatibility_layer', '__return_true' );
-			}
-
 			add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 );
 		}
 	}
diff --git a/plugins/woocommerce/src/Blocks/Templates/SingleProductTemplate.php b/plugins/woocommerce/src/Blocks/Templates/SingleProductTemplate.php
index 41d2a2bcb72..e97edf1d1d1 100644
--- a/plugins/woocommerce/src/Blocks/Templates/SingleProductTemplate.php
+++ b/plugins/woocommerce/src/Blocks/Templates/SingleProductTemplate.php
@@ -55,34 +55,6 @@ class SingleProductTemplate extends AbstractTemplate {
 			$compatibility_layer = new SingleProductTemplateCompatibility();
 			$compatibility_layer->init();

-			$valid_slugs         = array( self::SLUG );
-			$single_product_slug = 'product' === $post->post_type && $post->post_name ? 'single-product-' . $post->post_name : '';
-			if ( $single_product_slug ) {
-				$valid_slugs[] = 'single-product-' . $post->post_name;
-			}
-			$templates = get_block_templates( array( 'slug__in' => $valid_slugs ) );
-
-			if ( count( $templates ) === 0 ) {
-				return;
-			}
-
-			// Use the first template by default.
-			$template = reset( $templates );
-
-			// Check if there is a template matching the slug `single-product-{post_name}`.
-			if ( count( $valid_slugs ) > 1 && count( $templates ) > 1 ) {
-				foreach ( $templates as $t ) {
-					if ( $single_product_slug === $t->slug ) {
-						$template = $t;
-						break;
-					}
-				}
-			}
-
-			if ( isset( $template ) && BlockTemplateUtils::template_has_legacy_template_block( $template ) ) {
-				add_filter( 'woocommerce_disable_compatibility_layer', '__return_true' );
-			}
-
 			$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';
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Templates/AbstractTemplateCompatibilityTest.php b/plugins/woocommerce/tests/php/src/Blocks/Templates/AbstractTemplateCompatibilityTest.php
new file mode 100644
index 00000000000..4363511a082
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/Templates/AbstractTemplateCompatibilityTest.php
@@ -0,0 +1,195 @@
+<?php
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\Templates;
+
+use Automattic\WooCommerce\Blocks\Templates\AbstractTemplateCompatibility;
+use WP_UnitTestCase;
+
+/**
+ * Tests for AbstractTemplateCompatibility::set_compatibility_layer_flag().
+ */
+class AbstractTemplateCompatibilityTest extends WP_UnitTestCase {
+
+	/**
+	 * The System Under Test.
+	 *
+	 * @var AbstractTemplateCompatibility
+	 */
+	private $sut;
+
+	/**
+	 * @inheritdoc
+	 */
+	public function tearDown(): void {
+		$this->remove_compatibility_layer_filters();
+		parent::tearDown();
+	}
+
+	/**
+	 * @testdox Disables the compatibility layer when the template has a legacy template block.
+	 */
+	public function test_disables_compatibility_layer_when_template_has_legacy_block(): void {
+		$this->sut = $this->create_sut( true );
+
+		$this->sut->set_compatibility_layer_flag();
+
+		$this->assertTrue(
+			$this->is_compatibility_layer_disabled(),
+			'Legacy templates should disable the compatibility layer for subsequent checks.'
+		);
+	}
+
+	/**
+	 * @testdox Keeps the compatibility layer enabled when the template is blockified.
+	 */
+	public function test_keeps_compatibility_layer_enabled_when_template_is_blockified(): void {
+		$this->sut = $this->create_sut( false );
+
+		$this->sut->set_compatibility_layer_flag();
+
+		$this->assertFalse(
+			$this->is_compatibility_layer_disabled(),
+			'Blockified templates should keep the compatibility layer enabled by default.'
+		);
+	}
+
+	/**
+	 * @testdox Passes legacy detection as the filter default value.
+	 */
+	public function test_filter_receives_legacy_detection_as_default(): void {
+		$received_default = null;
+
+		add_filter(
+			'woocommerce_disable_compatibility_layer',
+			function ( $should_disable ) use ( &$received_default ) {
+				$received_default = $should_disable;
+
+				return $should_disable;
+			},
+			10,
+			1
+		);
+
+		$this->sut = $this->create_sut( true );
+		$this->sut->set_compatibility_layer_flag();
+
+		$this->assertTrue( $received_default, 'Filter default should reflect legacy template detection.' );
+	}
+
+	/**
+	 * @testdox Keeps the compatibility layer enabled when an extension overrides a legacy template default to false.
+	 */
+	public function test_keeps_compatibility_layer_enabled_when_extension_overrides_legacy_default_to_false(): void {
+		add_filter( 'woocommerce_disable_compatibility_layer', '__return_false' );
+
+		$this->sut = $this->create_sut( true );
+		$this->sut->set_compatibility_layer_flag();
+
+		$this->assertFalse(
+			$this->is_compatibility_layer_disabled(),
+			'Extensions should be able to keep the compatibility layer enabled on legacy templates.'
+		);
+	}
+
+	/**
+	 * @testdox Disables the compatibility layer when an extension overrides a blockified template default to true.
+	 */
+	public function test_disables_compatibility_layer_when_extension_overrides_blockified_default_to_true(): void {
+		add_filter( 'woocommerce_disable_compatibility_layer', '__return_true' );
+
+		$this->sut = $this->create_sut( false );
+		$this->sut->set_compatibility_layer_flag();
+
+		$this->assertTrue(
+			$this->is_compatibility_layer_disabled(),
+			'Extensions should be able to disable the compatibility layer on blockified templates.'
+		);
+	}
+
+	/**
+	 * Applies the compatibility layer filter the same way render callbacks do.
+	 *
+	 * @return bool
+	 */
+	private function is_compatibility_layer_disabled(): bool {
+		/**
+		 * Filter to disable the compatibility layer for the blockified templates.
+		 *
+		 * @since 7.6.0
+		 * @param bool $is_disabled_compatibility_layer Whether the compatibility layer should be disabled.
+		 */
+		return apply_filters( 'woocommerce_disable_compatibility_layer', false );
+	}
+
+	/**
+	 * Creates a test double with a fixed legacy-template detection result.
+	 *
+	 * @param bool $has_legacy_template_block Legacy template detection result to return.
+	 * @return AbstractTemplateCompatibility
+	 */
+	private function create_sut( bool $has_legacy_template_block ): AbstractTemplateCompatibility {
+		return new class( $has_legacy_template_block ) extends AbstractTemplateCompatibility {
+
+			/**
+			 * Whether the current template is detected as having a legacy template block.
+			 *
+			 * @var bool
+			 */
+			private $has_legacy_template_block;
+
+			/**
+			 * @param bool $has_legacy_template_block Legacy template detection result to return.
+			 */
+			public function __construct( bool $has_legacy_template_block ) {
+				$this->has_legacy_template_block = $has_legacy_template_block;
+			}
+
+			/**
+			 * @inheritdoc
+			 */
+			public function current_template_has_legacy_template_block() {
+				return $this->has_legacy_template_block;
+			}
+
+			/**
+			 * @param array       $parsed_block  The block being rendered.
+			 * @param array       $source_block  An un-modified copy of the parsed block.
+			 * @param object|null $parent_block  Parent block, if any.
+			 * @return array
+			 */
+			public function update_render_block_data( $parsed_block, $source_block, $parent_block ) {
+				unset( $source_block, $parent_block );
+
+				return $parsed_block;
+			}
+
+			/**
+			 * @param mixed $block_content The rendered block content.
+			 * @param mixed $block         The parsed block data.
+			 * @return string
+			 */
+			public function inject_hooks( $block_content, $block ) {
+				unset( $block );
+
+				return $block_content;
+			}
+
+			/**
+			 * @inheritdoc
+			 */
+			protected function set_hook_data() {
+				$this->hook_data = array();
+			}
+		};
+	}
+
+	/**
+	 * Removes filters registered during tests.
+	 */
+	private function remove_compatibility_layer_filters(): void {
+		remove_filter( 'woocommerce_disable_compatibility_layer', '__return_true' );
+		remove_filter( 'woocommerce_disable_compatibility_layer', '__return_false' );
+		remove_all_filters( 'woocommerce_disable_compatibility_layer' );
+	}
+}