Skip to content

Commit b8e0c3d

Browse files
committed
Connectors: Allow hyphens in connector IDs
Expands the connector ID validation regex from `/^[a-z0-9_]+$/` to `/^[a-z0-9_-]+$/`, aligning with the PHP AI Client library naming conventions. Hyphens are normalized to underscores when generating `setting_name` (e.g., `azure-openai` → `connectors_ai_azure_openai_api_key`). Developed in #11285. Props pers, gziolo, jorgefilipecosta, westonruter, flixos90, mukesh27. Fixes #64861. git-svn-id: https://develop.svn.wordpress.org/trunk@62056 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 5e8ec8f commit b8e0c3d

File tree

5 files changed

+60
-42
lines changed

5 files changed

+60
-42
lines changed

src/wp-includes/class-wp-connector-registry.php

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,11 @@ final class WP_Connector_Registry {
6767
*
6868
* Validates the provided arguments and stores the connector in the registry.
6969
* For connectors with `api_key` authentication, a `setting_name` is automatically
70-
* generated using the pattern `connectors_ai_{$id}_api_key` (e.g., connector ID
71-
* `openai` produces `connectors_ai_openai_api_key`). This setting name is used
72-
* for the Settings API registration and REST API exposure.
70+
* generated using the pattern `connectors_ai_{$id}_api_key`, with hyphens in the ID
71+
* normalized to underscores (e.g., connector ID `openai` produces
72+
* `connectors_ai_openai_api_key`, and `azure-openai` produces
73+
* `connectors_ai_azure_openai_api_key`). This setting name is used for the Settings
74+
* API registration and REST API exposure.
7375
*
7476
* Registering a connector with an ID that is already registered will trigger a
7577
* `_doing_it_wrong()` notice and return `null`. To override an existing connector,
@@ -80,7 +82,7 @@ final class WP_Connector_Registry {
8082
* @see WP_Connector_Registry::unregister()
8183
*
8284
* @param string $id The unique connector identifier. Must match the pattern
83-
* `/^[a-z0-9_]+$/` (lowercase alphanumeric and underscores only).
85+
* `/^[a-z0-9_-]+$/` (lowercase alphanumeric, hyphens, and underscores only).
8486
* @param array $args {
8587
* An associative array of arguments for the connector.
8688
*
@@ -106,11 +108,11 @@ final class WP_Connector_Registry {
106108
* @phpstan-return Connector|null
107109
*/
108110
public function register( string $id, array $args ): ?array {
109-
if ( ! preg_match( '/^[a-z0-9_]+$/', $id ) ) {
111+
if ( ! preg_match( '/^[a-z0-9_-]+$/', $id ) ) {
110112
_doing_it_wrong(
111113
__METHOD__,
112114
__(
113-
'Connector ID must contain only lowercase alphanumeric characters and underscores.'
115+
'Connector ID must contain only lowercase alphanumeric characters, hyphens, and underscores.'
114116
),
115117
'7.0.0'
116118
);
@@ -185,7 +187,7 @@ public function register( string $id, array $args ): ?array {
185187
if ( ! empty( $args['authentication']['credentials_url'] ) && is_string( $args['authentication']['credentials_url'] ) ) {
186188
$connector['authentication']['credentials_url'] = $args['authentication']['credentials_url'];
187189
}
188-
$connector['authentication']['setting_name'] = "connectors_ai_{$id}_api_key";
190+
$connector['authentication']['setting_name'] = 'connectors_ai_' . str_replace( '-', '_', $id ) . '_api_key';
189191
}
190192

191193
if ( ! empty( $args['plugin'] ) && is_array( $args['plugin'] ) ) {

tests/phpunit/includes/wp-ai-client-mock-provider-trait.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ class Mock_Connectors_Test_Provider extends AbstractProvider {
9696
*/
9797
protected static function createProviderMetadata(): ProviderMetadata {
9898
return new ProviderMetadata(
99-
'mock_connectors_test',
99+
'mock-connectors-test',
100100
'Mock Connectors Test',
101101
ProviderTypeEnum::cloud(),
102102
null,
@@ -156,15 +156,15 @@ trait WP_AI_Client_Mock_Provider_Trait {
156156
*/
157157
private static function register_mock_connectors_provider(): void {
158158
$ai_registry = AiClient::defaultRegistry();
159-
if ( ! $ai_registry->hasProvider( 'mock_connectors_test' ) ) {
159+
if ( ! $ai_registry->hasProvider( 'mock-connectors-test' ) ) {
160160
$ai_registry->registerProvider( Mock_Connectors_Test_Provider::class );
161161
}
162162

163163
// Also register in the WP connector registry if not already present.
164164
$connector_registry = WP_Connector_Registry::get_instance();
165-
if ( null !== $connector_registry && ! $connector_registry->is_registered( 'mock_connectors_test' ) ) {
165+
if ( null !== $connector_registry && ! $connector_registry->is_registered( 'mock-connectors-test' ) ) {
166166
$connector_registry->register(
167-
'mock_connectors_test',
167+
'mock-connectors-test',
168168
array(
169169
'name' => 'Mock Connectors Test',
170170
'description' => '',

tests/phpunit/tests/connectors/wpConnectorRegistry.php

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public function set_up(): void {
4646
* @ticket 64791
4747
*/
4848
public function test_register_returns_connector_data() {
49-
$result = $this->registry->register( 'test_provider', self::$default_args );
49+
$result = $this->registry->register( 'test-provider', self::$default_args );
5050

5151
$this->assertIsArray( $result );
5252
$this->assertSame( 'Test Provider', $result['name'] );
@@ -61,7 +61,16 @@ public function test_register_returns_connector_data() {
6161
* @ticket 64791
6262
*/
6363
public function test_register_generates_setting_name_for_api_key() {
64-
$result = $this->registry->register( 'my_ai', self::$default_args );
64+
$result = $this->registry->register( 'myai', self::$default_args );
65+
66+
$this->assertSame( 'connectors_ai_myai_api_key', $result['authentication']['setting_name'] );
67+
}
68+
69+
/**
70+
* @ticket 64861
71+
*/
72+
public function test_register_generates_setting_name_normalizes_hyphens() {
73+
$result = $this->registry->register( 'my-ai', self::$default_args );
6574

6675
$this->assertSame( 'connectors_ai_my_ai_api_key', $result['authentication']['setting_name'] );
6776
}
@@ -75,7 +84,7 @@ public function test_register_no_setting_name_for_none_auth() {
7584
'type' => 'ai_provider',
7685
'authentication' => array( 'method' => 'none' ),
7786
);
78-
$result = $this->registry->register( 'no_auth', $args );
87+
$result = $this->registry->register( 'no-auth', $args );
7988

8089
$this->assertIsArray( $result );
8190
$this->assertArrayNotHasKey( 'setting_name', $result['authentication'] );
@@ -91,7 +100,7 @@ public function test_register_defaults_description_to_empty_string() {
91100
'authentication' => array( 'method' => 'none' ),
92101
);
93102

94-
$result = $this->registry->register( 'minimal', $args );
103+
$result = $this->registry->register( 'minimal-provider', $args );
95104

96105
$this->assertSame( '', $result['description'] );
97106
}
@@ -103,7 +112,7 @@ public function test_register_includes_logo_url() {
103112
$args = self::$default_args;
104113
$args['logo_url'] = 'https://example.com/logo.png';
105114

106-
$result = $this->registry->register( 'with_logo', $args );
115+
$result = $this->registry->register( 'with-logo', $args );
107116

108117
$this->assertArrayHasKey( 'logo_url', $result );
109118
$this->assertSame( 'https://example.com/logo.png', $result['logo_url'] );
@@ -113,7 +122,7 @@ public function test_register_includes_logo_url() {
113122
* @ticket 64791
114123
*/
115124
public function test_register_omits_logo_url_when_not_provided() {
116-
$result = $this->registry->register( 'no_logo', self::$default_args );
125+
$result = $this->registry->register( 'no-logo', self::$default_args );
117126

118127
$this->assertArrayNotHasKey( 'logo_url', $result );
119128
}
@@ -125,7 +134,7 @@ public function test_register_omits_logo_url_when_empty() {
125134
$args = self::$default_args;
126135
$args['logo_url'] = '';
127136

128-
$result = $this->registry->register( 'empty_logo', $args );
137+
$result = $this->registry->register( 'empty-logo', $args );
129138

130139
$this->assertArrayNotHasKey( 'logo_url', $result );
131140
}
@@ -137,7 +146,7 @@ public function test_register_includes_plugin_data() {
137146
$args = self::$default_args;
138147
$args['plugin'] = array( 'slug' => 'my-plugin' );
139148

140-
$result = $this->registry->register( 'with_plugin', $args );
149+
$result = $this->registry->register( 'with-plugin', $args );
141150

142151
$this->assertArrayHasKey( 'plugin', $result );
143152
$this->assertSame( array( 'slug' => 'my-plugin' ), $result['plugin'] );
@@ -147,7 +156,7 @@ public function test_register_includes_plugin_data() {
147156
* @ticket 64791
148157
*/
149158
public function test_register_omits_plugin_when_not_provided() {
150-
$result = $this->registry->register( 'no_plugin', self::$default_args );
159+
$result = $this->registry->register( 'no-plugin', self::$default_args );
151160

152161
$this->assertArrayNotHasKey( 'plugin', $result );
153162
}
@@ -164,14 +173,21 @@ public function test_register_rejects_invalid_id_with_uppercase() {
164173
}
165174

166175
/**
167-
* @ticket 64791
176+
* @ticket 64861
168177
*/
169-
public function test_register_rejects_invalid_id_with_dashes() {
170-
$this->setExpectedIncorrectUsage( 'WP_Connector_Registry::register' );
171-
178+
public function test_register_accepts_id_with_hyphens() {
172179
$result = $this->registry->register( 'my-provider', self::$default_args );
173180

174-
$this->assertNull( $result );
181+
$this->assertIsArray( $result );
182+
}
183+
184+
/**
185+
* @ticket 64861
186+
*/
187+
public function test_register_accepts_id_with_underscores() {
188+
$result = $this->registry->register( 'my_provider', self::$default_args );
189+
190+
$this->assertIsArray( $result );
175191
}
176192

177193
/**
@@ -191,8 +207,8 @@ public function test_register_rejects_empty_id() {
191207
public function test_register_rejects_duplicate_id() {
192208
$this->setExpectedIncorrectUsage( 'WP_Connector_Registry::register' );
193209

194-
$this->registry->register( 'duplicate', self::$default_args );
195-
$result = $this->registry->register( 'duplicate', self::$default_args );
210+
$this->registry->register( 'test-duplicate', self::$default_args );
211+
$result = $this->registry->register( 'test-duplicate', self::$default_args );
196212

197213
$this->assertNull( $result );
198214
}
@@ -206,7 +222,7 @@ public function test_register_rejects_missing_name() {
206222
$args = self::$default_args;
207223
unset( $args['name'] );
208224

209-
$result = $this->registry->register( 'no_name', $args );
225+
$result = $this->registry->register( 'no-name', $args );
210226

211227
$this->assertNull( $result );
212228
}
@@ -220,7 +236,7 @@ public function test_register_rejects_empty_name() {
220236
$args = self::$default_args;
221237
$args['name'] = '';
222238

223-
$result = $this->registry->register( 'empty_name', $args );
239+
$result = $this->registry->register( 'empty-name', $args );
224240

225241
$this->assertNull( $result );
226242
}
@@ -234,7 +250,7 @@ public function test_register_rejects_missing_type() {
234250
$args = self::$default_args;
235251
unset( $args['type'] );
236252

237-
$result = $this->registry->register( 'no_type', $args );
253+
$result = $this->registry->register( 'no-type', $args );
238254

239255
$this->assertNull( $result );
240256
}
@@ -248,7 +264,7 @@ public function test_register_rejects_missing_authentication() {
248264
$args = self::$default_args;
249265
unset( $args['authentication'] );
250266

251-
$result = $this->registry->register( 'no_auth', $args );
267+
$result = $this->registry->register( 'no-auth', $args );
252268

253269
$this->assertNull( $result );
254270
}
@@ -262,7 +278,7 @@ public function test_register_rejects_invalid_auth_method() {
262278
$args = self::$default_args;
263279
$args['authentication']['method'] = 'oauth';
264280

265-
$result = $this->registry->register( 'bad_auth', $args );
281+
$result = $this->registry->register( 'bad-auth', $args );
266282

267283
$this->assertNull( $result );
268284
}
@@ -287,9 +303,9 @@ public function test_is_registered_returns_false_for_unregistered() {
287303
* @ticket 64791
288304
*/
289305
public function test_get_registered_returns_connector_data() {
290-
$this->registry->register( 'my_connector', self::$default_args );
306+
$this->registry->register( 'my-connector', self::$default_args );
291307

292-
$result = $this->registry->get_registered( 'my_connector' );
308+
$result = $this->registry->get_registered( 'my-connector' );
293309

294310
$this->assertIsArray( $result );
295311
$this->assertSame( 'Test Provider', $result['name'] );
@@ -334,13 +350,13 @@ public function test_get_all_registered_returns_empty_when_none() {
334350
* @ticket 64791
335351
*/
336352
public function test_unregister_removes_connector() {
337-
$this->registry->register( 'to_remove', self::$default_args );
353+
$this->registry->register( 'to-remove', self::$default_args );
338354

339-
$result = $this->registry->unregister( 'to_remove' );
355+
$result = $this->registry->unregister( 'to-remove' );
340356

341357
$this->assertIsArray( $result );
342358
$this->assertSame( 'Test Provider', $result['name'] );
343-
$this->assertFalse( $this->registry->is_registered( 'to_remove' ) );
359+
$this->assertFalse( $this->registry->is_registered( 'to-remove' ) );
344360
}
345361

346362
/**

tests/phpunit/tests/connectors/wpConnectorsGetConnectorSettings.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function test_returns_expected_connector_keys(): void {
3737
$this->assertArrayHasKey( 'google', $connectors );
3838
$this->assertArrayHasKey( 'openai', $connectors );
3939
$this->assertArrayHasKey( 'anthropic', $connectors );
40-
$this->assertArrayHasKey( 'mock_connectors_test', $connectors );
40+
$this->assertArrayHasKey( 'mock-connectors-test', $connectors );
4141
$this->assertCount( 4, $connectors );
4242
}
4343

@@ -80,7 +80,7 @@ public function test_api_key_connectors_have_setting_name_and_credentials_url():
8080

8181
$this->assertArrayHasKey( 'setting_name', $connector_data['authentication'], "Connector '{$connector_id}' authentication is missing 'setting_name'." );
8282
$this->assertSame(
83-
"connectors_ai_{$connector_id}_api_key",
83+
'connectors_ai_' . str_replace( '-', '_', $connector_id ) . '_api_key',
8484
$connector_data['authentication']['setting_name'] ?? null,
8585
"Connector '{$connector_id}' setting_name does not match expected format."
8686
);
@@ -105,7 +105,7 @@ public function test_featured_provider_names_match_expected(): void {
105105
*/
106106
public function test_includes_registered_provider_from_registry(): void {
107107
$connectors = wp_get_connectors();
108-
$mock = $connectors['mock_connectors_test'];
108+
$mock = $connectors['mock-connectors-test'];
109109

110110
$this->assertSame( 'Mock Connectors Test', $mock['name'] );
111111
$this->assertSame( '', $mock['description'] );

tests/phpunit/tests/connectors/wpConnectorsIsApiKeyValid.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public function test_unregistered_provider_returns_null() {
4949
public function test_configured_provider_returns_true() {
5050
self::set_mock_provider_configured( true );
5151

52-
$result = _wp_connectors_is_ai_api_key_valid( 'test-key', 'mock_connectors_test' );
52+
$result = _wp_connectors_is_ai_api_key_valid( 'test-key', 'mock-connectors-test' );
5353

5454
$this->assertTrue( $result );
5555
}
@@ -62,7 +62,7 @@ public function test_configured_provider_returns_true() {
6262
public function test_unconfigured_provider_returns_false() {
6363
self::set_mock_provider_configured( false );
6464

65-
$result = _wp_connectors_is_ai_api_key_valid( 'test-key', 'mock_connectors_test' );
65+
$result = _wp_connectors_is_ai_api_key_valid( 'test-key', 'mock-connectors-test' );
6666

6767
$this->assertFalse( $result );
6868
}

0 commit comments

Comments
 (0)