>|null */ private static ?array $sections = null; /** * Every registered section, normalised, sorted, and name-resolved. * * @since 1.4.0 * * @return array> */ public static function collect(): array { if ( self::$sections !== null ) { return self::$sections; } $registry = SettingsRegistry::instance(); // Register Free's built-in sections FIRST so the synthetic // `albert/settings` card created by `albert_register_setting()` can slot // between them in the rendered order. foreach ( SettingsBootstrap::get_builtin_sections() as $builtin ) { $registry->register_section( $builtin ); } /** * Fires before the unified settings sections are collected. * * Add-ons hook here to call {@see albert_register_setting()} or (for * advanced use) {@see albert_register_settings_section()}. * * Fires once per request. See the class docblock: firing it twice would * duplicate every field registered through the simple API. * * @since 1.1.0 */ do_action( 'albert/settings/register' ); /** * Filters the final list of settings sections. * * Last chance to add, remove, or re-order sections before render or save. * * @since 1.1.0 * * @param array> $sections Normalised, sorted sections. */ $sections = apply_filters( 'albert/settings/sections', $registry->get_sections() ); self::$sections = self::resolve_option_names( is_array( $sections ) ? $sections : [] ); return self::$sections; } /** * Forget the collected sections. Intended for tests. * * @since 1.4.0 * * @return void */ public static function reset_cache(): void { self::$sections = null; } /** * Stamp every field with the option name it reads and writes. * * One derivation, once, for every consumer. This exists because it * previously happened twice and the two disagreed: the renderer derived the * input's `name` as `option_name ?? id`, while the save loop derived the * `$_POST` key as `option_name ?? {section_id}_{field_id}`. Every field that * set an explicit `option_name` masked the difference; the privacy mode * field, the only one that did not, rendered `name="mode"` while the save * loop looked for `albert_privacy_mode`. The key was therefore never * present, the sanitiser always saw null, and its default was written back * on every save — so the setting silently reverted and could not be changed * from the screen at all. * * Resolving here, after `albert/settings/sections` so a filtered-in field * gets the same treatment, means every consumer reads the same value: * there is nothing left to derive. * * @since 1.4.0 * * @param array> $sections Normalised sections. * * @return array> */ private static function resolve_option_names( array $sections ): array { foreach ( $sections as $s_index => $section ) { if ( ! isset( $section['fields'] ) || ! is_array( $section['fields'] ) ) { continue; } $section_id = isset( $section['id'] ) && is_string( $section['id'] ) ? $section['id'] : ''; foreach ( $section['fields'] as $f_index => $field ) { if ( ! is_array( $field ) ) { continue; } $sections[ $s_index ]['fields'][ $f_index ]['option_name'] = SettingsRegistry::get_option_name( $section_id, isset( $field['id'] ) && is_string( $field['id'] ) ? $field['id'] : '', isset( $field['option_name'] ) && is_string( $field['option_name'] ) ? $field['option_name'] : null ); } } return $sections; } }