id = static::get_id(); if ( empty( $this->id ) ) { throw new InvalidArgumentException( esc_html__( 'Invalid Feature id returned by ::get_id().', 'ai' ) ); } $metadata = $this->load_metadata(); if ( empty( $metadata['label'] ) ) { throw new InvalidArgumentException( esc_html__( 'Feature label is required in load_metadata().', 'ai' ) ); } if ( empty( $metadata['description'] ) ) { throw new InvalidArgumentException( esc_html__( 'Feature description is required in load_metadata().', 'ai' ) ); } if ( empty( $metadata['category'] ) ) { $metadata['category'] = Feature_Category::OTHER; } $this->label = $metadata['label']; $this->description = $metadata['description']; $this->category = $metadata['category']; $this->stability = $metadata['stability'] ?? 'experimental'; $this->image = $metadata['image'] ?? ''; $this->capability = $metadata['capability'] ?? 'text_generation'; } /** * Loads feature metadata. * * Must return an array with keys: label, description. * Optionally includes: category, stability. * * @since 0.6.0 * * @return array{ * label: string, * description: string, * category?: string, * stability?: 'deprecated'|'experimental'|'stable', * image?: string, * } Feature metadata. */ abstract protected function load_metadata(): array; /** * {@inheritDoc} */ public function get_label(): string { return $this->label; } /** * {@inheritDoc} */ public function get_description(): string { return $this->description; } /** * {@inheritDoc} */ public function get_category(): string { return $this->category; } /** * {@inheritDoc} */ final public function is_globally_enabled(): bool { return (bool) get_option( Settings_Registration::GLOBAL_OPTION, false ); } /** * {@inheritDoc} */ final public function is_individually_enabled(): bool { $feature_enabled = (bool) get_option( "wpai_feature_{$this->id}_enabled", false ); // @todo remove in v1.0 $is_enabled = (bool) apply_filters_deprecated( "ai_experiments_experiment_{$this->id}_enabled", array( $feature_enabled ), '0.6.0', "wpai_feature_{$this->id}_enabled", esc_html__( 'This will be removed in v1.0', 'ai' ) ); /** * Filters the enabled status for a specific feature. * * The dynamic portion of the hook name, `$this->id`, refers to the feature ID. * * @since 0.6.0 * * @param bool $feature_enabled Whether the feature is enabled. */ return (bool) apply_filters( "wpai_feature_{$this->id}_enabled", $is_enabled ); } /** * {@inheritDoc} * * Features require both the global toggle and individual * feature toggle to be enabled. Results are cached per * instance to avoid redundant option lookups and filter calls. */ final public function is_enabled(): bool { // Return cached result if available. if ( null !== $this->enabled_cache ) { return $this->enabled_cache; } // Cache the result. $this->enabled_cache = $this->is_globally_enabled() && $this->is_individually_enabled(); return $this->enabled_cache; } /** * {@inheritDoc} */ final public function get_stability(): string { return $this->stability; } /** * {@inheritDoc} */ public function get_image(): string { return $this->image; } /** * {@inheritDoc} */ public function get_capability(): string { return $this->capability; } /** * Registers feature-specific settings. * * Override this method in child classes to register custom settings options * using WordPress Settings API (register_setting). * * @since 0.6.0 * * @return void */ public function register_settings(): void { // Default implementation does nothing. // Child classes can override to register custom settings. } /** * Gets the field definitions for feature-specific settings. * * Override this method in child classes to declare custom settings fields * that will be rendered as a DataForm on the settings page. Each field * should use the short option name (e.g. 'strategy'), not the full * namespaced option name. * * @since 0.7.0 * * @return array, * isValid?: array{min?: int, max?: int}, * }> Array of field definitions matching the DataForm Field shape. */ public function get_settings_fields(): array { return array(); } /** * Gets field definitions with fully resolved option names. * * Transforms the short field IDs from get_settings_fields() into * full WordPress option names suitable for the REST API and frontend. * * @since 0.7.0 * * @return array, * isValid?: array{min?: int, max?: int}, * }> Array of field definitions with full option names. */ public function get_settings_fields_metadata(): array { $fields = $this->get_settings_fields(); foreach ( $fields as &$field ) { $field['id'] = static::get_field_option_name( $field['id'] ); } unset( $field ); return $fields; } /** * Gets the option name for a custom feature setting field. * * Generates a properly namespaced option name for feature-specific settings. * Use this when registering and storing custom settings fields to ensure * consistent naming across the plugin. * * @since 0.6.0 * * @param string $option_name The base option name (e.g., 'api_key', 'temperature'). * @return string The fully namespaced option name. */ final public static function get_field_option_name( string $option_name ): string { return 'wpai_feature_' . static::get_id() . '_field_' . $option_name; } /** * {@inheritDoc} * * Must be implemented by child classes to set up hooks and functionality. */ abstract public function register(): void; }