“option_name” ] * e.g.: [ “ad_label_text” => “adpresso_settings_general” ] * @var array */ private array $map = []; /** * Request-Cache for already loaded WordPress options. * @var array */ private array $cache = []; /** * Keys the tabs registered in this request, with their option group. The map alone can't tell a * duplicate: outside the admin it starts out filled from the stored copy. * @var array */ private array $registered = []; /** * Cache key for the map of settings. */ private const MAP_CACHE_KEY = 'adpresso_settings_map'; /** * Prevents direct instantiation of the Settings class. */ private function __construct() { if ( is_admin() ) { $this->setup_backend_hooks(); } else { $this->load_map_from_cache(); } } /** * Get the singleton instance of the Settings class. */ public static function get_instance(): self { if ( self::$instance === null ) { self::$instance = new self(); } return self::$instance; } /** * Loads the settings map from the WordPress option cache. */ private function load_map_from_cache(): void { $this->map = get_option( self::MAP_CACHE_KEY, [] ); } /** * Registers the admin hooks to build and save the map. * Not tied to saving a tab: a first save goes through add_option, an unchanged one fires nothing, and an update can add keys. * Frontend code running before the tabs register (the ads.txt route, module loaders) only has this stored map. */ private function setup_backend_hooks(): void { add_action( 'admin_init', [ $this, 'regenerate_map_cache' ], 10, 0 ); } /** * Builds the $map property and saves it to the database. * Runs on every admin request; update_option() only writes when the map changed. */ public function regenerate_map_cache(): void { $this->map = []; $this->registered = []; $settings_page = SettingsPage::get(); foreach ( $settings_page->get_tabs() as $tab ) { $this->register_options_from_tab( $tab->get_id(), $tab->get_options() ); } // Autoloaded on purpose update_option( self::MAP_CACHE_KEY, $this->map, 'yes' ); } /** * Returns the whole map of settings. * * @return string[] */ public static function get_map(): array { return self::get_instance()->map; } /** * Get called from SettingsPage::register_tab , to fill the map with options from a specific tab. * * @param string $tab_id Die ID des Tabs (z.B. 'general'). * @param array $options Das Array der Optionsdefinitionen vom Tab. */ public function register_options_from_tab( string $tab_id, array $options ): void { $option_group_name = 'adpresso_settings_' . $tab_id; foreach ( array_keys( $options ) as $option_key ) { // Only a second tab claiming the key is a mistake, not the stored map or the same tab again. if ( isset( $this->registered[ $option_key ] ) && $this->registered[ $option_key ] !== $option_group_name ) { RuntimeDebugger::add_doing_it_wrong_entry( sprintf( 'Settings Warning: Option key "%s" is registered in multiple tabs. The value will be read from the last registered tab\'s option group ("%s").', esc_html( $option_key ), esc_html( $option_group_name ) ) ); } $this->registered[ $option_key ] = $option_group_name; $this->map[ $option_key ] = $option_group_name; } } /** * Public method to retrieve a setting value. * * @param string $key Unique key for the setting, e.g. 'ad_label_text'. * @param mixed $default The default value to return if the setting is not found. * * @return mixed Value of the setting if found, otherwise the default value. */ public function get( string $key, mixed $default = null ): mixed { $option_group_name = $this->map[ $key ] ?? null; if ( $option_group_name === null ) { return $default; } if ( ! isset( $this->cache[ $option_group_name ] ) ) { $this->cache[ $option_group_name ] = get_option( $option_group_name, [] ); } if ( isset( $this->cache[ $option_group_name ][ $key ] ) ) { return $this->cache[ $option_group_name ][ $key ]; } return $default; } /** * Provides a static wrapper function for easy access. */ public static function get_setting( string $key, mixed $default = null ): mixed { $value = self::get_instance()->get( $key, $default ); return apply_filters( 'adpresso_get_setting', $value, $key, $default ); } /** * Updates a single setting's value within its registered tab's option group * * @param string $key Unique key for the setting, e.g. 'debug_mode'. Must already * be registered to a tab (via register_options_from_tab()). * @param mixed $value The value to store. * * @return bool True if stored (or already matched the stored value), false if * the key isn't registered to any tab. */ public static function update_setting( string $key, mixed $value ): bool { $instance = self::get_instance(); $option_group_name = $instance->map[ $key ] ?? null; if ( $option_group_name === null ) { return false; } $options = get_option( $option_group_name, [] ); $old_value = $options[ $key ] ?? null; if ( maybe_serialize( $old_value ) === maybe_serialize( $value ) ) { $instance->cache[ $option_group_name ] = $options; return true; } $options[ $key ] = $value; $success = update_option( $option_group_name, $options ); if ( $success ) { $instance->cache[ $option_group_name ] = $options; } return $success; } /** * Retrieves a standalone WordPress option that is not part of a settings tab. * Uses the internal cache to ensure the option is fetched only once per request. * Prefixes the option name with the plugin's constant. * * @param string $option_name The name of the WordPress option (e.g., 'adblocker_count'). * @param mixed $default The default value to return if the option does not exist. * * @return mixed */ public static function get_direct_option( string $option_name, mixed $default = null ): mixed { $option_name = Constants::OPTION_PREFIX . $option_name; $instance = self::get_instance(); if ( isset( $instance->cache[ $option_name ] ) ) { return $instance->cache[ $option_name ]; } $value = get_option( $option_name, $default ); $instance->cache[ $option_name ] = $value; return $value; } /** * Updates a standalone WordPress option with built-in sanitization. * Prefixes the option name with the plugin's constant. * * @param string $option_name The name of the WordPress option. * @param mixed $raw_value The raw, untrusted value to be saved. * @param callable|string|null $sanitizer Optional. A specific sanitization function or callable. * Defaults to 'sanitize_text_field'. * Examples: 'absint', 'sanitize_email', [MyClass::class, 'my_sanitizer']. * * @return bool True if the value was updated, false otherwise. */ public static function update_direct_option( string $option_name, mixed $raw_value, callable|string $sanitizer = null ): bool { $instance = self::get_instance(); $option_name = Constants::OPTION_PREFIX . $option_name; // Default: save, recursive sanitization for arrays + mixed $default_sanitizer = static function( $value ) use ( &$default_sanitizer ) { if ( is_array( $value ) ) { $out = []; foreach ( $value as $k => $v ) { // Sanitize keys $safe_key = is_string( $k ) ? sanitize_key( $k ) : (string) $k; $out[ $safe_key ] = $default_sanitizer( $v ); } return $out; } // Keep Booleans/Ints if ( is_bool( $value ) ) { return $value ? 1 : 0; // WP-Settings often as 0/1 } if ( is_int( $value ) ) { return $value; } if ( is_float( $value ) ) { return $value; } if ( $value === null ) { return ''; } // All other strings return sanitize_text_field( (string) $value ); }; $sanitizer_to_use = $sanitizer ? ( is_callable( $sanitizer ) ? $sanitizer : 'sanitize_text_field' ) : $default_sanitizer; // Sanitize $sanitized_value = is_callable( $sanitizer_to_use ) ? call_user_func( $sanitizer_to_use, $raw_value ) : $default_sanitizer( $raw_value ); $old_value = get_option( $option_name, null ); if ( maybe_serialize( $old_value ) === maybe_serialize( $sanitized_value ) ) { $instance->cache[ $option_name ] = $sanitized_value; return true; } $success = update_option( $option_name, $sanitized_value ); if ( $success ) { $instance->cache[ $option_name ] = $sanitized_value; } return $success; } }