Skip to content

Commit 66c05ed

Browse files
Address review feedback on the core/settings ability
- Simplify the input schema: replace the group-XOR-name `oneOf` with optional, combinable `group` and `fields` filters (rename `settings` -> `fields`, matching core/get-site-info). Default to an empty object so the type:object schema default serializes as {}. - Memoize the exposed settings so the input/output schema and execute() derive from a single walk of get_registered_settings(). - Cast object-typed values to objects so they serialize as {} (not []) and satisfy the output schema validated by execute(). - Drop the redundant `site` ability-category fallback (core registers it and the plugin requires WP 7.0). - Use __() instead of esc_html__() for ability strings, and @SInCE x.x.x per CONTRIBUTING.md. - Harden value handling so the new code passes PHPStan at the strictest level. - Tests: assert keys order-insensitively and cover combined group+fields filtering.
1 parent 8970d2d commit 66c05ed

5 files changed

Lines changed: 134 additions & 150 deletions

File tree

includes/Abilities/Settings/Settings.php

Lines changed: 79 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*
55
* @package WordPress\AI
66
*
7-
* @since 1.1.0
7+
* @since x.x.x
88
*/
99

1010
declare( strict_types=1 );
@@ -24,67 +24,53 @@
2424
*
2525
* This class is kept almost identical to the WordPress core class `WP_Settings_Abilities`
2626
* so the two implementations stay in sync. Differences from the core class are marked with
27-
* `// Plugin:` comments. Additionally, all user-facing strings use esc_html__() with the
28-
* 'ai' text domain rather than core's __().
27+
* `// Plugin:` comments. Additionally, all user-facing strings use the 'ai' text domain.
2928
*
3029
* @internal This class should not be used outside the plugin and there is no guarantee of backwards compatibility.
3130
*
32-
* @since 1.1.0
31+
* @since x.x.x
3332
*/
3433
class Settings {
3534

3635
/**
3736
* The ability category used for settings abilities.
3837
*
39-
* @since 1.1.0
38+
* @since x.x.x
4039
* @var string
4140
*/
4241
public const CATEGORY = 'site';
4342

43+
/**
44+
* Settings exposed through the Abilities API, computed once at registration.
45+
*
46+
* Plugin: cached so the input/output schema and the executed result derive from the exact
47+
* same structure, and {@see get_registered_settings()} is only walked once per request.
48+
*
49+
* @since x.x.x
50+
* @var array<string, array{option: string, group: string, default: mixed, schema: array<string, mixed>}>|null
51+
*/
52+
private static $exposed_settings = null;
53+
4454
/**
4555
* Hooks the ability into the Abilities API.
4656
*
4757
* Plugin: this method has no equivalent in the core class. In core, register() is
4858
* invoked directly from wp_register_core_abilities() (already on the
4959
* `wp_abilities_api_init` hook). The plugin instead hooks register() slightly later
50-
* (priority 11) so it can override any core-provided copy, and registers the category
51-
* as a fallback in case core has not.
60+
* (priority 11) so it can override any core-provided copy.
5261
*
53-
* @since 1.1.0
62+
* @since x.x.x
5463
*/
5564
public static function init(): void {
56-
add_action( 'wp_abilities_api_categories_init', array( self::class, 'register_category' ), 11 );
5765
add_action( 'wp_abilities_api_init', array( self::class, 'register' ), 11 );
5866
}
5967

60-
/**
61-
* Registers the `site` ability category if it is not already registered.
62-
*
63-
* Plugin: this method has no equivalent in the core class; core relies on
64-
* wp_register_core_ability_categories() to register the `site` category.
65-
*
66-
* @since 1.1.0
67-
*/
68-
public static function register_category(): void {
69-
if ( wp_has_ability_category( self::CATEGORY ) ) {
70-
return;
71-
}
72-
73-
wp_register_ability_category(
74-
self::CATEGORY,
75-
array(
76-
'label' => esc_html__( 'Site', 'ai' ),
77-
'description' => esc_html__( 'Abilities that retrieve or modify site information and settings.', 'ai' ),
78-
)
79-
);
80-
}
81-
8268
/**
8369
* Registers all settings abilities.
8470
*
8571
* Must run on the `wp_abilities_api_init` hook.
8672
*
87-
* @since 1.1.0
73+
* @since x.x.x
8874
*/
8975
public static function register(): void {
9076
self::register_get_settings();
@@ -100,32 +86,39 @@ public static function register(): void {
10086
/**
10187
* Registers the read-only `core/settings` ability.
10288
*
103-
* @since 1.1.0
89+
* @since x.x.x
10490
*/
10591
public static function register_get_settings(): void {
10692
// Plugin: unregister any core-provided copy first so the plugin's version wins.
10793
if ( wp_has_ability( 'core/settings' ) ) {
10894
wp_unregister_ability( 'core/settings' );
10995
}
11096

111-
$settings = self::get_exposed_settings();
112-
$groups = array_values( array_unique( array_filter( wp_list_pluck( $settings, 'group' ) ) ) );
113-
$setting_names = array_keys( $settings );
114-
$properties = array();
97+
// Compute once; execute_get_settings() reuses this exact structure.
98+
self::$exposed_settings = self::get_exposed_settings();
99+
100+
$settings = self::$exposed_settings;
101+
$field_names = array_keys( $settings );
102+
$groups = array();
103+
$properties = array();
115104
foreach ( $settings as $exposed_name => $setting ) {
116105
$properties[ $exposed_name ] = $setting['schema'];
106+
if ( '' === $setting['group'] || in_array( $setting['group'], $groups, true ) ) {
107+
continue;
108+
}
109+
$groups[] = $setting['group'];
117110
}
118111

119112
wp_register_ability(
120113
'core/settings',
121114
array(
122-
'label' => esc_html__( 'Get Settings', 'ai' ),
123-
'description' => esc_html__( 'Returns WordPress settings as a flat map of setting name to value. By default returns all settings exposed to abilities, or optionally a subset filtered by settings group or by setting name.', 'ai' ),
115+
'label' => __( 'Get Settings', 'ai' ),
116+
'description' => __( 'Returns WordPress settings as a flat map of setting name to value. By default returns all settings exposed to abilities, or optionally a subset filtered by settings group, by setting name, or both.', 'ai' ),
124117
'category' => self::CATEGORY,
125-
'input_schema' => self::get_settings_input_schema( $groups, $setting_names ),
118+
'input_schema' => self::get_settings_input_schema( $groups, $field_names ),
126119
'output_schema' => array(
127120
'type' => 'object',
128-
'description' => esc_html__( 'A map of setting name to its current value.', 'ai' ),
121+
'description' => __( 'A map of setting name to its current value.', 'ai' ),
129122
'properties' => $properties,
130123
'additionalProperties' => false,
131124
),
@@ -146,28 +139,28 @@ public static function register_get_settings(): void {
146139
/**
147140
* Executes the `core/settings` ability.
148141
*
149-
* @since 1.1.0
142+
* @since x.x.x
150143
*
151144
* @param mixed $input Optional. The ability input. Default empty array.
152145
* @return array<string, mixed> Map of exposed setting name to current value.
153146
*/
154147
public static function execute_get_settings( $input = array() ): array {
155148
$input = is_array( $input ) ? $input : array();
156149

157-
$settings = self::get_exposed_settings();
158-
$group = isset( $input['group'] ) ? (string) $input['group'] : '';
159-
$names = isset( $input['settings'] ) && is_array( $input['settings'] ) ? $input['settings'] : array();
150+
$settings = self::$exposed_settings ?? self::get_exposed_settings();
151+
$group = isset( $input['group'] ) && is_string( $input['group'] ) ? $input['group'] : '';
152+
$fields = isset( $input['fields'] ) && is_array( $input['fields'] ) ? $input['fields'] : array();
160153

161154
$result = array();
162155
foreach ( $settings as $exposed_name => $setting ) {
163156
if ( '' !== $group && $setting['group'] !== $group ) {
164157
continue;
165158
}
166-
if ( ! empty( $names ) && ! in_array( $exposed_name, $names, true ) ) {
159+
if ( ! empty( $fields ) && ! in_array( $exposed_name, $fields, true ) ) {
167160
continue;
168161
}
169162

170-
$type = isset( $setting['schema']['type'] ) ? (string) $setting['schema']['type'] : 'string';
163+
$type = isset( $setting['schema']['type'] ) && is_string( $setting['schema']['type'] ) ? $setting['schema']['type'] : 'string';
171164
$value = get_option( $setting['option'], $setting['default'] );
172165

173166
$result[ $exposed_name ] = self::cast_value( $value, $type );
@@ -179,7 +172,7 @@ public static function execute_get_settings( $input = array() ): array {
179172
/**
180173
* Checks whether the current user may use the settings abilities.
181174
*
182-
* @since 1.1.0
175+
* @since x.x.x
183176
*
184177
* @return bool True if the current user can manage options.
185178
*/
@@ -188,55 +181,38 @@ public static function has_permission(): bool {
188181
}
189182

190183
/**
191-
* Builds the input schema for the get ability: filter by group XOR by name.
184+
* Builds the input schema for the get ability: optional filters by group and/or name.
192185
*
193-
* @since 1.1.0
186+
* Both `group` and `fields` are optional; supplying both narrows the response to their
187+
* intersection, and supplying neither returns every exposed setting.
194188
*
195-
* @param string[] $groups Available settings groups.
196-
* @param string[] $setting_names Available exposed setting names.
189+
* @since x.x.x
190+
*
191+
* @param string[] $groups Available settings groups.
192+
* @param string[] $field_names Available exposed setting names.
197193
* @return array<string, mixed> The input JSON Schema.
198194
*/
199-
protected static function get_settings_input_schema( array $groups, array $setting_names ): array {
195+
protected static function get_settings_input_schema( array $groups, array $field_names ): array {
200196
return array(
201-
'type' => 'object',
202-
'default' => array(),
203-
// Filter by group OR by name, but not both at once.
204-
'oneOf' => array(
205-
array(
206-
'title' => esc_html__( 'All settings', 'ai' ),
207-
'type' => 'object',
208-
'additionalProperties' => false,
197+
'type' => 'object',
198+
// Object (not array()) so the serialized schema default is {}, consistent with type:object.
199+
'default' => (object) array(),
200+
'properties' => array(
201+
'group' => array(
202+
'type' => 'string',
203+
'enum' => $groups,
204+
'description' => __( 'Return only settings that belong to this settings group.', 'ai' ),
209205
),
210-
array(
211-
'title' => esc_html__( 'Filter by group', 'ai' ),
212-
'type' => 'object',
213-
'required' => array( 'group' ),
214-
'properties' => array(
215-
'group' => array(
216-
'type' => 'string',
217-
'enum' => $groups,
218-
'description' => esc_html__( 'Return only settings that belong to this settings group.', 'ai' ),
219-
),
206+
'fields' => array(
207+
'type' => 'array',
208+
'items' => array(
209+
'type' => 'string',
210+
'enum' => $field_names,
220211
),
221-
'additionalProperties' => false,
222-
),
223-
array(
224-
'title' => esc_html__( 'Filter by name', 'ai' ),
225-
'type' => 'object',
226-
'required' => array( 'settings' ),
227-
'properties' => array(
228-
'settings' => array(
229-
'type' => 'array',
230-
'items' => array(
231-
'type' => 'string',
232-
'enum' => $setting_names,
233-
),
234-
'description' => esc_html__( 'Return only the settings with these names.', 'ai' ),
235-
),
236-
),
237-
'additionalProperties' => false,
212+
'description' => __( 'Return only the settings with these names.', 'ai' ),
238213
),
239214
),
215+
'additionalProperties' => false,
240216
);
241217
}
242218

@@ -248,7 +224,7 @@ protected static function get_settings_input_schema( array $groups, array $setti
248224
* underlying option name, the settings group, the registration default, and a JSON Schema
249225
* describing the value.
250226
*
251-
* @since 1.1.0
227+
* @since x.x.x
252228
*
253229
* @return array<string, array{option: string, group: string, default: mixed, schema: array<string, mixed>}> Settings keyed by exposed name.
254230
*/
@@ -262,11 +238,11 @@ protected static function get_exposed_settings(): array {
262238
}
263239

264240
$option_name = (string) $option_name;
265-
$exposed_name = is_array( $show ) && ! empty( $show['name'] ) ? (string) $show['name'] : $option_name;
241+
$exposed_name = is_array( $show ) && isset( $show['name'] ) && is_string( $show['name'] ) && '' !== $show['name'] ? $show['name'] : $option_name;
266242

267243
$settings[ $exposed_name ] = array(
268244
'option' => $option_name,
269-
'group' => isset( $args['group'] ) ? (string) $args['group'] : '',
245+
'group' => isset( $args['group'] ) && is_string( $args['group'] ) ? $args['group'] : '',
270246
'default' => array_key_exists( 'default', $args ) ? $args['default'] : false,
271247
'schema' => self::value_schema( $args, $show ),
272248
);
@@ -278,15 +254,15 @@ protected static function get_exposed_settings(): array {
278254
/**
279255
* Builds the JSON Schema describing a single setting's value.
280256
*
281-
* @since 1.1.0
257+
* @since x.x.x
282258
*
283259
* @param array<string, mixed> $args The setting registration arguments.
284260
* @param bool|array<string, mixed> $show The setting's `show_in_abilities` value.
285261
* @return array<string, mixed> The value JSON Schema.
286262
*/
287263
protected static function value_schema( array $args, $show ): array {
288264
$schema = array(
289-
'type' => isset( $args['type'] ) ? (string) $args['type'] : 'string',
265+
'type' => isset( $args['type'] ) && is_string( $args['type'] ) ? $args['type'] : 'string',
290266
);
291267
if ( ! empty( $args['label'] ) ) {
292268
$schema['title'] = $args['label'];
@@ -295,7 +271,9 @@ protected static function value_schema( array $args, $show ): array {
295271
$schema['description'] = $args['description'];
296272
}
297273
if ( is_array( $show ) && isset( $show['schema'] ) && is_array( $show['schema'] ) ) {
298-
$schema = array_merge( $schema, $show['schema'] );
274+
/** @var array<string, mixed> $show_schema */
275+
$show_schema = $show['schema'];
276+
$schema = array_merge( $schema, $show_schema );
299277
}
300278

301279
return $schema;
@@ -304,7 +282,7 @@ protected static function value_schema( array $args, $show ): array {
304282
/**
305283
* Casts a stored option value to the type declared in its settings registration.
306284
*
307-
* @since 1.1.0
285+
* @since x.x.x
308286
*
309287
* @param mixed $value The raw option value.
310288
* @param string $type The registered setting type.
@@ -315,12 +293,15 @@ protected static function cast_value( $value, string $type ) {
315293
case 'boolean':
316294
return (bool) $value;
317295
case 'integer':
318-
return (int) $value;
296+
return is_scalar( $value ) ? (int) $value : 0;
319297
case 'number':
320-
return (float) $value;
298+
return is_scalar( $value ) ? (float) $value : 0.0;
321299
case 'array':
322-
case 'object':
323300
return is_array( $value ) ? $value : array();
301+
case 'object':
302+
// Cast to object so an empty/non-array value serializes as {} (not []) and
303+
// satisfies the `object` output schema validated by execute().
304+
return (object) ( is_array( $value ) ? $value : array() );
324305
default:
325306
return is_scalar( $value ) ? (string) $value : $value;
326307
}

includes/Abilities/Show_In_Abilities.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*
55
* @package WordPress\AI
66
*
7-
* @since 1.1.0
7+
* @since x.x.x
88
*/
99

1010
declare( strict_types=1 );
@@ -27,14 +27,14 @@
2727
*
2828
* @internal This class should not be used outside the plugin and there is no guarantee of backwards compatibility.
2929
*
30-
* @since 1.1.0
30+
* @since x.x.x
3131
*/
3232
class Show_In_Abilities {
3333

3434
/**
3535
* Registers the hooks that mark core objects as exposed to abilities.
3636
*
37-
* @since 1.1.0
37+
* @since x.x.x
3838
*/
3939
public static function register(): void {
4040
add_filter( 'register_setting_args', array( self::class, 'mark_setting' ), 10, 4 );
@@ -46,7 +46,7 @@ public static function register(): void {
4646
* Respects an explicit `show_in_abilities` value already present on the setting (for
4747
* example once core ships it natively), only filling it in when absent.
4848
*
49-
* @since 1.1.0
49+
* @since x.x.x
5050
*
5151
* @param array<string, mixed> $args The setting registration arguments.
5252
* @param array<string, mixed> $defaults The default registration arguments.
@@ -71,7 +71,7 @@ public static function mark_setting( array $args, array $defaults, string $optio
7171
* optional `name` and `schema` keys (mirroring the `show_in_rest` shape). This matches
7272
* the set marked natively by the core `core/settings` implementation.
7373
*
74-
* @since 1.1.0
74+
* @since x.x.x
7575
*
7676
* @return array<string, bool|array<string, mixed>> Settings map keyed by option name.
7777
*/

0 commit comments

Comments
 (0)