diff --git a/src/wp-includes/class-wp-icons-registry.php b/src/wp-includes/class-wp-icons-registry.php index 1851a4150a057..4d2288a0a2218 100644 --- a/src/wp-includes/class-wp-icons-registry.php +++ b/src/wp-includes/class-wp-icons-registry.php @@ -20,6 +20,14 @@ class WP_Icons_Registry { */ protected $registered_icons = array(); + /** + * Whether the default icons bundled with core have been registered yet. + * + * @since 7.1.0 + * @var bool + */ + protected $default_icons_loaded = false; + /** * Container for the main instance of the class. * @@ -336,6 +344,8 @@ public function get_registered_icon( $icon_name ) { * @return array[] Array of arrays containing the registered icon properties. */ public function get_registered_icons( $search = '' ) { + $this->load_default_icons(); + $icons = array(); foreach ( $this->registered_icons as $icon ) { @@ -362,9 +372,35 @@ public function get_registered_icons( $search = '' ) { * @return bool True if the icon is registered, false otherwise. */ public function is_registered( $icon_name ) { + $this->load_default_icons(); + return isset( $this->registered_icons[ $icon_name ] ); } + /** + * Registers the default icons bundled with core, once, on first use. + * + * The core icon manifest declares 88 icons, each with a translated label. + * Registering them eagerly costs every request — including front-end + * requests that never render an icon — so registration is deferred until + * something actually reads the registry. + * + * @since 7.1.0 + */ + protected function load_default_icons() { + if ( $this->default_icons_loaded ) { + return; + } + + /* + * Set before registering: _wp_register_default_icons() calls back into + * this class, and this flag is what stops that from recursing. + */ + $this->default_icons_loaded = true; + + _wp_register_default_icons(); + } + /** * Utility method to retrieve the main instance of the class. * diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 12ca0045b98b4..64d49f8cd9aa1 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -816,7 +816,10 @@ // Icons. add_action( 'init', '_wp_register_default_icon_collections', 0 ); -add_action( 'init', '_wp_register_default_icons' ); +/* + * The default icons themselves are registered lazily, on first read of the + * icons registry. See WP_Icons_Registry::load_default_icons(). + */ // Add ignoredHookedBlocks metadata attribute to the template and template part post types. add_filter( 'rest_pre_insert_wp_template', 'inject_ignored_hooked_blocks_metadata_attributes' ); diff --git a/src/wp-includes/icons.php b/src/wp-includes/icons.php index 0af4b6d61b873..144a29d531a16 100644 --- a/src/wp-includes/icons.php +++ b/src/wp-includes/icons.php @@ -98,6 +98,15 @@ function _wp_register_default_icon_collections() { * @access private */ function _wp_register_default_icons() { + /* + * Icons can be requested before `init` now that the default icons are + * registered on first read, so make sure the collection they belong to + * exists rather than relying on the `init` callback having run. + */ + if ( ! WP_Icon_Collections_Registry::get_instance()->is_registered( 'core' ) ) { + _wp_register_default_icon_collections(); + } + $icons_directory = ABSPATH . WPINC . '/images/icon-library/'; $manifest_path = ABSPATH . WPINC . '/assets/icon-library-manifest.php'; diff --git a/tests/phpunit/tests/icons/wpIconsRegistry.php b/tests/phpunit/tests/icons/wpIconsRegistry.php index 42853d2666a95..a68d75a3a9715 100644 --- a/tests/phpunit/tests/icons/wpIconsRegistry.php +++ b/tests/phpunit/tests/icons/wpIconsRegistry.php @@ -453,4 +453,108 @@ public function test_get_content_returns_null_for_invalid_file( $contents, $exte $this->assertNull( $icon['content'] ); } + + /** + * Resets the registry singleton so a test starts with the default icons + * unloaded. + * + * Needed because tear_down() unregisters a test collection, and collection + * unregistration reads the icon registry in order to drop that collection's + * icons — which triggers the lazy load into the replacement instance. + */ + private function reset_registry() { + $reflection = new ReflectionClass( WP_Icons_Registry::class ); + $property = $reflection->getProperty( 'instance' ); + if ( PHP_VERSION_ID < 80100 ) { + $property->setAccessible( true ); + } + $property->setValue( null, null ); + + $this->registry = WP_Icons_Registry::get_instance(); + } + + /** + * Reads the registered icons property directly, so that the assertion itself + * does not trigger the lazy registration it is checking for. + * + * @return array Registered icons. + */ + private function get_registered_icons_property() { + $reflection = new ReflectionClass( WP_Icons_Registry::class ); + $property = $reflection->getProperty( 'registered_icons' ); + if ( PHP_VERSION_ID < 80100 ) { + $property->setAccessible( true ); + } + + return $property->getValue( $this->registry ); + } + + /** + * @ticket 00000 + * + * @covers ::load_default_icons + */ + public function test_default_icons_are_not_registered_until_the_registry_is_read() { + $this->reset_registry(); + + $this->assertSame( + array(), + $this->get_registered_icons_property(), + 'The default icons should not be registered before the registry is read.' + ); + } + + /** + * @ticket 00000 + * + * @covers ::load_default_icons + */ + public function test_default_icons_are_registered_on_first_read() { + $this->reset_registry(); + + $icon = $this->registry->get_registered_icon( 'core/arrow-left' ); + + $this->assertIsArray( $icon, 'A core icon should resolve without explicit registration.' ); + $this->assertSame( 'core/arrow-left', $icon['name'] ); + $this->assertStringStartsWith( 'assertNotEmpty( + $this->get_registered_icons_property(), + 'Reading the registry should have registered the default icons.' + ); + } + + /** + * @ticket 00000 + * + * @covers ::is_registered + */ + public function test_is_registered_triggers_default_icon_registration() { + $this->reset_registry(); + + $this->assertTrue( $this->registry->is_registered( 'core/arrow-left' ) ); + } + + /** + * The default icons belong to the `core` collection, which is normally + * registered on `init`. A lazy load can happen before that, so the collection + * must be ensured at load time rather than assumed to exist. + * + * @ticket 00000 + * + * @covers ::load_default_icons + */ + public function test_default_icons_load_even_if_core_collection_is_missing() { + $collections = WP_Icon_Collections_Registry::get_instance(); + if ( $collections->is_registered( 'core' ) ) { + $collections->unregister( 'core' ); + } + $this->assertFalse( $collections->is_registered( 'core' ) ); + + $this->reset_registry(); + + $icons = $this->registry->get_registered_icons(); + + $this->assertNotEmpty( $icons, 'Default icons should register even when the collection was not set up yet.' ); + $this->assertTrue( $collections->is_registered( 'core' ), 'The core collection should be registered on demand.' ); + } }