apiKey = trim( $apiKey ); $this->model = self::normalizeModel( $model ); $this->enabled = $enabled; } /** * Baseline settings for fresh installs. */ public static function defaults(): self { return new self( '', self::DEFAULT_MODEL, false ); } /** * API key, returned verbatim for provider/API boundaries only. */ public function apiKey(): string { return $this->apiKey; } /** * Configured model name. */ public function model(): string { return $this->model; } /** * Whether OpenAI-backed extraction is enabled. */ public function enabled(): bool { return $this->enabled; } /** * Whether the key is non-empty and OpenAI-backed extraction is enabled. */ public function isConfigured(): bool { return $this->enabled && $this->apiKey !== ''; } /** * Masked API key for UI display. */ public function maskedApiKey(): string { return SettingsCredentialMask::mask( $this->apiKey ); } /** * Copy with a new API key. * * @param string $apiKey New API key. */ public function withApiKey( string $apiKey ): self { return new self( $apiKey, $this->model, $this->enabled ); } /** * Copy with a new model. * * @param string $model New model name. * * @throws InvalidArgumentException When `$model` is too long. */ public function withModel( string $model ): self { return new self( $this->apiKey, $model, $this->enabled ); } /** * Copy with a new enabled flag. * * @param bool $enabled Whether OpenAI-backed extraction is enabled. */ public function withEnabled( bool $enabled ): self { return new self( $this->apiKey, $this->model, $enabled ); } /** * Structural equality. * * @param self $other Other settings snapshot. */ public function equals( self $other ): bool { return $this->apiKey === $other->apiKey && $this->model === $other->model && $this->enabled === $other->enabled; } /** * Serialize for persistence. * * @return array */ public function toArray(): array { return array( 'api_key' => $this->apiKey, 'model' => $this->model, 'enabled' => $this->enabled, ); } /** * Construct from persisted payload. * * @param array $data Persisted payload. * * @throws InvalidArgumentException When the payload is malformed. */ public static function fromArray( array $data ): self { if ( ! array_key_exists( 'api_key', $data ) || ! is_string( $data['api_key'] ) ) { $message = 'OpenAiSettings payload must carry a string `api_key`.'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } if ( ! array_key_exists( 'model', $data ) || ! is_string( $data['model'] ) ) { $message = 'OpenAiSettings payload must carry a string `model`.'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } $enabled = true; if ( array_key_exists( 'enabled', $data ) ) { $enabled = (bool) $data['enabled']; } return new self( $data['api_key'], $data['model'], $enabled ); } /** * Normalize the model name. * * @param string $model Raw model name. * * @throws InvalidArgumentException When `$model` is too long. */ private static function normalizeModel( string $model ): string { $trimmed = trim( $model ); if ( $trimmed === '' ) { return self::DEFAULT_MODEL; } if ( strlen( $trimmed ) > 128 ) { $message = 'OpenAiSettings model must be 128 characters or fewer.'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } return $trimmed; } }