diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index a283ef07..04a0bee6 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -28,7 +28,7 @@ 'native_constant_invocation' => true, 'native_function_casing' => true, 'native_function_invocation' => true, - 'native_function_type_declaration_casing' => true, + 'native_type_declaration_casing' => true, ] ); $config->setFinder( $finder ); diff --git a/CHANGELOG.md b/CHANGELOG.md index 00c855b4..131e0861 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ This library adheres to [Semantic Versioning](https://semver.org/) and [Keep a C * `disable_pantheon_constant_overrides`: Added a feature to disable forcing use of `WP_SITEURL` and `WP_HOME` on Pantheon environments. * `force_two_factor_authentication`: Added a feature to force Two Factor Authentication for users with `edit_posts` permissions. * `disable_deep_pagination`: Added a feature to restrict pagination to at most 100 pages, by default. This includes a filter `alleyvate_deep_pagination_max_pages` to override this limit, as well as a new `WP_Query` argument to override the limit: `__dangerously_set_max_pages`. +* `disable_block_editor_rest_api_preload_paths` Added a feature to disable preloading Synced Patterns (Reusable + Blocks) on the block edit screen to improve performance on sites with many patterns. ## 3.0.1 diff --git a/README.md b/README.md index 76339416..17e0e77c 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,16 @@ This feature removes selected nodes from the admin bar. This feature disables WordPress attachment pages entirely from the front end of the site. +### `disable_block_editor_rest_api_preload_paths` + +This feature enhances the stability and performance of the block edit screen by disabling the preloading of Synced +Patterns (Reusable Blocks). Typically, preloading triggers `the_content` filter for each block, along with +additional processing. This can lead to unexpected behavior and performance degradation, especially on sites with +hundreds of synced patterns. Notably, an error in a single block can propagate issues across all block edit screens. +Disabling preloading makes the system more resilient—less susceptible to cascading failures—thus improving overall +admin stability. For technical details on how WP core implements preloading, refer to +`wp-admin/edit-form-blocks.php.` + ### `disable_comments` This feature disables WordPress comments entirely, including the ability to post, view, edit, list, count, modify settings for, or access URLs that are related to comments completely. diff --git a/composer.json b/composer.json index c4cd0a11..caf8f5bc 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ ], "phpcbf": "phpcbf", "phpcs": "phpcs", - "phpstan": "phpstan --memory-limit=512M", + "phpstan": "phpstan --memory-limit=768M", "phpunit": "phpunit", "test": [ "@lint", diff --git a/src/alley/wp/alleyvate/features/class-disable-block-editor-rest-api-preload-paths.php b/src/alley/wp/alleyvate/features/class-disable-block-editor-rest-api-preload-paths.php new file mode 100644 index 00000000..921c9507 --- /dev/null +++ b/src/alley/wp/alleyvate/features/class-disable-block-editor-rest-api-preload-paths.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @package wp-alleyvate + */ + +declare( strict_types=1 ); + +namespace Alley\WP\Alleyvate\Features; + +use Alley\WP\Types\Feature; + +/** + * Disables the preloading of the blocks which happens on all edit post pages. + */ +final class Disable_Block_Editor_Rest_Api_Preload_Paths implements Feature { + /** + * Boot the feature. + */ + public function boot(): void { + add_filter( + 'block_editor_rest_api_preload_paths', + [ self::class, 'filter__block_editor_rest_api_preload_paths' ], + 9999 + ); + } + + /** + * Filter the block editor REST API preload paths. + * + * @param mixed[] $paths The paths to preload. + * + * @return mixed[] The filtered paths. + */ + public static function filter__block_editor_rest_api_preload_paths( $paths ) { + if ( ! \is_array( $paths ) ) { + return $paths; + } + return array_values( + array_filter( + $paths, + function ( $v ) { + // Remove the blocks preload path for performance reasons. + return ! \is_string( $v ) || ! str_starts_with( $v, '/wp/v2/blocks?context=edit' ); + }, + ) + ); + } +} diff --git a/src/alley/wp/alleyvate/load.php b/src/alley/wp/alleyvate/load.php index 548b5ab8..6cdb6ac4 100644 --- a/src/alley/wp/alleyvate/load.php +++ b/src/alley/wp/alleyvate/load.php @@ -97,6 +97,10 @@ function load(): void { 'disable_deep_pagination', new Features\Disable_Deep_Pagination(), ), + new Feature( + 'disable_block_editor_rest_api_preload_paths', + new Features\Disable_Block_Editor_Rest_Api_Preload_Paths(), + ), ); $plugin->boot(); diff --git a/tests/alley/wp/alleyvate/features/test-disable-block-editor-rest-api-preload-paths.php b/tests/alley/wp/alleyvate/features/test-disable-block-editor-rest-api-preload-paths.php new file mode 100644 index 00000000..60576591 --- /dev/null +++ b/tests/alley/wp/alleyvate/features/test-disable-block-editor-rest-api-preload-paths.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @package wp-alleyvate + */ + +declare( strict_types=1 ); + +namespace Alley\WP\Alleyvate\Features; + +use Mantle\Testkit\Test_Case; + +/** + * Tests for disabling block editor REST API preload paths. + */ +final class Test_Disable_Block_Editor_Rest_Api_Preload_Paths extends Test_Case { + + /** + * Feature instance. + * + * @var Disable_Block_Editor_Rest_Api_Preload_Paths + */ + private Disable_Block_Editor_Rest_Api_Preload_Paths $feature; + + /** + * Set up. + */ + protected function setUp(): void { + parent::setUp(); + + $this->feature = new Disable_Block_Editor_Rest_Api_Preload_Paths(); + } + + /** + * Test that the feature short-circuits a redirect that would otherwise occur. + */ + public function test_disable_block_editor_rest_api_preload_paths(): void { // phpcs:ignore Generic.NamingConventions.ConstructorName.OldStyle + + $post = self::factory()->post->create_and_get( + [ + 'post_title' => 'Testing REST API Preload Paths', + ] + ); + + /** + * This code mimics the logic in wp-admin/edit-form-blocks.php to generate the preload paths. + */ + $rest_path = rest_get_route_for_post( $post ); + $post_type = get_post_type( $post ); + $preload_paths = [ + '/wp/v2/types?context=view', + '/wp/v2/taxonomies?context=view', + add_query_arg( + [ + 'context' => 'edit', + 'per_page' => - 1, + ], + rest_get_route_for_post_type_items( 'wp_block' ) + ), + add_query_arg( 'context', 'edit', $rest_path ), + sprintf( '/wp/v2/types/%s?context=edit', $post_type ), + '/wp/v2/users/me', + [ rest_get_route_for_post_type_items( 'attachment' ), 'OPTIONS' ], + [ rest_get_route_for_post_type_items( 'page' ), 'OPTIONS' ], + [ rest_get_route_for_post_type_items( 'wp_block' ), 'OPTIONS' ], + [ rest_get_route_for_post_type_items( 'wp_template' ), 'OPTIONS' ], + sprintf( '%s/autosaves?context=edit', $rest_path ), + '/wp/v2/settings', + [ '/wp/v2/settings', 'OPTIONS' ], + ]; + + // Apply the filter that modifies the preload paths before the feature is activated. + $preload_paths = apply_filters( 'block_editor_rest_api_preload_paths', $preload_paths ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + + // Assert that the blocks rest path is in the preload paths. + $this->assertContains( '/wp/v2/blocks?context=edit&per_page=-1', $preload_paths ); + + // Check the other preload paths to ensure they are present. + $this->check_preloads_paths( $preload_paths, $rest_path, $post_type ); + + // Activate feature. + $this->feature->boot(); + + // Apply the filter that modifies the preload paths. + $preload_paths = apply_filters( 'block_editor_rest_api_preload_paths', $preload_paths ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + + // The blocks rest path should no longer be in the preload paths. + $this->assertNotContains( '/wp/v2/blocks?context=edit&per_page=-1', $preload_paths ); + + // Check the other preload paths to ensure they are present. + $this->check_preloads_paths( $preload_paths, $rest_path, $post_type ); + } + + /** + * Check the preload paths. + * + * @param mixed $preload_paths The preload paths. + * @param string $rest_path The rest path. + * @param string $post_type The post type. + * + * @return void + */ + public function check_preloads_paths( mixed $preload_paths, string $rest_path, string $post_type ): void { + // The other preload paths should still be present. + $this->assertContains( '/wp/v2/types?context=view', $preload_paths ); + $this->assertContains( '/wp/v2/taxonomies?context=view', $preload_paths ); + $this->assertContains( add_query_arg( 'context', 'edit', $rest_path ), $preload_paths ); + $this->assertContains( sprintf( '/wp/v2/types/%s?context=edit', $post_type ), $preload_paths ); + $this->assertContains( '/wp/v2/users/me', $preload_paths ); + $this->assertContains( [ rest_get_route_for_post_type_items( 'attachment' ), 'OPTIONS' ], $preload_paths ); + $this->assertContains( [ rest_get_route_for_post_type_items( 'page' ), 'OPTIONS' ], $preload_paths ); + $this->assertContains( [ rest_get_route_for_post_type_items( 'wp_block' ), 'OPTIONS' ], $preload_paths ); + $this->assertContains( [ rest_get_route_for_post_type_items( 'wp_template' ), 'OPTIONS' ], $preload_paths ); + $this->assertContains( sprintf( '%s/autosaves?context=edit', $rest_path ), $preload_paths ); + $this->assertContains( '/wp/v2/settings', $preload_paths ); + $this->assertContains( [ '/wp/v2/settings', 'OPTIONS' ], $preload_paths ); + } +}