Skip to content

Commit 4b5606b

Browse files
gziolofelixarntz
andauthored
Connectors: Add connectors registry for extensibility (#76364)
* Connectors: Add connectors registry for extensibility Ports the connectors registry from WordPress core (wordpress-develop#11175) to Gutenberg. Introduces WP_Connector_Registry class and wp_connectors_init action hook so plugins can register their own connectors alongside the built-in defaults (Anthropic, Google, OpenAI). Co-Authored-By: Claude Opus 4.6 <[email protected]> * Connectors: Polyfill wp_is_connector_registered, wp_get_connector, wp_get_connectors Uses function_exists guards so plugins can use the wp_* names now and seamlessly switch to core's implementation once WordPress 7.0 ships. Co-Authored-By: Claude Opus 4.6 <[email protected]> * Connectors: Move core polyfills to lib/compat/wordpress-7.0 Move WP_Connector_Registry class and wp_* helper functions to lib/compat/wordpress-7.0/ where other core polyfills live. This aligns with the existing PHPCS exclude pattern for the default text domain and keeps core-ported code in the standard location. Co-Authored-By: Claude Opus 4.6 <[email protected]> * Connectors: Add backport changelog entry for wordpress-develop#11175 Co-Authored-By: Claude Opus 4.6 <[email protected]> * Connectors: Add logo_url support to registry and resolve during init Extend WP_Connector_Registry to persist logo_url, removing the need for a separate enrichment step. Resolve logo URLs from AI Client metadata alongside name and description in _gutenberg_connectors_init. Co-Authored-By: Claude Opus 4.6 <[email protected]> * Connectors: Polyfill _wp_connectors_resolve_ai_provider_logo_url from core Move the logo URL resolver to lib/compat/wordpress-7.0/connectors.php as a polyfill for the _wp_connectors_resolve_ai_provider_logo_url function being added to WP core. Remove the Gutenberg-prefixed version and its unit tests, which will be covered by core. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]> Co-authored-by: gziolo <[email protected]> Co-authored-by: felixarntz <[email protected]>
1 parent d9ac946 commit 4b5606b

6 files changed

Lines changed: 579 additions & 220 deletions

File tree

backport-changelog/7.0/11175.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
https://github.com/WordPress/wordpress-develop/pull/11175
2+
3+
* https://github.com/WordPress/gutenberg/pull/76364
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
<?php
2+
/**
3+
* Connectors API
4+
*
5+
* Defines WP_Connector_Registry class.
6+
*
7+
* @package gutenberg
8+
* @since 7.0.0
9+
*/
10+
11+
if ( ! class_exists( 'WP_Connector_Registry' ) ) {
12+
/**
13+
* Manages the registration and lookup of connectors.
14+
*
15+
* @since 7.0.0
16+
* @access private
17+
*
18+
* @phpstan-type Connector array{
19+
* name: string,
20+
* description: string,
21+
* logo_url?: string|null,
22+
* type: string,
23+
* authentication: array{
24+
* method: string,
25+
* credentials_url?: string|null,
26+
* setting_name?: string
27+
* },
28+
* plugin?: array{
29+
* slug: string
30+
* }
31+
* }
32+
*/
33+
final class WP_Connector_Registry {
34+
/**
35+
* The singleton instance of the registry.
36+
*
37+
* @since 7.0.0
38+
*/
39+
private static ?WP_Connector_Registry $instance = null;
40+
41+
/**
42+
* Holds the registered connectors.
43+
*
44+
* Each connector is stored as an associative array with keys:
45+
* name, description, type, authentication, and optionally plugin.
46+
*
47+
* @since 7.0.0
48+
* @var array<string, array>
49+
* @phpstan-var array<string, Connector>
50+
*/
51+
private array $registered_connectors = array();
52+
53+
/**
54+
* Registers a new connector.
55+
*
56+
* @since 7.0.0
57+
*
58+
* @param string $id The unique connector identifier. Must contain only lowercase
59+
* alphanumeric characters and underscores.
60+
* @param array $args {
61+
* An associative array of arguments for the connector.
62+
*
63+
* @type string $name Required. The connector's display name.
64+
* @type string $description Optional. The connector's description. Default empty string.
65+
* @type string|null $logo_url Optional. URL to the connector's logo image. Default null.
66+
* @type string $type Required. The connector type. Currently, only 'ai_provider' is supported.
67+
* @type array $authentication {
68+
* Required. Authentication configuration.
69+
*
70+
* @type string $method Required. The authentication method: 'api_key' or 'none'.
71+
* @type string|null $credentials_url Optional. URL where users can obtain API credentials.
72+
* }
73+
* @type array $plugin {
74+
* Optional. Plugin data for install/activate UI.
75+
*
76+
* @type string $slug The WordPress.org plugin slug.
77+
* }
78+
* }
79+
* @return array|null The registered connector data on success, null on failure.
80+
*
81+
* @phpstan-param Connector $args
82+
* @phpstan-return Connector|null
83+
*/
84+
public function register( string $id, array $args ): ?array {
85+
if ( ! preg_match( '/^[a-z0-9_]+$/', $id ) ) {
86+
_doing_it_wrong(
87+
__METHOD__,
88+
__(
89+
'Connector ID must contain only lowercase alphanumeric characters and underscores.'
90+
),
91+
'7.0.0'
92+
);
93+
return null;
94+
}
95+
96+
if ( $this->is_registered( $id ) ) {
97+
_doing_it_wrong(
98+
__METHOD__,
99+
/* translators: %s: Connector ID. */
100+
sprintf( __( 'Connector "%s" is already registered.' ), esc_html( $id ) ),
101+
'7.0.0'
102+
);
103+
return null;
104+
}
105+
106+
// Validate required fields.
107+
if ( empty( $args['name'] ) || ! is_string( $args['name'] ) ) {
108+
_doing_it_wrong(
109+
__METHOD__,
110+
/* translators: %s: Connector ID. */
111+
sprintf( __( 'Connector "%s" requires a non-empty "name" string.' ), esc_html( $id ) ),
112+
'7.0.0'
113+
);
114+
return null;
115+
}
116+
117+
if ( empty( $args['type'] ) || ! is_string( $args['type'] ) ) {
118+
_doing_it_wrong(
119+
__METHOD__,
120+
/* translators: %s: Connector ID. */
121+
sprintf( __( 'Connector "%s" requires a non-empty "type" string.' ), esc_html( $id ) ),
122+
'7.0.0'
123+
);
124+
return null;
125+
}
126+
127+
if ( ! isset( $args['authentication'] ) || ! is_array( $args['authentication'] ) ) {
128+
_doing_it_wrong(
129+
__METHOD__,
130+
/* translators: %s: Connector ID. */
131+
sprintf( __( 'Connector "%s" requires an "authentication" array.' ), esc_html( $id ) ),
132+
'7.0.0'
133+
);
134+
return null;
135+
}
136+
137+
if ( empty( $args['authentication']['method'] ) || ! in_array( $args['authentication']['method'], array( 'api_key', 'none' ), true ) ) {
138+
_doing_it_wrong(
139+
__METHOD__,
140+
/* translators: %s: Connector ID. */
141+
sprintf( __( 'Connector "%s" authentication method must be "api_key" or "none".' ), esc_html( $id ) ),
142+
'7.0.0'
143+
);
144+
return null;
145+
}
146+
147+
$connector = array(
148+
'name' => $args['name'],
149+
'description' => isset( $args['description'] ) && is_string( $args['description'] ) ? $args['description'] : '',
150+
'type' => $args['type'],
151+
'authentication' => array(
152+
'method' => $args['authentication']['method'],
153+
),
154+
);
155+
156+
if ( ! empty( $args['logo_url'] ) && is_string( $args['logo_url'] ) ) {
157+
$connector['logo_url'] = $args['logo_url'];
158+
}
159+
160+
if ( 'api_key' === $args['authentication']['method'] ) {
161+
$connector['authentication']['credentials_url'] = $args['authentication']['credentials_url'] ?? null;
162+
$connector['authentication']['setting_name'] = "connectors_ai_{$id}_api_key";
163+
}
164+
165+
if ( ! empty( $args['plugin'] ) && is_array( $args['plugin'] ) ) {
166+
$connector['plugin'] = $args['plugin'];
167+
}
168+
169+
$this->registered_connectors[ $id ] = $connector;
170+
return $connector;
171+
}
172+
173+
/**
174+
* Unregisters a connector.
175+
*
176+
* @since 7.0.0
177+
*
178+
* @param string $id The connector identifier.
179+
* @return array|null The unregistered connector data on success, null on failure.
180+
*
181+
* @phpstan-return Connector|null
182+
*/
183+
public function unregister( string $id ): ?array {
184+
if ( ! $this->is_registered( $id ) ) {
185+
_doing_it_wrong(
186+
__METHOD__,
187+
/* translators: %s: Connector ID. */
188+
sprintf( __( 'Connector "%s" not found.' ), esc_html( $id ) ),
189+
'7.0.0'
190+
);
191+
return null;
192+
}
193+
194+
$unregistered = $this->registered_connectors[ $id ];
195+
unset( $this->registered_connectors[ $id ] );
196+
197+
return $unregistered;
198+
}
199+
200+
/**
201+
* Retrieves the list of all registered connectors.
202+
*
203+
* Do not use this method directly. Instead, use the `wp_get_connectors()` function.
204+
*
205+
* @since 7.0.0
206+
*
207+
* @see wp_get_connectors()
208+
*
209+
* @return array<string, array> The array of registered connectors keyed by connector ID.
210+
* @phpstan-return array<string, Connector>
211+
*/
212+
public function get_all_registered(): array {
213+
return $this->registered_connectors;
214+
}
215+
216+
/**
217+
* Checks if a connector is registered.
218+
*
219+
* Do not use this method directly. Instead, use the `wp_is_connector_registered()` function.
220+
*
221+
* @since 7.0.0
222+
*
223+
* @see wp_is_connector_registered()
224+
*
225+
* @param string $id The connector identifier.
226+
* @return bool True if the connector is registered, false otherwise.
227+
*/
228+
public function is_registered( string $id ): bool {
229+
return isset( $this->registered_connectors[ $id ] );
230+
}
231+
232+
/**
233+
* Retrieves a registered connector.
234+
*
235+
* Do not use this method directly. Instead, use the `wp_get_connector()` function.
236+
*
237+
* @since 7.0.0
238+
*
239+
* @see wp_get_connector()
240+
*
241+
* @param string $id The connector identifier.
242+
* @return array|null The registered connector data, or null if it is not registered.
243+
* @phpstan-return Connector|null
244+
*/
245+
public function get_registered( string $id ): ?array {
246+
if ( ! $this->is_registered( $id ) ) {
247+
_doing_it_wrong(
248+
__METHOD__,
249+
/* translators: %s: Connector ID. */
250+
sprintf( __( 'Connector "%s" not found.' ), esc_html( $id ) ),
251+
'7.0.0'
252+
);
253+
return null;
254+
}
255+
return $this->registered_connectors[ $id ];
256+
}
257+
258+
/**
259+
* Retrieves the main instance of the registry class.
260+
*
261+
* @since 7.0.0
262+
*
263+
* @return WP_Connector_Registry|null The main registry instance, or null if not yet initialized.
264+
*/
265+
public static function get_instance(): ?self {
266+
return self::$instance;
267+
}
268+
269+
/**
270+
* Sets the main instance of the registry class.
271+
*
272+
* @since 7.0.0
273+
* @access private
274+
*
275+
* @param WP_Connector_Registry $registry The registry instance.
276+
*/
277+
public static function set_instance( WP_Connector_Registry $registry ): void {
278+
if ( ! doing_action( 'init' ) ) {
279+
_doing_it_wrong(
280+
__METHOD__,
281+
__( 'The connector registry instance must be set during the <code>init</code> action.' ),
282+
'7.0.0'
283+
);
284+
return;
285+
}
286+
287+
self::$instance = $registry;
288+
}
289+
}
290+
}

0 commit comments

Comments
 (0)