Icons: Add APIs for collection and icon registration#77260
Icons: Add APIs for collection and icon registration#77260
Conversation
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>
Exposes `/wp/v2/icons/<namespace>` alongside the existing list and single-item routes, mirroring the hierarchical URL style used by block-types. The same `get_items` handler serves both the global list and the collection-scoped listing via the URL-captured `namespace` parameter, which is also formally declared in `get_collection_params`. The default icon collection slug is renamed from `wordpress` to `core` so that it matches the namespace prefix (`core/`) used by bundled icons, removing the confusing split between namespace and collection identifiers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Callers of `wp_register_icon` now pass an unqualified icon name (e.g. `arrow-left`) together with a `collection` slug, instead of encoding both into a single namespaced string (`core/arrow-left`). The registry continues to key storage by `<collection>/<name>` internally so that the existing single-item REST route, cross-collection name-collision protection, and `is_registered`/`get_registered_icon` lookups keep working without broader changes. The REST response is reshaped to match: icons now expose separate `collection` and `name` fields instead of a single namespaced `name`, which lines up with the hierarchical `/icons/<collection>` route added earlier and avoids clients having to parse the slash-delimited form. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous commit reshaped the icon response to return an unqualified `name` plus a separate `collection` field, but that diverges from the namespaced identifier clients already use to address single items via `/icons/<collection>/<name>`. Restore the namespaced `name` so the response value can be used as-is for lookups, and keep `collection` as an additional field for convenience. Reimplement the override as a thin wrapper around the parent method to avoid duplicating the base field-filtering logic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rework the existing tests to match the current registration contract: callers pass an unqualified icon name together with a `collection` slug, and the registry stores items under a `<collection>/<name>` key. Set up a test collection in `set_up`/`tear_down` so the registration path has a valid collection to target, and refresh the invalid-name fixtures to reflect that slashes are now rejected at input rather than required. Add coverage for the collection requirement itself (missing / non-string / unregistered collection) and for cross-collection name reuse, since the split between name and collection is the core behavior that differs from the previous namespaced-name design. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…icons Makes the bootstrap function's intent explicit: it specifically seeds the default `core` collection from the bundled manifest, mirroring the naming of `gutenberg_register_icon_collections` which seeds the default collection itself. The prior generic name was easy to mistake for a public registration helper. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Matches the earlier rename of the icon bootstrap function to `gutenberg_register_default_icons`, so both helpers that seed the bundled defaults share a `_default_` marker and read as clearly internal rather than part of the public registration API. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The inserter needs every registered icon to populate its picker, but the default `getEntityRecords` request paginates to the first page only, which silently truncates the list once more than a page's worth of icons are registered (e.g. once plugins contribute their own collections). Passing `per_page: -1` forces the resolver's chunked fetch path so the picker always reflects the full registry. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| : null, | ||
| allIcons: isInserterOpen | ||
| ? getEntityRecords( 'root', 'icon' ) | ||
| ? getEntityRecords( 'root', 'icon', { per_page: -1 } ) |
There was a problem hiding this comment.
This is a temporary fix and should be removed before merging. See #76406 (comment)
Matches the WordPress coding standard's variable-alignment rule, so phpcbf no longer rewrites these lines on contributors' pre-commit runs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.
lib/compat/wordpress-7.1/class-wp-icon-collections-registry.php
Outdated
Show resolved
Hide resolved
phpunit/experimental/class-wp-icons-registry-gutenberg-test.php
Outdated
Show resolved
Hide resolved
|
Size Change: +10 B (0%) Total Size: 7.74 MB 📦 View Changed
ℹ️ View Unchanged
|
Unregistering a collection previously left its icons in the icons registry, which produced orphaned entries still reachable through `/wp/v2/icons` and `/wp/v2/icons/<collection>/<name>` even though the collection-scoped route returned 404. Cascade the removal so the two registries stay consistent, and add a regression test covering both the cascade and the fact that icons in unrelated collections are left alone. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Lifts the collection slug out of the `$args` array and the qualified `<collection>/<name>` string into its own required parameter on both `wp_register_icon`/`wp_unregister_icon` and the underlying registry methods. The symmetric `( $icon_name, $collection, ... )` shape makes the dependency between an icon and its collection explicit at the call site, avoids callers having to manually concatenate the qualified name just to remove an icon, and keeps the public API in line with the internal storage convention where the two values are always tracked separately. The collection-cascade in `WP_Icon_Collections_Registry::unregister` is updated accordingly to pass the unqualified name and slug to the new signature. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`_x()` takes a context string as its second argument, but the call was passing the text domain, so the string was being registered with an unintended context and never picking up the translation. Switch to `__()` so the label is translatable via the `gutenberg` text domain the same way as the surrounding strings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The existing message read "valid a \"filePath\"" due to a transposed article. Correct it to "a valid \"filePath\"" so the error is readable and translatable in a natural form. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The `_doing_it_wrong` call in `WP_Icon_Collections_Registry::unregister` referenced a future `21.4.0` marker, but the rest of this class (and the surrounding icons API it ships with) consistently reports `7.1.0` as the introduction version. Align the version argument so all `_doing_it_wrong` notices from this file point at the same release. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Without this check, passing anything other than an array as the second argument (e.g. null, a string, or a forgotten argument from a partial refactor) reached `array_keys` / `array_fill_keys` and produced a PHP type error or warning instead of a clean `_doing_it_wrong` notice. Fail early with the same pattern used for the other validation branches so misuse is surfaced as a developer warning and the registration simply returns false. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The URL-segment route already restricts the capture to the collection slug pattern, but the same parameter as a query string had only a `string` type check. Adding the matching `pattern` lets `rest_validate_request_arg` reject malformed input with a 400 before the handler runs, instead of funnelling everything through the 404-on-unregistered-collection path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`test_register_invalid_name` was annotated with `@dataProvider` but ignored the argument and looped over the provider manually, so every run received an array (e.g. `[ 'Plus' ]`) as the name and only the non-string branch of the validator was actually hit. Accept `$name` from the provider and drop the manual loop so each case — slash, uppercase, leading underscore, non-string — is covered as a separate assertion in the failure output. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| * 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.
The cascade fetched the icons registry via the base class singleton, which stays in sync with the Gutenberg subclass only after `gutenberg_override_wp_icons_registry()` has run. Anything that resets the subclass singleton on its own (notably the PHPUnit `tear_down` between tests) leaves the two pointers diverged, and the cascade then iterates a stale instance and silently no-ops -- which is exactly what made `test_unregister_collection_cascades_to_icons` fail intermittently. The collections registry is shipped only with Gutenberg and only Gutenberg-registered icons carry a `collection` field for the cascade to match on, so resolving to `WP_Icons_Registry_Gutenberg` directly is both accurate and removes the singleton-sync dependency. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cover unregister() success and unknown-icon paths in the icons registry test, and move the collection-cascade test out to the collections suite where it belongs. 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>
|
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 db6fb51. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/24339216463
|
Part of #75715
Note: 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_Registry: New. Singleton that stores icon collections. It supports basic methods such as registering, unregistering, and retrieving items from a collection.WP_Icons_Registry_Gutenberg:gutenberg_register_default_iconsinstead.WP_REST_Icons_Controller_Gutenberg: Add 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, $collection, $args )wp_unregister_icon( $icon_name, $collection )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.