diff --git a/src/wp-admin/includes/theme.php b/src/wp-admin/includes/theme.php
index b5579052a1..f822e57cf2 100644
--- a/src/wp-admin/includes/theme.php
+++ b/src/wp-admin/includes/theme.php
@@ -81,6 +81,8 @@ function delete_theme( $stylesheet, $redirect = '' ) {
*/
do_action( 'delete_theme', $stylesheet );
+ $theme = wp_get_theme( $stylesheet );
+
$themes_dir = trailingslashit( $themes_dir );
$theme_dir = trailingslashit( $themes_dir . $stylesheet );
$deleted = $wp_filesystem->delete( $theme_dir, true );
@@ -125,6 +127,9 @@ function delete_theme( $stylesheet, $redirect = '' ) {
WP_Theme::network_disable_theme( $stylesheet );
}
+ // Clear theme caches.
+ $theme->cache_delete();
+
// Force refresh of theme update information.
delete_site_transient( 'update_themes' );
diff --git a/src/wp-includes/block-patterns.php b/src/wp-includes/block-patterns.php
index 6f18672505..0bd5f4bdc8 100644
--- a/src/wp-includes/block-patterns.php
+++ b/src/wp-includes/block-patterns.php
@@ -319,37 +319,132 @@ function _register_remote_theme_patterns() {
/**
* Register any patterns that the active theme may provide under its
- * `./patterns/` directory. Each pattern is defined as a PHP file and defines
- * its metadata using plugin-style headers. The minimum required definition is:
- *
- * /**
- * * Title: My Pattern
- * * Slug: my-theme/my-pattern
- * *
- *
- * The output of the PHP source corresponds to the content of the pattern, e.g.:
- *
- *
- *
- * If applicable, this will collect from both parent and child theme.
- *
- * Other settable fields include:
- *
- * - Description
- * - Viewport Width
- * - Inserter (yes/no)
- * - Categories (comma-separated values)
- * - Keywords (comma-separated values)
- * - Block Types (comma-separated values)
- * - Post Types (comma-separated values)
- * - Template Types (comma-separated values)
+ * `./patterns/` directory.
*
* @since 6.0.0
* @since 6.1.0 The `postTypes` property was added.
* @since 6.2.0 The `templateTypes` property was added.
+ * @since 6.4.0 Uses the `_wp_get_block_patterns` function.
* @access private
*/
function _register_theme_block_patterns() {
+ /*
+ * Register patterns for the active theme. If the theme is a child theme,
+ * let it override any patterns from the parent theme that shares the same slug.
+ */
+ $themes = array();
+ $theme = wp_get_theme();
+ $themes[] = $theme;
+ if ( $theme->parent() ) {
+ $themes[] = $theme->parent();
+ }
+ $registry = WP_Block_Patterns_Registry::get_instance();
+
+ foreach ( $themes as $theme ) {
+ $pattern_data = _wp_get_block_patterns( $theme );
+ $dirpath = $theme->get_stylesheet_directory() . '/patterns/';
+ $text_domain = $theme->get( 'TextDomain' );
+
+ foreach ( $pattern_data['patterns'] as $file => $pattern_data ) {
+ if ( $registry->is_registered( $pattern_data['slug'] ) ) {
+ continue;
+ }
+
+ // The actual pattern content is the output of the file.
+ ob_start();
+ include $dirpath . $file;
+ $pattern_data['content'] = ob_get_clean();
+ if ( ! $pattern_data['content'] ) {
+ continue;
+ }
+
+ // Translate the pattern metadata.
+ // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain,WordPress.WP.I18n.LowLevelTranslationFunction
+ $pattern_data['title'] = translate_with_gettext_context( $pattern_data['title'], 'Pattern title', $text_domain );
+ if ( ! empty( $pattern_data['description'] ) ) {
+ // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain,WordPress.WP.I18n.LowLevelTranslationFunction
+ $pattern_data['description'] = translate_with_gettext_context( $pattern_data['description'], 'Pattern description', $text_domain );
+ }
+
+ register_block_pattern( $pattern_data['slug'], $pattern_data );
+ }
+ }
+}
+add_action( 'init', '_register_theme_block_patterns' );
+
+/**
+ * Gets block pattern data for a specified theme.
+ * Each pattern is defined as a PHP file and defines
+ * its metadata using plugin-style headers. The minimum required definition is:
+ *
+ * /**
+ * * Title: My Pattern
+ * * Slug: my-theme/my-pattern
+ * *
+ *
+ * The output of the PHP source corresponds to the content of the pattern, e.g.:
+ *
+ *
+ *
+ * If applicable, this will collect from both parent and child theme.
+ *
+ * Other settable fields include:
+ *
+ * - Description
+ * - Viewport Width
+ * - Inserter (yes/no)
+ * - Categories (comma-separated values)
+ * - Keywords (comma-separated values)
+ * - Block Types (comma-separated values)
+ * - Post Types (comma-separated values)
+ * - Template Types (comma-separated values)
+ *
+ * @since 6.4.0
+ * @access private
+ *
+ * @param WP_Theme $theme Theme object.
+ * @return array Block pattern data.
+ */
+
+function _wp_get_block_patterns( WP_Theme $theme ) {
+ if ( ! $theme->exists() ) {
+ return array(
+ 'version' => false,
+ 'patterns' => array(),
+ );
+ }
+
+ $transient_name = 'wp_theme_patterns_' . $theme->get_stylesheet();
+ $version = $theme->get( 'Version' );
+ $can_use_cached = ! wp_is_development_mode( 'theme' );
+
+ if ( $can_use_cached ) {
+ $pattern_data = get_transient( $transient_name );
+ if ( is_array( $pattern_data ) && $pattern_data['version'] === $version ) {
+ return $pattern_data;
+ }
+ }
+
+ $pattern_data = array(
+ 'version' => $version,
+ 'patterns' => array(),
+ );
+ $dirpath = $theme->get_stylesheet_directory() . '/patterns/';
+
+ if ( ! file_exists( $dirpath ) ) {
+ if ( $can_use_cached ) {
+ set_transient( $transient_name, $pattern_data );
+ }
+ return $pattern_data;
+ }
+ $files = glob( $dirpath . '*.php' );
+ if ( ! $files ) {
+ if ( $can_use_cached ) {
+ set_transient( $transient_name, $pattern_data );
+ }
+ return $pattern_data;
+ }
+
$default_headers = array(
'title' => 'Title',
'slug' => 'Slug',
@@ -363,130 +458,94 @@ function _register_theme_block_patterns() {
'templateTypes' => 'Template Types',
);
- /*
- * Register patterns for the active theme. If the theme is a child theme,
- * let it override any patterns from the parent theme that shares the same slug.
- */
- $themes = array();
- $stylesheet = get_stylesheet();
- $template = get_template();
- if ( $stylesheet !== $template ) {
- $themes[] = wp_get_theme( $stylesheet );
- }
- $themes[] = wp_get_theme( $template );
+ $properties_to_parse = array(
+ 'categories',
+ 'keywords',
+ 'blockTypes',
+ 'postTypes',
+ 'templateTypes',
+ );
- foreach ( $themes as $theme ) {
- $dirpath = $theme->get_stylesheet_directory() . '/patterns/';
- if ( ! is_dir( $dirpath ) || ! is_readable( $dirpath ) ) {
+ foreach ( $files as $file ) {
+ $pattern = get_file_data( $file, $default_headers );
+
+ if ( empty( $pattern['slug'] ) ) {
+ _doing_it_wrong(
+ __FUNCTION__,
+ sprintf(
+ /* translators: %s: file name. */
+ __( 'Could not register file "%s" as a block pattern ("Slug" field missing)' ),
+ $file
+ ),
+ '6.0.0'
+ );
continue;
}
- if ( file_exists( $dirpath ) ) {
- $files = glob( $dirpath . '*.php' );
- if ( $files ) {
- foreach ( $files as $file ) {
- $pattern_data = get_file_data( $file, $default_headers );
- if ( empty( $pattern_data['slug'] ) ) {
- _doing_it_wrong(
- '_register_theme_block_patterns',
- sprintf(
- /* translators: %s: file name. */
- __( 'Could not register file "%s" as a block pattern ("Slug" field missing)' ),
- $file
- ),
- '6.0.0'
- );
- continue;
- }
+ if ( ! preg_match( '/^[A-z0-9\/_-]+$/', $pattern['slug'] ) ) {
+ _doing_it_wrong(
+ __FUNCTION__,
+ sprintf(
+ /* translators: %1s: file name; %2s: slug value found. */
+ __( 'Could not register file "%1$s" as a block pattern (invalid slug "%2$s")' ),
+ $file,
+ $pattern['slug']
+ ),
+ '6.0.0'
+ );
+ }
- if ( ! preg_match( '/^[A-z0-9\/_-]+$/', $pattern_data['slug'] ) ) {
- _doing_it_wrong(
- '_register_theme_block_patterns',
- sprintf(
- /* translators: %1s: file name; %2s: slug value found. */
- __( 'Could not register file "%1$s" as a block pattern (invalid slug "%2$s")' ),
- $file,
- $pattern_data['slug']
- ),
- '6.0.0'
- );
- }
+ // Title is a required property.
+ if ( ! $pattern['title'] ) {
+ _doing_it_wrong(
+ __FUNCTION__,
+ sprintf(
+ /* translators: %1s: file name. */
+ __( 'Could not register file "%s" as a block pattern ("Title" field missing)' ),
+ $file
+ ),
+ '6.0.0'
+ );
+ continue;
+ }
- if ( WP_Block_Patterns_Registry::get_instance()->is_registered( $pattern_data['slug'] ) ) {
- continue;
- }
-
- // Title is a required property.
- if ( ! $pattern_data['title'] ) {
- _doing_it_wrong(
- '_register_theme_block_patterns',
- sprintf(
- /* translators: %1s: file name; %2s: slug value found. */
- __( 'Could not register file "%s" as a block pattern ("Title" field missing)' ),
- $file
- ),
- '6.0.0'
- );
- continue;
- }
-
- // For properties of type array, parse data as comma-separated.
- foreach ( array( 'categories', 'keywords', 'blockTypes', 'postTypes', 'templateTypes' ) as $property ) {
- if ( ! empty( $pattern_data[ $property ] ) ) {
- $pattern_data[ $property ] = array_filter(
- preg_split(
- '/[\s,]+/',
- (string) $pattern_data[ $property ]
- )
- );
- } else {
- unset( $pattern_data[ $property ] );
- }
- }
-
- // Parse properties of type int.
- foreach ( array( 'viewportWidth' ) as $property ) {
- if ( ! empty( $pattern_data[ $property ] ) ) {
- $pattern_data[ $property ] = (int) $pattern_data[ $property ];
- } else {
- unset( $pattern_data[ $property ] );
- }
- }
-
- // Parse properties of type bool.
- foreach ( array( 'inserter' ) as $property ) {
- if ( ! empty( $pattern_data[ $property ] ) ) {
- $pattern_data[ $property ] = in_array(
- strtolower( $pattern_data[ $property ] ),
- array( 'yes', 'true' ),
- true
- );
- } else {
- unset( $pattern_data[ $property ] );
- }
- }
-
- // Translate the pattern metadata.
- $text_domain = $theme->get( 'TextDomain' );
- // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain,WordPress.WP.I18n.LowLevelTranslationFunction
- $pattern_data['title'] = translate_with_gettext_context( $pattern_data['title'], 'Pattern title', $text_domain );
- if ( ! empty( $pattern_data['description'] ) ) {
- // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain,WordPress.WP.I18n.LowLevelTranslationFunction
- $pattern_data['description'] = translate_with_gettext_context( $pattern_data['description'], 'Pattern description', $text_domain );
- }
-
- // The actual pattern content is the output of the file.
- ob_start();
- include $file;
- $pattern_data['content'] = ob_get_clean();
- if ( ! $pattern_data['content'] ) {
- continue;
- }
-
- register_block_pattern( $pattern_data['slug'], $pattern_data );
- }
+ // For properties of type array, parse data as comma-separated.
+ foreach ( $properties_to_parse as $property ) {
+ if ( ! empty( $pattern[ $property ] ) ) {
+ $pattern[ $property ] = array_filter( wp_parse_list( (string) $pattern[ $property ] ) );
+ } else {
+ unset( $pattern[ $property ] );
}
}
+
+ // Parse properties of type int.
+ $property = 'viewportWidth';
+ if ( ! empty( $pattern[ $property ] ) ) {
+ $pattern[ $property ] = (int) $pattern[ $property ];
+ } else {
+ unset( $pattern[ $property ] );
+ }
+
+ // Parse properties of type bool.
+ $property = 'inserter';
+ if ( ! empty( $pattern[ $property ] ) ) {
+ $pattern[ $property ] = in_array(
+ strtolower( $pattern[ $property ] ),
+ array( 'yes', 'true' ),
+ true
+ );
+ } else {
+ unset( $pattern[ $property ] );
+ }
+
+ $key = str_replace( $dirpath, '', $file );
+
+ $pattern_data['patterns'][ $key ] = $pattern;
}
+
+ if ( $can_use_cached ) {
+ set_transient( $transient_name, $pattern_data );
+ }
+
+ return $pattern_data;
}
-add_action( 'init', '_register_theme_block_patterns' );
diff --git a/src/wp-includes/class-wp-theme.php b/src/wp-includes/class-wp-theme.php
index d7c36440ee..e5142d0f08 100644
--- a/src/wp-includes/class-wp-theme.php
+++ b/src/wp-includes/class-wp-theme.php
@@ -821,6 +821,16 @@ final class WP_Theme implements ArrayAccess {
$this->block_template_folders = null;
$this->headers = array();
$this->__construct( $this->stylesheet, $this->theme_root );
+ $this->delete_pattern_cache();
+ }
+
+ /**
+ * Clear block pattern cache.
+ *
+ * @since 6.4.0
+ */
+ public function delete_pattern_cache() {
+ delete_transient( 'wp_theme_patterns_' . $this->stylesheet );
}
/**
diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php
index 1eccd460b1..b5fba76159 100644
--- a/src/wp-includes/theme.php
+++ b/src/wp-includes/theme.php
@@ -873,6 +873,10 @@ function switch_theme( $stylesheet ) {
$wp_stylesheet_path = null;
$wp_template_path = null;
+ // Clear pattern caches.
+ $new_theme->delete_pattern_cache();
+ $old_theme->delete_pattern_cache();
+
/**
* Fires after the theme is switched.
*
diff --git a/tests/phpunit/data/themedir1/block-theme-patterns/patterns/cta.php b/tests/phpunit/data/themedir1/block-theme-patterns/patterns/cta.php
new file mode 100644
index 0000000000..f6e0b1d308
--- /dev/null
+++ b/tests/phpunit/data/themedir1/block-theme-patterns/patterns/cta.php
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/phpunit/data/themedir1/block-theme-patterns/style.css b/tests/phpunit/data/themedir1/block-theme-patterns/style.css
new file mode 100644
index 0000000000..5bb4fdb800
--- /dev/null
+++ b/tests/phpunit/data/themedir1/block-theme-patterns/style.css
@@ -0,0 +1,8 @@
+/*
+Theme Name: Block Theme Patterns
+Theme URI: https://wordpress.org/
+Description: For testing purposes only.
+Template: block-theme
+Version: 1.0.0
+Text Domain: block-theme-patterns
+*/
diff --git a/tests/phpunit/tests/blocks/wpGetBlockPatterns.php b/tests/phpunit/tests/blocks/wpGetBlockPatterns.php
new file mode 100644
index 0000000000..6b05aa1528
--- /dev/null
+++ b/tests/phpunit/tests/blocks/wpGetBlockPatterns.php
@@ -0,0 +1,145 @@
+assertSameSets( $expected, $patterns );
+ }
+
+ /**
+ * @ticket 59490
+ */
+ public function test_delete_theme_cache() {
+ $theme = wp_get_theme( 'block-theme-patterns' );
+ _wp_get_block_patterns( $theme );
+ $transient = get_transient( 'wp_theme_patterns_block-theme-patterns' );
+ $this->assertSameSets(
+ array(
+ 'version' => '1.0.0',
+ 'patterns' => array(
+ 'cta.php' => array(
+ 'title' => 'Centered Call To Action',
+ 'slug' => 'block-theme-patterns/cta',
+ 'description' => '',
+ 'categories' => array( 'call-to-action' ),
+ ),
+ ),
+ ),
+ $transient,
+ 'The transient for block theme patterns should be set'
+ );
+ $theme->cache_delete();
+ $transient = get_transient( 'wp_theme_patterns_block-theme-patterns' );
+ $this->assertFalse(
+ $transient,
+ 'The transient for block theme patterns should have been cleared'
+ );
+ }
+
+ /**
+ * @ticket 59490
+ */
+ public function test_should_clear_transient_after_switching_theme() {
+ switch_theme( 'block-theme' );
+ _wp_get_block_patterns( wp_get_theme() );
+ $this->assertSameSets(
+ array(
+ 'version' => '1.0.0',
+ 'patterns' => array(),
+ ),
+ get_transient( 'wp_theme_patterns_block-theme' ),
+ 'The transient for block theme should be set'
+ );
+ switch_theme( 'block-theme-patterns' );
+ $this->assertFalse( get_transient( 'wp_theme_patterns_block-theme' ), 'Transient should not be set for block theme after switch theme' );
+ $this->assertFalse( get_transient( 'wp_theme_patterns_block-theme-patterns' ), 'Transient should not be set for block theme patterns before being requested' );
+ _wp_get_block_patterns( wp_get_theme() );
+ $transient = get_transient( 'wp_theme_patterns_block-theme-patterns' );
+ $this->assertSameSets(
+ array(
+ 'version' => '1.0.0',
+ 'patterns' => array(
+ 'cta.php' => array(
+ 'title' => 'Centered Call To Action',
+ 'slug' => 'block-theme-patterns/cta',
+ 'description' => '',
+ 'categories' => array( 'call-to-action' ),
+ ),
+ ),
+ ),
+ $transient,
+ 'The transient for block theme patterns should be set'
+ );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_wp_get_block_patterns() {
+ return array(
+ array(
+ 'theme' => 'block-theme',
+ 'patterns' => array(
+ 'version' => '1.0.0',
+ 'patterns' => array(),
+ ),
+ ),
+ array(
+ 'theme' => 'block-theme-child',
+ 'patterns' => array(
+ 'version' => '1.0.0',
+ 'patterns' => array(),
+ ),
+ ),
+ array(
+ 'theme' => 'block-theme-patterns',
+ 'patterns' => array(
+ 'version' => '1.0.0',
+ 'patterns' => array(
+ 'cta.php' => array(
+ 'title' => 'Centered Call To Action',
+ 'slug' => 'block-theme-patterns/cta',
+ 'description' => '',
+ 'categories' => array( 'call-to-action' ),
+ ),
+ ),
+ ),
+ ),
+ array(
+ 'theme' => 'broken-theme',
+ 'patterns' => array(
+ 'version' => false,
+ 'patterns' => array(),
+ ),
+ ),
+ array(
+ 'theme' => 'invalid',
+ 'patterns' => array(
+ 'version' => false,
+ 'patterns' => array(),
+ ),
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/tests/theme/themeDir.php b/tests/phpunit/tests/theme/themeDir.php
index a01299b327..1d6fb45db3 100644
--- a/tests/phpunit/tests/theme/themeDir.php
+++ b/tests/phpunit/tests/theme/themeDir.php
@@ -185,6 +185,7 @@ class Tests_Theme_ThemeDir extends WP_UnitTestCase {
'Block Theme [0.4.0]',
'Block Theme [1.0.0] in subdirectory',
'Block Theme Deprecated Path',
+ 'Block Theme Patterns',
'Block Theme Post Content Default',
'Block Theme with defined Typography Fonts',
'Block Theme with Hooked Blocks',