Commit bfddbced1cd for woocommerce

commit bfddbced1cd5cbddc6c3853a6508695daf277762
Author: Raluca Stan <ralucastn@gmail.com>
Date:   Thu Jun 25 16:26:04 2026 +0200

    Performance: lazily load block pattern content (#65922)

    * perf: lazily load block pattern content via core filePath mechanism

    BlockPatterns::register_block_patterns() previously ran ob_start();
    include; ob_get_clean() for all ~41 pattern files on every request
    (front-end, REST, cron), costing ~7-8 ms even where patterns are never
    consumed.

    Register patterns with core's `filePath` property (WP 6.5+) instead, so
    WP_Block_Patterns_Registry loads and caches the content lazily only when
    a pattern is actually requested. PatternRegistry now accepts a pattern
    that carries `filePath` in place of inline `content`. No content
    processing was lost: the WC registry only translates metadata and
    registers categories, none of which depend on the content.

    Removes the now-obsolete load_pattern_content() PHPStan baseline entry.

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    * perf: guard lazy pattern filePath against missing files

    Confirm a pattern's source file still exists before registering it with a
    `filePath`. The pattern list can come from a stale site transient, and
    without inline content core would emit an undefined-content warning when it
    tried to lazily load a missing file on demand. This mirrors core's own
    `_register_theme_block_patterns()` file_exists guard.

    Adds a regression test for the skip path and points the cached-pattern test
    at an existing fixture so the guard is satisfied.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    * Guard pattern source against malformed cache entries

    Validate that a cached pattern's `source` is a non-empty string before
    dereferencing it to build the path, so a stale or malformed site transient
    can't raise a PHP warning before the file_exists guard. Adds a regression test.

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/perf-lazy-block-patterns b/plugins/woocommerce/changelog/perf-lazy-block-patterns
new file mode 100644
index 00000000000..129c32f09d8
--- /dev/null
+++ b/plugins/woocommerce/changelog/perf-lazy-block-patterns
@@ -0,0 +1,3 @@
+Significance: patch
+Type: performance
+Comment: Load WooCommerce block pattern content lazily via core's `filePath` mechanism instead of including all pattern files on every request, saving ~7-8 ms on requests that do not consume patterns.
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index c6f0a356591..60057b96c5b 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -49725,12 +49725,6 @@ parameters:
 			count: 1
 			path: src/Blocks/AssetsController.php

-		-
-			message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockPatterns\:\:load_pattern_content\(\) should return string but returns string\|false\.$#'
-			identifier: return.type
-			count: 1
-			path: src/Blocks/BlockPatterns.php
-
 		-
 			message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockPatterns\:\:set_pattern_cache\(\) has no return type specified\.$#'
 			identifier: missingType.return
diff --git a/plugins/woocommerce/src/Blocks/BlockPatterns.php b/plugins/woocommerce/src/Blocks/BlockPatterns.php
index 3767a854c5d..1a3c939fc44 100644
--- a/plugins/woocommerce/src/Blocks/BlockPatterns.php
+++ b/plugins/woocommerce/src/Blocks/BlockPatterns.php
@@ -80,25 +80,16 @@ class BlockPatterns {
 		}
 	}

