` — that is owned by the page. * * @since 1.1.0 */ class SettingsRenderer { /** * Render a single field. * * @since 1.1.0 * * @param array $field Normalised field definition. * @param mixed $current_value Current saved value (or default). * * @return void */ public function render_field( array $field, $current_value ): void { $option_name = $this->resolve_option_name( $field ); // Whether something in code — a wp-config.php constant, a filter — owns // this value rather than the stored option. Asked once and passed down, // so the control, the value it shows and the note under it cannot // disagree about what is in force. $override = Value::override( $option_name ); // A field may compute what it shows from something other than the stored // option. Applied before anything reads $current_value, so every branch // below sees the displayed value, not the stored one. An explicit // `display_value` wins over the automatic path: it is the escape hatch // for a value the generic resolver cannot express. if ( isset( $field['display_value'] ) && is_callable( $field['display_value'] ) ) { $current_value = call_user_func( $field['display_value'], $current_value ); } elseif ( $override !== null ) { $current_value = $override['value']; } $current_value = $this->canonical_choice( $field, $current_value ); $type = isset( $field['type'] ) && is_string( $field['type'] ) ? $field['type'] : 'text'; $label = isset( $field['label'] ) && is_string( $field['label'] ) ? $field['label'] : ''; $description = isset( $field['description'] ) && is_string( $field['description'] ) ? $field['description'] : ''; $badge = isset( $field['badge'] ) && is_string( $field['badge'] ) ? $field['badge'] : ''; $input_id = 'albert-field-' . str_replace( '/', '_', $option_name ); if ( $type === 'custom' || $type === 'radio-cards' ) { // Custom and radio-cards fields keep a single-column layout: a custom // render callback (licenses table, copy-to-clipboard widgets) or a stack // of full-width cards neither fit the two-column grid below. // // A group of radios is wrapped in a
and named by its // , which is how HTML names a group and what a screen reader // announces before each option. A '; } echo ''; } /** * Render a checkbox with a paired hidden input so unchecked submits as "0". * * @since 1.1.0 * * @param array $field Normalised field definition. * @param mixed $current_value Current value. * @param string $option_name wp_options key. * @param string $input_id HTML id attribute. * * @return void */ private function render_checkbox( array $field, $current_value, string $option_name, string $input_id ): void { $attributes = isset( $field['attributes'] ) && is_array( $field['attributes'] ) ? $field['attributes'] : []; $checked = (bool) $current_value; // The hidden input is intentionally rendered FIRST so PHP only sees "0" when the // checkbox is unchecked — when checked, the visible input overrides it with "1". printf( '', esc_attr( $option_name ) ); printf( ''; } /** * Invoke a custom render callback safely. * * Catches any throwable so a buggy addon callback can't take down the * whole settings page. * * @since 1.1.0 * * @param array $field Normalised field definition. * @param mixed $current_value Current value. * * @return void */ private function render_custom( array $field, $current_value ): void { $callback = isset( $field['render_callback'] ) && is_callable( $field['render_callback'] ) ? $field['render_callback'] : null; if ( $callback === null ) { return; } try { call_user_func( $callback, $field, $current_value ); } catch ( Throwable $e ) { $field_id = isset( $field['id'] ) && is_string( $field['id'] ) ? $field['id'] : '(unknown)'; error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- intentional error trail for addon authors. sprintf( '[Albert Settings] render_callback for field "%s" threw: %s', $field_id, $e->getMessage() ) ); echo '

' . esc_html__( 'A settings field could not be rendered. Check the PHP error log for details.', 'albert-ai-butler' ) . '

'; } } /** * The generated hint for a setting owned by code. * * Names the constant or filter, because "this is set elsewhere" without * saying where leaves somebody grepping. Both names are developer-authored * identifiers, never user input, and reach the page through the same * ``-only allowlist as any other hint. * * @since 1.4.0 * * @param array{source: string, value: mixed, name: string} $override Active override. * * @return array{text: string, tone: string} */ private function override_hint( array $override ): array { $name = isset( $override['name'] ) && is_string( $override['name'] ) ? $override['name'] : ''; if ( ( $override['source'] ?? '' ) === 'constant' ) { $text = sprintf( /* translators: 1: opening , 2: closing wrapping a PHP constant name */ __( 'Set by the %1$s%3$s%2$s constant in this site\'s configuration, so it can\'t be changed here.', 'albert-ai-butler' ), '', '', $name ); } else { $text = sprintf( /* translators: 1: opening , 2: closing wrapping a filter name */ __( 'Set in code by the %1$s%3$s%2$s filter, so it can\'t be changed here.', 'albert-ai-butler' ), '', '', $name ); } return [ 'text' => $text, 'tone' => 'info', ]; } }