Icons: Add APIs for collection and icon registration#77260
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a collection-based registration layer to the Icons API, enabling plugins/themes to register their own SVG icon collections and icons, and extends the REST API to query icons by collection.
Changes:
- Introduces an icon collections registry and public wrapper functions for registering/unregistering icon collections and icons.
- Refactors
WP_Icons_Registry_Gutenbergto require acollectionfor icon registration and to qualify stored icon names as{collection}/{icon}. - Extends the icons REST controller with a collection-scoped listing route and updates the Icon block to request all icons when opening the inserter.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| phpunit/experimental/class-wp-icons-registry-gutenberg-test.php | Updates/extends registry tests for collection-aware registration behavior. |
| packages/block-library/src/icon/edit.js | Changes icon list fetching parameters when inserter is open. |
| lib/load.php | Loads new 7.1 compat files for collections + icons API wrappers. |
| lib/compat/wordpress-7.1/icons.php | Adds public wrapper functions and default collection/icon registration hooks. |
| lib/compat/wordpress-7.1/class-wp-icon-collections-registry.php | New singleton registry for icon collections with basic CRUD. |
| lib/class-wp-rest-icons-controller-gutenberg.php | Adds collection-scoped icons route and includes collection in REST schema/response. |
| lib/class-wp-icons-registry-gutenberg.php | Refactors registration to be collection-based and adds unregister(). |
Comments suppressed due to low confidence (1)
phpunit/experimental/class-wp-icons-registry-gutenberg-test.php:57
- The helper comment says it invokes
register"despite it being private", butWP_Icons_Registry_Gutenberg::register()is now public. Either update the comment (and consider calling the method directly instead of using reflection) to keep the test intent clear.
/**
* Invokes WP_Icons_Registry_Gutenberg::register despite it being private
*
* @param string $icon_name Icon name (without namespace prefix).
* @param array $icon_properties Icon properties (label, content, filePath, collection).
* @return bool True if the icon was registered successfully.
*/
private function register( $icon_name, $icon_properties ) {
$method = new ReflectionMethod( $this->registry, 'register' );
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * Registers a new icon. | ||
| * | ||
| * @param string $icon_name Icon name including namespace. | ||
| * @param array $args { | ||
| * List of properties for the icon. | ||
| * | ||
| * @type string $label Required. A human-readable label for the icon. | ||
| * @type string $collection Required. The slug of a registered icon collection that this icon belongs to. | ||
| * @type string $content Optional. SVG markup for the icon. | ||
| * If not provided, the content will be retrieved from the `filePath` if set. | ||
| * If both `content` and `filePath` are not set, the icon will not be registered. | ||
| * @type string $filePath Optional. The full path to the file containing the icon content. | ||
| * } | ||
| * @return bool True if the icon was registered successfully, else false. | ||
| */ | ||
| function wp_register_icon( $icon_name, $args ) { | ||
| return WP_Icons_Registry::get_instance()->register( $icon_name, $args ); | ||
| } |
There was a problem hiding this comment.
The wp_register_icon() docblock says $icon_name includes a namespace, but the exposed API and the registry implementation now expect an unqualified icon slug (with the collection provided in $args['collection']). Update the param docs to match the actual accepted format to avoid consumers passing collection/icon and getting _doing_it_wrong failures.
There was a problem hiding this comment.
Why not keep registration the same and just require collection from $args? The wp_unregister_icon can remain the same as it is now.
There was a problem hiding this comment.
Fixed in 69a5551
Furthermore, based on #77260 (comment), I have made the parameter optional.
There was a problem hiding this comment.
Furthermore, based on #77260 (comment), I have made the parameter optional.
So it would default to core if collection is omitted?
There was a problem hiding this comment.
Yes. I don't have a strong opinion on whether the collection should be a required parameter. What do you think?
There was a problem hiding this comment.
As long as intent and results are documented, I also don't have a strong opinion here.
What happens if I re-register the star icon with the default collection? Will the core icon be replaced, or do I get a "doing it wrong" warning?
There was a problem hiding this comment.
We get a "doing it wrong" warning.
register_block_type outputs "doing it wrong", but register_block_pattern has its pattern replaced. I'm a little unsure about which pattern to follow for the icon registration.
|
Size Change: +9 B (0%) Total Size: 7.82 MB 📦 View Changed
ℹ️ View Unchanged
|
| * Arguments for registering an icon collection. | ||
| * | ||
| * @type string $label Required. A human-readable label for the icon collection. | ||
| * @type string $description Optional. A human-readable description for the icon collection. |
There was a problem hiding this comment.
I haven't decided yet whether to visually display the collection description, but it probably won't cause any problems if it's included.
Expose public APIs for registering third-party SVG icons by grouping them
into collections. Every icon is associated with a single collection
(defaulting to `core`), and icons are uniquely identified by
`{collection-slug}/{icon-slug}`. Unregistering a collection cascades to
all icons within it, and the same icon slug may coexist across different
collections.
New `WP_Icon_Collections_Registry` singleton stores collections.
`WP_Icons_Registry::register()` becomes public, requires a `collection`
property, and gains a matching `unregister()` method. Wrapper functions
`wp_register_icon_collection()`, `wp_unregister_icon_collection()`,
`wp_register_icon()`, and `wp_unregister_icon()` are introduced, and the
default `core` collection plus bundled icons are registered on `init`.
The REST controller gains a `/wp/v2/icons/<namespace>` route for
collection-scoped listings and exposes a `collection` field in responses.
Ports the equivalent functionality from the Gutenberg plugin
(WordPress/gutenberg#77260) to Core, along with covering unit tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Flaky tests detected in d8c7615. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/27469427779
|
Introduce a singleton registry class that lets plugins register icon collections with a label, description, and categories. This provides the foundation for a `wp_register_icon_collection()` wrapper and for grouping icons in the editor UI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Expose wp_register_icon_collection() / wp_unregister_icon_collection() as the public API for plugins, and register a default 'wordpress' collection on init so the registry is populated out of the box. Wire the new files into lib/load.php so they run under the WP 7.1 compat layer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ensures every icon belongs to a registered collection so the collections registry can be relied on as the source of truth. Default icon collection registration runs at init priority 0 so collections exist before the Gutenberg registry override replays registered icons. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…hooks Moves core icon registration out of the registry constructor into a `gutenberg_register_icons` action and registers default collections via `gutenberg_register_icon_collections`. Both hooks remove the matching core actions (`_wp_register_default_icons` / `_wp_register_default_icon_collections`) when present, so the Gutenberg plugin owns registration end-to-end and stays in sync with future core registration hooks without double-registering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Exposes a public registration API on top of the icons registry by widening `register` visibility and adding a matching `unregister` method on `WP_Icons_Registry_Gutenberg`. This lets plugins register icons without reaching into reflection and lets the Gutenberg registration paths call the public API directly. `gutenberg_register_icons` runs at the default priority so the registry override at priority 1 has already replaced the core singleton, allowing the wrapper to resolve the Gutenberg instance through `WP_Icons_Registry::get_instance()`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removes the 'categories' property from the collection registration API and its validation. Category support adds a second axis of grouping on top of collections and is best introduced as a follow-up once the base collection/icon registration API has settled, rather than landing both at once. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Allow clients to request icons limited to a specific registered collection via /wp/v2/icons?collection=<slug>. Without this, fetching icons for a given collection would require downloading all registered icons and filtering on the client, which scales poorly once large third-party collections are registered. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mirror the coverage in core's tests/phpunit/tests/icons/wpIconCollectionsRegistry.php so the Gutenberg copy of the collections registry is exercised the same way, including the cascade-to-icons behavior via the Gutenberg icons registry. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
db6fb51 to
1c71bca
Compare
The icons REST controller does not paginate, so passing { per_page: -1 } has no effect and only adds a redundant query parameter.
Replace the array_fill_keys/array_key_exists pair with a plain list checked via in_array, since only two keys are permitted and the indirection adds no value at this scale. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Miguel Fonseca <150562+mcsf@users.noreply.github.com>
Reverts the public registration wrapper to the original `wp_register_icon( $icon_name, $args )` shape and lets `collection` travel as a required key inside `$args`, alongside `label`, `content`, and `filePath`. The unqualified-name benefit of the previous change only applied to `wp_unregister_icon`, where callers no longer have to concatenate `<collection>/<name>` themselves; on the registration side the qualified-name issue never existed, so splitting `collection` out as a positional parameter just for symmetry was unnecessary churn for callers. `wp_unregister_icon( $icon_name, $collection )` and the underlying registry methods are left as is. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Treat the `collection` property of `WP_Icons_Registry::register()` as optional and fall back to the built-in `core` collection when callers do not specify one. The validation that previously rejected a missing collection now only fires when a non-string value is passed; an unregistered collection slug still triggers `_doing_it_wrong`. Plugin authors registering a small number of icons no longer need to know about the collection concept at all — they can register icons directly into `core` by omitting the field. Authors that want their own namespace can still pass `collection` explicitly after registering a collection through `wp_register_icon_collection()`. The internal docblock and the `wp_register_icon` wrapper docblock are updated to mark `collection` as optional with the new default. The `test_register_requires_collection` test is replaced with `test_register_defaults_collection_to_core`, which asserts that an omitted collection lands the icon under `core/<name>`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the `true` strict-mode argument to the allowed-keys check so that property names are compared by both type and value, satisfying WPCS WordPress.PHP.StrictInArray and matching the rest of the file's strict-typed validation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
I would like to proceed with this PR cautiously as it introduces many public APIs. Just in case, let me ping more contributors. @WordPress/gutenberg-core @ryanwelcher |
Aligns the wording with the slug ("core") so the description reflects
the collection identity rather than its default-registration status.
Align the icon registration API property with WordPress snake_case conventions following the base branch change. The manifest JSON key (`$icon_data['filePath']`) stays camelCase since it mirrors the JS manifest format. Co-Authored-By: Claude <noreply@anthropic.com>
…-regstration # Conflicts: # lib/class-wp-icons-registry-gutenberg.php # phpunit/experimental/class-wp-icons-registry-gutenberg-test.php
Replace the separate icon name and collection arguments with a single namespaced name in the form "collection/icon-name" (e.g. "core/arrow-left"), mirroring how block types are named. The collection is now derived from the name string instead of a `collection` property, and `wp_unregister_icon` takes the same namespaced name so the register/unregister pair stays symmetric. Names without a prefix still default to the "core" collection. Co-Authored-By: Claude <noreply@anthropic.com>
|
Update: In this PR, I changed the base branch from Another change is that I modified the arguments for // Before
wp_register_icon( 'star', 'my-icons', array(
'label' => 'Star',
'content' => '<svg/></svg>',
) );
wp_unregister_icon( 'star', 'my-icons' );
// After
wp_register_icon( 'my-icons/star', array(
'label' => 'Star',
'content' => '<svg/></svg>',
) );
wp_unregister_icon( 'my-icons/star' );The reason is to align with the icon rendering functions and formats being considered in #78332. echo wp_get_icon( 'core/plus', array(
'size' => 18,
'class' => 'my-button-icon',
'label' => 'Add item',
) ); |
| } | ||
|
|
||
| if ( has_action( 'init', '_wp_register_default_icon_collections' ) ) { | ||
| remove_action( 'init', '_wp_register_default_icon_collections' ); |
There was a problem hiding this comment.
We're removing the action with default priority 10, but what if it's add_action() call was done with a different priority? Luckuly, has_action() should return the priority, so we can just use that as the $priority when calling remove_action()
| ); | ||
| } | ||
|
|
||
| if ( has_action( 'init', '_wp_register_default_icon_collections' ) ) { |
There was a problem hiding this comment.
If the priority was 0, this will return false and never do what is intended. We might want to do !== false here instead.
| if ( has_action( 'init', '_wp_register_default_icons' ) ) { | ||
| remove_action( 'init', '_wp_register_default_icons' ); | ||
| } |
There was a problem hiding this comment.
See my comments above regarding has_action and remove_action - they apply here as well.
| return false; | ||
| } | ||
|
|
||
| if ( ! preg_match( '/^[a-z][a-z-]*$/', $collection_slug ) ) { |
There was a problem hiding this comment.
Are we really disallowing digits? What if folks want to have variations of the icons, or if they have numbers in their name (for example, if we have a collection called "i18n")?
There was a problem hiding this comment.
That's right. I adopted similar rules to the Block Type Registry's namespace and relaxed the regular expression slightly. See d8c7615
| @@ -15,4 +15,130 @@ | |||
| * @since 7.1.0 | |||
| */ | |||
| class WP_REST_Icons_Controller_Gutenberg extends WP_REST_Icons_Controller { | |||
There was a problem hiding this comment.
The endpoint could use some tests
There was a problem hiding this comment.
| * } | ||
| * @return bool True if the icon was registered successfully, else false. | ||
| */ | ||
| function wp_register_icon( $icon_name, $args ) { |
There was a problem hiding this comment.
Are we good to allow 3rd party plugins to register core/ icons? We don't seem to have an explicit check for that, and currently core/ icons are allowed to be registered from both core and plugins.
There was a problem hiding this comment.
While I don't have a strong opinion, some consumers might want to add just a few icons rather than a large set. In such cases, I believe it would be more convenient to include them as part of the core icons rather than creating a new collection.
This might be similar to how developers can use the core/ namespace to execute register_block_type.
Custom icons can be registered with a `file_path`, but the path was never checked before `file_get_contents()`, so a missing, unreadable, non-SVG, or non-string path emitted a raw PHP warning and a misleading "invalid SVG markup" error. Guard `get_content()` with the same validation core uses in `WP_Block_Patterns_Registry`: resolve via `realpath()`, then require an `.svg` extension, a regular file, and readability, returning null with a clear message otherwise. Override `get_content()` in `WP_Icons_Registry_Gutenberg` as well so the validation applies when the base `WP_Icons_Registry` is provided by core instead of the compat shim. Add tests covering valid and invalid file paths. Co-Authored-By: Claude <noreply@anthropic.com>
| if ( | ||
| ! is_string( $icon_path ) || | ||
| ! str_ends_with( $icon_path, '.svg' ) || | ||
| ! is_file( $icon_path ) || | ||
| ! is_readable( $icon_path ) | ||
| ) { | ||
| wp_trigger_error( | ||
| __METHOD__, | ||
| __( 'Icon file is missing or unreadable.', 'gutenberg' ) | ||
| ); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Based on #79100 (comment), I have made the file path check more robust. This implementation is the same as that for the block pattern registry.
has_action() returns the registered priority (or false), which is 0 for the priority-0 hooks. The previous truthy check skipped removal at priority 0, and remove_action() assumed the default priority 10. Capture the returned priority, compare with !== false, and pass it to remove_action() so the core actions are removed regardless of priority. Co-Authored-By: Claude <noreply@anthropic.com>
Mirror register_block_type's name validation by accepting lowercase alphanumeric characters and hyphens. The previous rule rejected digits, which blocked legitimate slugs such as "i18n" or icon variations that include numbers. Update the slug tests accordingly: digit-containing slugs are now valid, and underscores remain rejected. Co-Authored-By: Claude <noreply@anthropic.com>
file_pathkey in icon registry #79100Note: I understand that this PR is large. However, I believe this is the minimum implementation required to correctly expose the API for registering icons.
What?
This PR exposes basic APIs for registering SVG icons.
The approach proposed by this PR is as follows. Please share your thoughts on this approach:
core(WordPress).{collection-slug}/{icon-slug}, as before.In the future, we might also support "categories," similar to font collections. That is, something like this:
Font Awesome > Symbol > Arrow LeftClasses
I added and extended classes to allow custom icon registration.
WP_Icon_Collections_RegistryNew. Singleton that stores icon collections. It supports basic methods such as registering, unregistering, and retrieving items from a collection.
WP_Icons_Registry_Gutenberggutenberg_register_default_iconsinstead.WP_REST_Icons_Controller_GutenbergAdd an endpoint to retrieve only the icons belonging to a specific collection.
/wp/v2/icons: Unchanged. Lists all registered icons./wp/v2/icons/<namespace>: New. Lists icons belonging to the given collection slug/wp/v2/icons/<namespace>/<name>: Unchanged. Single-item lookup.PHP functions
These are new wrapper functions for managing collections and icons.
wp_register_icon_collection( $slug, $args )wp_unregister_icon_collection( $slug )wp_register_icon( $icon_name, $args )wp_unregister_icon( $icon_name )Testing Instructions
Verify that the main APIs are functioning correctly. Below are code examples.
Register icons:
Confirm registered icons:
Unregister icons and collections
Screenshot
As you can see, the icon picker modal does not currently support filtering by collection. This will be addressed in a follow-up update.
Use of AI Tools
Parts of this PR (code refactoring, commit messages, PR description) were drafted with assistance from Claude Code. All generated changes were reviewed and adjusted manually before committing.