PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.4.0
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.4.0
2.4.1 2.4.0 2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.2 2.2.1 2.2.0 2.1.2 2.1.1 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 All 68 releases
better-payment / includes / AI / Schema / CampaignSchema.php

CampaignSchema.php in Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More 2.4.0, at includes/AI/Schema/CampaignSchema.php

605 lines 25.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Better_Payment\Lite\AI\Schema;
4
5 use Better_Payment\Lite\Campaign\Elements\ElementRegistry;
6
7 if ( ! defined( 'ABSPATH' ) ) {
8 exit;
9 }
10
11 /**
12 * Canonical, machine-readable description of a Better Payment campaign.
13 *
14 * This is the single source of truth the AI layer uses to (a) tell a model what
15 * it is allowed to produce and (b) validate what a model returns. It never
16 * hardcodes the element list — everything is derived live from
17 * {@see ElementRegistry::get_all()} so the AI schema can never drift from the
18 * builder's real capabilities.
19 *
20 * A campaign is structured JSON:
21 * {
22 * "layout": "1-column" | "2-column" | "3-column",
23 * "columns": [
24 * { "id": string, "label": string, "width": "NN%",
25 * "elements": [ { "id": string, "type": <element type>, "settings": { ... } } ] }
26 * ]
27 * }
28 * plus flat campaign meta (goal amount, colours, donation amounts, ...).
29 *
30 * @see \Better_Payment\Lite\AI\Operations\OperationValidator Uses these allowlists.
31 * @see \Better_Payment\Lite\AI\Prompt\PromptBuilder Serialises this for the model.
32 */
33 class CampaignSchema {
34
35 /**
36 * Feature slug Pro declares on `better_payment/pro_provides` once it ships the
37 * Pro-widget guidance itself: the generation instruction, the "Pro widgets"
38 * prompt section on a Pro install, the three widgets' content guides and the
39 * video URL guard. See Classes\ProCompatibility.
40 */
41 const PRO_GUIDANCE_FEATURE = 'ai_pro_widget_guidance';
42
43 /**
44 * Whether Lite still supplies its own copy of the Pro-widget guidance.
45 *
46 * True only for an active Pro that does not declare
47 * {@see self::PRO_GUIDANCE_FEATURE} (Better Payment Pro <= 1.4.3). A free install
48 * never runs the Pro-install guidance, the Pro content guides, the Pro setting-key
49 * aliases or the video URL guard: its Pro elements are lock stubs with no
50 * settings to write, and its locked-widget disclosure
51 * ({@see \Better_Payment\Lite\AI\Prompt\PromptBuilder}'s locked section,
52 * {@see \Better_Payment\Lite\AI\Services\AIService::pro_locked_notice()}) is
53 * not gated on this.
54 */
55 public static function lite_supplies_pro_guidance(): bool {
56 return \Better_Payment\Lite\Classes\ProCompatibility::lite_copy_applies( self::PRO_GUIDANCE_FEATURE );
57 }
58
59 /**
60 * Layout presets the builder understands.
61 *
62 * @return array<int, string>
63 */
64 public static function layout_presets(): array {
65 return [ '1-column', '2-column', '3-column' ];
66 }
67
68 /**
69 * Every registered element type key (e.g. campaign_title, photo, ...).
70 *
71 * @return array<int, string>
72 */
73 public static function element_types(): array {
74 return array_keys( ElementRegistry::get_all() );
75 }
76
77 /**
78 * Whether a given element type is registered.
79 *
80 * Deliberately entitlement-BLIND. A campaign built while Pro was active may
81 * still hold a `donors_wall`; when the licence lapses that element must
82 * survive an AI edit turn untouched, exactly as
83 * {@see \Better_Payment\Lite\Campaign\MetaBox::enforce_pro_entitlement()}
84 * restores its settings rather than deleting it. Making this method
85 * entitlement-aware would make an "edit the title" turn — whose `set_layout`
86 * echoes the whole page back — silently delete the user's Pro elements.
87 *
88 * Entitlement decides what we *offer* ({@see self::offerable_element_types()}),
89 * never what we recognise.
90 */
91 public static function is_element_type( string $type ): bool {
92 return in_array( $type, self::element_types(), true );
93 }
94
95 /**
96 * Whether Pro is active on this install, read live per request.
97 *
98 * Never inferred from stored campaign data — same contract the builder and
99 * MetaBox follow.
100 */
101 public static function pro_enabled(): bool {
102 return (bool) apply_filters( 'better_payment/pro_enabled', false );
103 }
104
105 /**
106 * Whether an element type is flagged Pro in the registry.
107 *
108 * The `pro` flag is present whether or not Pro is installed — on Lite's lock
109 * stubs and on Pro's full definitions alike — so this asks "is this a Pro
110 * widget", not "is it locked".
111 */
112 public static function is_pro_element_type( string $type ): bool {
113 $schema = ElementRegistry::get( $type );
114
115 return null !== $schema && ! empty( $schema['pro'] );
116 }
117
118 /**
119 * The element types this install may be offered: everything registered, minus
120 * the Pro ones when Pro is inactive.
121 *
122 * A free install used to be handed `donors_wall`, `faq` and `video` in the
123 * prompt under a rule reading "Only use the element types listed above" — so
124 * the model reasonably used them, and they reached a page that has no
125 * renderer for them and drops them to nothing. This is the list that decides
126 * what the model is told exists.
127 *
128 * @return array<int, string>
129 */
130 public static function offerable_element_types(): array {
131 $types = self::element_types();
132
133 if ( self::pro_enabled() ) {
134 return $types;
135 }
136
137 return array_values( array_filter(
138 $types,
139 static function ( $type ) {
140 return ! self::is_pro_element_type( $type );
141 }
142 ) );
143 }
144
145 /**
146 * Whether a type may be offered on this install (fed to the model in the
147 * prompt schema).
148 *
149 * NOTE this is no longer the gate for *inserting* an element. A free user who
150 * explicitly asks the AI for a Pro widget gets that exact widget dropped onto
151 * the canvas as a locked preview — see
152 * {@see \Better_Payment\Lite\AI\Operations\OperationValidator::validate_insert_block()}
153 * and {@see self::locked_pro_types_in_operations()}. Offering (proactive) and
154 * inserting (on explicit request) are now different questions.
155 */
156 public static function is_offerable_element_type( string $type ): bool {
157 return in_array( $type, self::offerable_element_types(), true );
158 }
159
160 /**
161 * The Pro element types this batch of operations puts onto the canvas that
162 * the install is NOT entitled to — the widgets that will render as a locked
163 * preview and stay inactive on the live page.
164 *
165 * This exists so the assistant can tell the user plainly, whatever the model
166 * wrote in its own reply, that a widget they asked for is a Pro-only feature.
167 * Reused by {@see \Better_Payment\Lite\AI\Services\AIService::run()}.
168 *
169 * Detection is deliberately "newly introduced", not merely "present". A
170 * lapsed-Pro user's routine edit ("shorten the title") comes back as a
171 * `set_layout` that echoes the whole page — leftover Pro widgets and all — and
172 * announcing "FAQ is a Pro widget" on every such turn would be noise about
173 * something the user did not just do. A type therefore counts only when an
174 * `insert_block` creates it, or a `set_layout` adds one the current layout did
175 * not already contain. Returns `[]` when Pro is active (nothing is locked).
176 *
177 * @param array $operations Validated operations (canonical `op` + type keys).
178 * @param array $context Trusted current state: [ 'layout' => [...] ].
179 * @return array<int, string> Distinct Pro type keys, in first-seen order.
180 */
181 public static function locked_pro_types_in_operations( array $operations, array $context = [] ): array {
182 if ( self::pro_enabled() ) {
183 return [];
184 }
185
186 $existing = self::pro_types_in_columns( $context['layout']['columns'] ?? [] );
187 $found = [];
188
189 foreach ( $operations as $operation ) {
190 if ( ! is_array( $operation ) ) {
191 continue;
192 }
193 $name = $operation['op'] ?? ( $operation['operation'] ?? '' );
194
195 if ( 'insert_block' === $name ) {
196 $type = self::resolve_element_type( is_string( $operation['type'] ?? null ) ? $operation['type'] : '' );
197 if ( '' !== $type && self::is_pro_element_type( $type ) && ! in_array( $type, $found, true ) ) {
198 $found[] = $type;
199 }
200 continue;
201 }
202
203 if ( 'set_layout' === $name ) {
204 foreach ( self::pro_types_in_columns( $operation['columns'] ?? [] ) as $type ) {
205 if ( ! in_array( $type, $existing, true ) && ! in_array( $type, $found, true ) ) {
206 $found[] = $type;
207 }
208 }
209 }
210 }
211
212 return $found;
213 }
214
215 /**
216 * Distinct Pro element type keys present in a set of layout columns.
217 *
218 * @param mixed $columns
219 * @return array<int, string>
220 */
221 private static function pro_types_in_columns( $columns ): array {
222 $types = [];
223 if ( ! is_array( $columns ) ) {
224 return $types;
225 }
226 foreach ( $columns as $column ) {
227 if ( ! is_array( $column ) || empty( $column['elements'] ) || ! is_array( $column['elements'] ) ) {
228 continue;
229 }
230 foreach ( $column['elements'] as $element ) {
231 if ( ! is_array( $element ) ) {
232 continue;
233 }
234 $type = self::resolve_element_type( is_string( $element['type'] ?? null ) ? $element['type'] : '' );
235 if ( '' !== $type && self::is_pro_element_type( $type ) && ! in_array( $type, $types, true ) ) {
236 $types[] = $type;
237 }
238 }
239 }
240 return $types;
241 }
242
243 /**
244 * Pro element type keys registered on this install, regardless of entitlement.
245 *
246 * Used by the prompt builder to name the locked Pro widgets to a free install
247 * (they are absent from {@see self::offerable_element_types()}), so the model
248 * can honour an explicit request for one instead of substituting a free widget.
249 *
250 * @return array<int, string>
251 */
252 public static function pro_element_types(): array {
253 return array_values( array_filter(
254 self::element_types(),
255 [ self::class, 'is_pro_element_type' ]
256 ) );
257 }
258
259 /**
260 * Common alias → canonical element type. Models frequently emit generic names
261 * (heading, paragraph, image, button …) instead of the registry keys; mapping
262 * them recovers the element instead of silently dropping it.
263 *
264 * @return array<string, string>
265 */
266 private static function type_aliases(): array {
267 return apply_filters( 'better_payment/ai/element_type_aliases', [
268 'heading' => 'campaign_title', 'title' => 'campaign_title', 'header' => 'campaign_title', 'headline' => 'campaign_title', 'campaigntitle' => 'campaign_title',
269 'paragraph' => 'campaign_description', 'text' => 'campaign_description', 'story' => 'campaign_description', 'description' => 'campaign_description', 'content' => 'campaign_description', 'body' => 'campaign_description', 'richtext' => 'campaign_description', 'rich_text' => 'campaign_description',
270 'image' => 'photo', 'img' => 'photo', 'picture' => 'photo', 'hero_image' => 'photo', 'heroimage' => 'photo', 'gallery' => 'photo',
271 'button' => 'donation_form', 'cta' => 'donation_form', 'donate_button' => 'donation_form', 'donatebutton' => 'donation_form', 'donate' => 'donation_form', 'donatenow' => 'donation_form', 'donate_now' => 'donation_form',
272 'progress' => 'progress_bar', 'progressbar' => 'progress_bar',
273 'donation' => 'donate_amount', 'amounts' => 'donate_amount', 'donation_amounts' => 'donate_amount', 'suggested_amounts' => 'donate_amount', 'donation_amount' => 'donate_amount', 'tiers' => 'donate_amount',
274 'summary' => 'campaign_summary', 'stats' => 'campaign_summary', 'statistics' => 'campaign_summary', 'campaignsummary' => 'campaign_summary',
275 'share' => 'social_sharing', 'sharing' => 'social_sharing', 'social' => 'social_sharing', 'socialshare' => 'social_sharing', 'social_share' => 'social_sharing',
276 'links' => 'social_links', 'sociallinks' => 'social_links', 'social_link' => 'social_links',
277 'author' => 'organizer', 'creator' => 'organizer', 'organizer_card' => 'organizer', 'host' => 'organizer',
278 // Pro elements. Aliased unconditionally — resolution is not permission,
279 // and a free install simply never sees these types offered.
280 'donors' => 'donors_wall', 'donorswall' => 'donors_wall', 'donor_list' => 'donors_wall', 'donorlist' => 'donors_wall', 'supporters' => 'donors_wall', 'recent_donors' => 'donors_wall', 'contributors' => 'donors_wall',
281 'faqs' => 'faq', 'questions' => 'faq', 'accordion' => 'faq', 'q_and_a' => 'faq', 'qa' => 'faq', 'frequently_asked_questions' => 'faq',
282 'youtube' => 'video', 'vimeo' => 'video', 'embed' => 'video', 'video_embed' => 'video', 'media' => 'video',
283 ] );
284 }
285
286 /**
287 * Resolve a possibly-aliased element type to a registered type, or '' when it
288 * cannot be mapped.
289 */
290 public static function resolve_element_type( string $type ): string {
291 $t = strtolower( trim( $type ) );
292 if ( self::is_element_type( $t ) ) {
293 return $t;
294 }
295 $norm = preg_replace( '/[\s\-]+/', '_', $t );
296 if ( self::is_element_type( $norm ) ) {
297 return $norm;
298 }
299 $aliases = self::type_aliases();
300 if ( isset( $aliases[ $t ] ) ) {
301 return $aliases[ $t ];
302 }
303 if ( isset( $aliases[ $norm ] ) ) {
304 return $aliases[ $norm ];
305 }
306 return '';
307 }
308
309 /**
310 * Per-type alias → canonical setting key, so a mis-named content key
311 * (e.g. `text` on a title) still lands in the right place.
312 *
313 * @return array<string, array<string, string>>
314 */
315 private static function setting_key_aliases(): array {
316 $aliases = [
317 'campaign_title' => [ 'text' => 'title', 'heading' => 'title', 'label' => 'title', 'headline' => 'title', 'name' => 'title' ],
318 'campaign_description' => [ 'text' => 'content', 'body' => 'content', 'paragraph' => 'content', 'description' => 'content', 'story' => 'content', 'heading' => 'headline', 'title' => 'headline' ],
319 'donation_form' => [ 'text' => 'button_label', 'label' => 'button_label', 'cta' => 'button_label', 'button_text' => 'button_label', 'title' => 'button_label' ],
320 'photo' => [ 'url' => 'src', 'image' => 'src', 'source' => 'src', 'href' => 'src' ],
321 'progress_bar' => [ 'title' => 'headline', 'text' => 'headline' ],
322 'campaign_summary' => [ 'title' => 'headline', 'text' => 'headline' ],
323 'donate_amount' => [ 'title' => 'headline', 'text' => 'headline' ],
324 'organizer' => [ 'text' => 'description', 'bio' => 'description', 'name' => 'role_title', 'title' => 'role_title' ],
325 'social_sharing' => [ 'title' => 'headline', 'text' => 'headline' ],
326 'social_links' => [ 'title' => 'headline', 'text' => 'headline' ],
327 ];
328
329 // Pro supplies its widgets' aliases through the filter below once it declares
330 // `ai_pro_widget_guidance`; this copy serves older Pro releases only.
331 if ( self::lite_supplies_pro_guidance() ) {
332 $aliases += self::lite_pro_setting_key_aliases();
333 }
334
335 return apply_filters( 'better_payment/ai/setting_key_aliases', $aliases );
336 }
337
338 /**
339 * Transitional copy for Better Payment Pro <= 1.4.3; delete when
340 * ProCompatibility::MIN_PRO_VERSION >= 1.4.4.
341 * Pro copy: better-payment-pro/includes/AI/ProWidgetGuidance.php (setting_key_aliases()).
342 * Slug: ai_pro_widget_guidance.
343 *
344 * Setting-key aliases for the three Pro widgets. Only reached through
345 * {@see self::setting_key_aliases()} while {@see self::lite_supplies_pro_guidance()}.
346 *
347 * @return array<string, array<string, string>>
348 */
349 public static function lite_pro_setting_key_aliases(): array {
350 return [
351 'donors_wall' => [ 'title' => 'headline', 'text' => 'headline', 'heading' => 'headline', 'limit' => 'number_to_show', 'count' => 'number_to_show' ],
352 // `faq` names its heading `heading`, not `headline` — the one element
353 // that breaks the pattern, so the reverse alias matters here.
354 'faq' => [ 'title' => 'heading', 'text' => 'heading', 'headline' => 'heading', 'questions' => 'items', 'faqs' => 'items', 'list' => 'items' ],
355 'video' => [ 'src' => 'url', 'link' => 'url', 'video_url' => 'url', 'embed_url' => 'url' ],
356 ];
357 }
358
359 /**
360 * Resolve a possibly-aliased setting key for a type. Returns the canonical key
361 * when it is (or maps to) an allowed key, otherwise the original (which the
362 * allowlist will then drop).
363 */
364 public static function resolve_setting_key( string $type, string $key ): string {
365 if ( self::is_allowed_settings_key( $type, $key ) ) {
366 return $key;
367 }
368 $map = self::setting_key_aliases();
369 $k = strtolower( trim( $key ) );
370 if ( isset( $map[ $type ][ $k ] ) ) {
371 return $map[ $type ][ $k ];
372 }
373 return $key;
374 }
375
376 /**
377 * Campaign-level meta keys the AI is allowed to write.
378 *
379 * Deliberately a subset of {@see \Better_Payment\Lite\Campaign\MetaBox} keys:
380 * layout (bpc_fields_layout), the form page id and the template key are
381 * managed structurally, not by free-form AI meta writes.
382 *
383 * @return array<int, string>
384 */
385 public static function writable_meta_keys(): array {
386 $keys = [
387 'title',
388 'bpc_goal_amount',
389 'bpc_end_date',
390 'bpc_status',
391 'bpc_allow_custom_amount',
392 'bpc_minimum_amount',
393 'bpc_color_primary',
394 'bpc_color_background',
395 'bpc_css_class',
396 'bpc_suggested_amounts',
397 ];
398
399 /**
400 * Filter the campaign meta keys the AI layer may write.
401 *
402 * @param array<int, string> $keys
403 */
404 return apply_filters( 'better_payment/ai/writable_meta_keys', $keys );
405 }
406
407 public static function is_writable_meta_key( string $key ): bool {
408 return in_array( $key, self::writable_meta_keys(), true );
409 }
410
411 /**
412 * The full set of setting keys a given element type accepts.
413 *
414 * Built from the element's defaultSettings keys plus every control key in
415 * its settingsSchema (recursing into collapsible `section` children). Synthetic
416 * container keys (sections, notes — always prefixed with `_`) are excluded.
417 *
418 * @return array<int, string>
419 */
420 public static function allowed_settings_keys( string $type ): array {
421 $schema = ElementRegistry::get( $type );
422 if ( null === $schema ) {
423 return [];
424 }
425
426 $keys = array_keys( (array) ( $schema['defaultSettings'] ?? [] ) );
427
428 $collect = function ( $controls, &$out ) use ( &$collect ) {
429 foreach ( (array) $controls as $control ) {
430 if ( ! is_array( $control ) ) {
431 continue;
432 }
433 $ctype = $control['type'] ?? '';
434 if ( in_array( $ctype, [ 'section', 'section_label', 'note' ], true ) ) {
435 if ( ! empty( $control['children'] ) ) {
436 $collect( $control['children'], $out );
437 }
438 continue;
439 }
440 if ( ! empty( $control['key'] ) && 0 !== strpos( (string) $control['key'], '_' ) ) {
441 $out[] = (string) $control['key'];
442 }
443 if ( ! empty( $control['children'] ) ) {
444 $collect( $control['children'], $out );
445 }
446 }
447 };
448
449 $collect( $schema['settingsSchema'] ?? [], $keys );
450
451 return array_values( array_unique( $keys ) );
452 }
453
454 public static function is_allowed_settings_key( string $type, string $key ): bool {
455 return in_array( $key, self::allowed_settings_keys( $type ), true );
456 }
457
458 /**
459 * Enumerated values for a select/align control, or null when the key is free-form.
460 *
461 * @return array<int, string>|null
462 */
463 public static function enum_values( string $type, string $key ) {
464 // `align` controls carry no options array but accept a fixed set.
465 $control = self::find_control( $type, $key );
466 if ( null === $control ) {
467 return null;
468 }
469 if ( 'align' === ( $control['type'] ?? '' ) ) {
470 return [ 'left', 'center', 'right' ];
471 }
472 if ( ! empty( $control['options'] ) && is_array( $control['options'] ) ) {
473 $values = [];
474 foreach ( $control['options'] as $option ) {
475 if ( is_array( $option ) && array_key_exists( 'value', $option ) ) {
476 $values[] = (string) $option['value'];
477 }
478 }
479 return $values;
480 }
481 return null;
482 }
483
484 /**
485 * Locate a single control definition (recursing sections) for an element key.
486 *
487 * @return array|null
488 */
489 public static function find_control( string $type, string $key ) {
490 $schema = ElementRegistry::get( $type );
491 if ( null === $schema ) {
492 return null;
493 }
494
495 $search = function ( $controls ) use ( &$search, $key ) {
496 foreach ( (array) $controls as $control ) {
497 if ( ! is_array( $control ) ) {
498 continue;
499 }
500 if ( ( $control['key'] ?? null ) === $key ) {
501 return $control;
502 }
503 if ( ! empty( $control['children'] ) ) {
504 $found = $search( $control['children'] );
505 if ( null !== $found ) {
506 return $found;
507 }
508 }
509 }
510 return null;
511 };
512
513 return $search( $schema['settingsSchema'] ?? [] );
514 }
515
516 /**
517 * A compact, prompt-friendly description of the whole builder capability set.
518 *
519 * Shape:
520 * [
521 * 'layout_presets' => [...],
522 * 'meta_keys' => [...],
523 * 'elements' => [
524 * [ 'type' => 'campaign_title', 'label' => '...', 'guide' => '...',
525 * 'settings' => [ 'title' => [ 'type' => 'text' ],
526 * 'align' => [ 'type' => 'align', 'enum' => ['left','center','right'] ] ] ],
527 * ...
528 * ],
529 * ]
530 *
531 * Only settable, non-container keys are described, keeping the prompt small.
532 *
533 * Two things shape this list beyond the raw registry:
534 * - Pro elements are omitted unless Pro is active. Describing a widget the
535 * install cannot render is not a harmless extra option: the rules tell the
536 * model these types are the ones it may use.
537 * - Each element carries a `guide` — what its content is actually *for*.
538 * A key list alone is not self-describing, and three elements sharing a
539 * key named `headline` otherwise get three copies of the same sentence.
540 *
541 * @return array<string, mixed>
542 */
543 public static function for_prompt(): array {
544 $elements = [];
545
546 foreach ( self::offerable_element_types() as $type ) {
547 $elements[] = self::describe_element( $type );
548 }
549
550 return [
551 'layout_presets' => self::layout_presets(),
552 'meta_keys' => self::writable_meta_keys(),
553 'elements' => $elements,
554 ];
555 }
556
557 /**
558 * Describe ONE element type: its label, its content guide, and every settable
559 * key with that key's control type and enum.
560 *
561 * Extracted from {@see self::for_prompt()} so the whole-page schema and the
562 * single-widget "target element" prompt section are built from the same code.
563 * Two builders would drift, and a drift here is not cosmetic — the targeted
564 * section exists precisely to tell the model which fields the widget in front
565 * of the user actually has, so a stale copy would describe a widget that is
566 * not the one being edited.
567 *
568 * Deliberately **entitlement-blind**, unlike {@see self::for_prompt()}. This
569 * describes an element that already exists on the page, and the caller has
570 * already decided it may be edited; filtering here would leave a lapsed
571 * subscriber's Pro widget selectable but undescribable, so the model would be
572 * asked to edit a widget it had been told nothing about. Entitlement decides
573 * what is *offered* (`offerable_element_types()`) and what may be newly
574 * *inserted* (`validate_insert_block()`) — never what is recognised.
575 *
576 * @param string $type Element type key.
577 * @return array{type: string, label: string, guide: string, settings: array<string, array>}
578 */
579 public static function describe_element( string $type ): array {
580 $schema = ElementRegistry::get( $type );
581 $schema = is_array( $schema ) ? $schema : [];
582
583 $settings = [];
584 foreach ( self::allowed_settings_keys( $type ) as $key ) {
585 $control = self::find_control( $type, $key );
586 $entry = [ 'type' => $control['type'] ?? 'text' ];
587 $enum = self::enum_values( $type, $key );
588 if ( null !== $enum && ! empty( $enum ) ) {
589 $entry['enum'] = $enum;
590 }
591 if ( ! empty( $control['label'] ) ) {
592 $entry['label'] = (string) $control['label'];
593 }
594 $settings[ $key ] = $entry;
595 }
596
597 return [
598 'type' => $type,
599 'label' => (string) ( $schema['label'] ?? $type ),
600 'guide' => ElementContentGuide::for_type( $type ),
601 'settings' => $settings,
602 ];
603 }
604 }
605