Editor: Improve performance of _register_theme_block_patterns function.

The `_register_theme_block_patterns` function imposed a significant resource overhead. This issue primarily stems from themes, such as TT4, that register a substantial number of block patterns. These patterns necessitate numerous file operations, including file lookups, file reading into memory, and related processes. To provide an overview, the _register_theme_block_patterns function performed the following file operations:

- is_dir
- is_readable
- file_exists
- glob
- file_get_contents (utilized via get_file_data)

To address these issues, caching using a transient has been added to a new function call `_wp_get_block_patterns`. If theme development mode is disabled and theme exists, the block patterns are saved in a transient cache. This cache is used all requests after that, saving file lookups and reading files into memory. Cache invalidation is done, when themes are switched, deleted or updated. Meaning that block patterns are not stored in the cache incorrectly. 

Props flixos90, joemcgill, peterwilsoncc, costdev, swissspidy, aristath, westonruter, spacedmonkey.
Fixes #59490

git-svn-id: https://develop.svn.wordpress.org/trunk@56765 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Jonny Harris
2023-10-03 15:16:55 +00:00
parent d5505fc3b3
commit 3ad20183b0
8 changed files with 409 additions and 141 deletions

View File

@@ -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' );

View File

@@ -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.:
*
* <main><p><?php echo "Hello"; ?></p></main>
*
* 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.:
*
* <main><p><?php echo "Hello"; ?></p></main>
*
* 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' );

View File

@@ -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 );
}
/**

View File

@@ -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.
*

View File

@@ -0,0 +1,36 @@
<?php
/**
* Title: Centered Call To Action
* Slug: block-theme-patterns/cta
* Categories: call-to-action
*/
?>
<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"var:preset|spacing|50","bottom":"var:preset|spacing|50","left":"var:preset|spacing|50","right":"var:preset|spacing|50"}}},"layout":{"type":"constrained"}} -->
<div class="wp-block-group alignfull" style="padding-top:var(--wp--preset--spacing--50);padding-right:var(--wp--preset--spacing--50);padding-bottom:var(--wp--preset--spacing--50);padding-left:var(--wp--preset--spacing--50)"><!-- wp:group {"align":"wide","style":{"border":{"radius":"16px"},"spacing":{"padding":{"top":"var:preset|spacing|40","bottom":"var:preset|spacing|40","left":"var:preset|spacing|50","right":"var:preset|spacing|50"}}},"backgroundColor":"base-2","layout":{"type":"constrained"}} -->
<div class="wp-block-group alignwide has-base-2-background-color has-background" style="border-radius:16px;padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--50);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--50)"><!-- wp:spacer {"height":"var:preset|spacing|10"} -->
<div style="height:var(--wp--preset--spacing--10)" aria-hidden="true" class="wp-block-spacer"></div>
<!-- /wp:spacer -->
<!-- wp:heading {"textAlign":"center","fontSize":"x-large"} -->
<h2 class="wp-block-heading has-text-align-center has-x-large-font-size"><?php echo esc_html_x( 'Join 900+ subscribers', 'Sample text for Subscriber Heading with numbers', 'twentytwentyfour' ); ?></h2>
<!-- /wp:heading -->
<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center"><?php echo esc_html_x( 'Stay in the loop with everything you need to know.', 'Sample text for Subscriber Description', 'twentytwentyfour' ); ?></p>
<!-- /wp:paragraph -->
<!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} -->
<div class="wp-block-buttons"><!-- wp:button -->
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button"><?php echo esc_html_x( 'Sign up', 'Sample text for Sign Up Button', 'twentytwentyfour' ); ?></a></div>
<!-- /wp:button -->
</div>
<!-- /wp:buttons -->
<!-- wp:spacer {"height":"var:preset|spacing|10"} -->
<div style="height:var(--wp--preset--spacing--10)" aria-hidden="true" class="wp-block-spacer"></div>
<!-- /wp:spacer -->
</div>
<!-- /wp:group -->
</div>
<!-- /wp:group -->

View File

@@ -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
*/

View File

@@ -0,0 +1,145 @@
<?php
/**
* Tests for _wp_get_block_patterns.
*
* @package WordPress
* @subpackage Blocks
* @since 6.4.0
*
* @group blocks
*
* @covers ::_wp_get_block_patterns
*/
class Tests_Blocks_WpGetBlockPatterns extends WP_UnitTestCase {
/**
* @ticket 59490
*
* @dataProvider data_wp_get_block_patterns
*
* @param string $theme The theme's slug.
* @param array $expected The expected pattern data.
*/
public function test_should_return_block_patterns( $theme, $expected ) {
$patterns = _wp_get_block_patterns( wp_get_theme( $theme ) );
$this->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(),
),
),
);
}
}

View File

@@ -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',