-	/**
-	 * Loads the content of a pattern.
-	 *
-	 * @param string $pattern_path The path to the pattern.
-	 * @return string The content of the pattern.
-	 */
-	private function load_pattern_content( $pattern_path ) {
-		if ( ! file_exists( $pattern_path ) ) {
-			return '';
-		}
-
-		ob_start();
-		include $pattern_path;
-		return ob_get_clean();
-	}
-
 	/**
 	 * Register block patterns from core.
 	 *
+	 * The pattern content is not loaded here. Instead, each pattern is registered
+	 * with the absolute path to its source file (via the `filePath` property), so
+	 * that core's `WP_Block_Patterns_Registry` loads (and caches) the content
+	 * lazily, only when a pattern is actually requested. This avoids the cost of
+	 * `include`-ing all pattern files on every request (e.g. front-end, REST,
+	 * cron), where patterns are never consumed.
+	 *
 	 * @return void
 	 */
 	public function register_block_patterns() {
@@ -108,11 +99,25 @@ class BlockPatterns {

 		$patterns = $this->get_block_patterns();
 		foreach ( $patterns as $pattern ) {
-			$pattern_path      = $this->patterns_path . '/' . $pattern['source'];
-			$pattern['source'] = $pattern_path;
+			// The pattern list can come from a stale or malformed site transient, so make sure the source is a
+			// usable string before dereferencing it, to avoid a PHP warning when building the path.
+			if ( empty( $pattern['source'] ) || ! is_string( $pattern['source'] ) ) {
+				continue;
+			}
+
+			$pattern_path = $this->patterns_path . '/' . $pattern['source'];
+
+			// The pattern list can come from a stale cache, so confirm the file
+			// still exists before registering it with a `filePath`. Without
+			// inline content, core would otherwise emit an undefined-content
+			// warning when it tries to load a missing file on demand. This
+			// mirrors core's own `_register_theme_block_patterns()` guard.
+			if ( ! file_exists( $pattern_path ) ) {
+				continue;
+			}

-			$content            = $this->load_pattern_content( $pattern_path );
-			$pattern['content'] = $content;
+			$pattern['source']   = $pattern_path;
+			$pattern['filePath'] = $pattern_path;

 			$this->pattern_registry->register_block_pattern( $pattern_path, $pattern );
 		}
diff --git a/plugins/woocommerce/src/Blocks/Patterns/PatternRegistry.php b/plugins/woocommerce/src/Blocks/Patterns/PatternRegistry.php
index dc7177ac5de..04a5453614c 100644
--- a/plugins/woocommerce/src/Blocks/Patterns/PatternRegistry.php
+++ b/plugins/woocommerce/src/Blocks/Patterns/PatternRegistry.php
@@ -149,10 +149,20 @@ class PatternRegistry {
 			$pattern_data['description'] = translate_with_gettext_context( $pattern_data['description'], 'Pattern description', 'woocommerce' );
 		}

-		if ( empty( $pattern_data['content'] ) ) {
+		// A pattern is registrable as long as it provides either inline content
+		// or a `filePath` that core can load lazily (WP 6.5+). Bail only when
+		// neither is available.
+		if ( empty( $pattern_data['content'] ) && empty( $pattern_data['filePath'] ) ) {
 			return;
 		}

+		// When a `filePath` is provided, let core load the content lazily on
+		// demand. Drop any empty `content` so core falls back to `filePath`
+		// instead of registering an empty pattern.
+		if ( ! empty( $pattern_data['filePath'] ) && empty( $pattern_data['content'] ) ) {
+			unset( $pattern_data['content'] );
+		}
+
 		$category_labels = $this->get_category_labels();

 		if ( ! empty( $pattern_data['categories'] ) ) {
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockPatterns/BlockPatterns.php b/plugins/woocommerce/tests/php/src/Blocks/BlockPatterns/BlockPatterns.php
index 552a026f587..e2b77199d4e 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockPatterns/BlockPatterns.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockPatterns/BlockPatterns.php
@@ -59,14 +59,8 @@ class BlockPatterns extends \WP_UnitTestCase {
 	 */
 	public function test_block_patterns_registration() {

-		ob_start();
-		include __DIR__ . '/patterns/mock-header.php';
-		$mock_header_content = ob_get_clean();
-
-		ob_start();
-		include __DIR__ . '/patterns/mock-footer.php';
-		$mock_footer_content = ob_get_clean();
-
+		// Content is no longer loaded eagerly. Patterns are registered with a
+		// `filePath` so core can load the content lazily on demand.
 		$this->pattern_registry
 			->expects( $this->exactly( 2 ) )
 			->method( 'register_block_pattern' )
@@ -85,7 +79,7 @@ class BlockPatterns extends \WP_UnitTestCase {
 						'featureFlag'   => '',
 						'templateTypes' => '',
 						'source'        => __DIR__ . '/patterns/mock-footer.php',
-						'content'       => $mock_footer_content,
+						'filePath'      => __DIR__ . '/patterns/mock-footer.php',
 					),
 				),
 				array(
@@ -102,7 +96,7 @@ class BlockPatterns extends \WP_UnitTestCase {
 						'featureFlag'   => '',
 						'templateTypes' => '',
 						'source'        => __DIR__ . '/patterns/mock-header.php',
-						'content'       => $mock_header_content,
+						'filePath'      => __DIR__ . '/patterns/mock-header.php',
 					),
 				),
 			);
@@ -114,10 +108,13 @@ class BlockPatterns extends \WP_UnitTestCase {
 	 * Tests if patterns are registered with the cached data.
 	 */
 	public function test_cached_block_patterns_registration() {
+		// The cached source points at a file that still exists on disk, so it is
+		// registered with a `filePath` for lazy loading. The registry is mocked,
+		// so the file is never actually read; only its existence matters here.
 		$mock_patterns = array(
 			array(
 				'title'   => 'Mock Cached',
-				'source'  => 'mock-cached.php',
+				'source'  => 'mock-header.php',
 				'content' => '',
 			),
 		);
@@ -128,21 +125,76 @@ class BlockPatterns extends \WP_UnitTestCase {

 		set_site_transient( 'woocommerce_blocks_patterns', $pattern_data );

-		$expected_pattern            = $mock_patterns[0];
-		$expected_pattern['source']  = __DIR__ . '/patterns/mock-cached.php';
-		$expected_pattern['content'] = '';
+		$expected_pattern             = $mock_patterns[0];
+		$expected_pattern['source']   = __DIR__ . '/patterns/mock-header.php';
+		$expected_pattern['content']  = '';
+		$expected_pattern['filePath'] = __DIR__ . '/patterns/mock-header.php';

 		$this->pattern_registry
 			->expects( $this->exactly( 1 ) )
 			->method( 'register_block_pattern' )
 			->with(
-				__DIR__ . '/patterns/mock-cached.php',
+				__DIR__ . '/patterns/mock-header.php',
 				$expected_pattern,
 			);

 		$this->block_patterns->register_block_patterns();
 	}

+	/**
+	 * Tests that a cached pattern whose source file no longer exists on disk is
+	 * skipped, so core is never asked to lazily load a missing file.
+	 */
+	public function test_cached_pattern_with_missing_file_is_skipped() {
+		$pattern_data = array(
+			'version'  => WOOCOMMERCE_VERSION,
+			'patterns' => array(
+				array(
+					'title'   => 'Mock Missing',
+					'source'  => 'does-not-exist.php',
+					'content' => '',
+				),
+			),
+		);
+
+		set_site_transient( 'woocommerce_blocks_patterns', $pattern_data );
+
+		$this->pattern_registry
+			->expects( $this->never() )
+			->method( 'register_block_pattern' );
+
+		$this->block_patterns->register_block_patterns();
+	}
+
+	/**
+	 * Tests that a cached pattern with a missing or invalid source is skipped, so the source is never
+	 * dereferenced (which would raise a PHP warning) when building the pattern path.
+	 */
+	public function test_cached_pattern_with_invalid_source_is_skipped() {
+		$pattern_data = array(
+			'version'  => WOOCOMMERCE_VERSION,
+			'patterns' => array(
+				array(
+					'title'   => 'No Source',
+					'content' => '',
+				),
+				array(
+					'title'   => 'Invalid Source',
+					'source'  => array( 'not', 'a', 'string' ),
+					'content' => '',
+				),
+			),
+		);
+
+		set_site_transient( 'woocommerce_blocks_patterns', $pattern_data );
+
+		$this->pattern_registry
+			->expects( $this->never() )
+			->method( 'register_block_pattern' );
+
+		$this->block_patterns->register_block_patterns();
+	}
+
 	/**
 	 * Tests if patterns are registered with the cached data.
 	 */
@@ -160,14 +212,8 @@ class BlockPatterns extends \WP_UnitTestCase {

 		set_site_transient( 'woocommerce_blocks_patterns', $pattern_data );

-		ob_start();
-		include __DIR__ . '/patterns/mock-header.php';
-		$mock_header_content = ob_get_clean();
-
-		ob_start();
-		include __DIR__ . '/patterns/mock-footer.php';
-		$mock_footer_content = ob_get_clean();
-
+		// Content is no longer loaded eagerly. Patterns are registered with a
+		// `filePath` so core can load the content lazily on demand.
 		$this->pattern_registry
 			->expects( $this->exactly( 2 ) )
 			->method( 'register_block_pattern' )
@@ -186,7 +232,7 @@ class BlockPatterns extends \WP_UnitTestCase {
 						'featureFlag'   => '',
 						'templateTypes' => '',
 						'source'        => __DIR__ . '/patterns/mock-footer.php',
-						'content'       => $mock_footer_content,
+						'filePath'      => __DIR__ . '/patterns/mock-footer.php',
 					),
 				),
 				array(
@@ -203,7 +249,7 @@ class BlockPatterns extends \WP_UnitTestCase {
 						'featureFlag'   => '',
 						'templateTypes' => '',
 						'source'        => __DIR__ . '/patterns/mock-header.php',
-						'content'       => $mock_header_content,
+						'filePath'      => __DIR__ . '/patterns/mock-header.php',
 					),
 				),
 			);