defaultTemplate = self::normalizeDefaultTemplate( $defaultTemplate ); $this->themeOverrideEnabled = $themeOverrideEnabled; $this->showProviderBadgeByDefault = $showProviderBadgeByDefault; $this->showDisclaimerByDefault = $showDisclaimerByDefault; $this->lazyLoadImages = $lazyLoadImages; $this->placeholderImagePolicy = self::normalizePlaceholderImagePolicy( $placeholderImagePolicy ); $this->defaultDesignPreset = self::normalizeClosedSet( 'defaultDesignPreset', $defaultDesignPreset, self::DESIGN_PRESETS ); $this->defaultButtonLayout = self::normalizeClosedSet( 'defaultButtonLayout', $defaultButtonLayout, self::BUTTON_LAYOUTS ); $this->defaultButtonShape = self::normalizeClosedSet( 'defaultButtonShape', $defaultButtonShape, self::BUTTON_SHAPES ); $this->defaultButtonTone = self::normalizeClosedSet( 'defaultButtonTone', $defaultButtonTone, self::BUTTON_TONES ); $this->defaultColorScheme = self::normalizeClosedSet( 'defaultColorScheme', $defaultColorScheme, self::COLOR_SCHEMES ); } /** * Construct a rendering-defaults snapshot with the canonical * §13.5 baseline values. */ public static function defaults(): self { return new self( 'single-default-card', true, false, true, true, 'transparent' ); } /** * §6.4 template key used as the embed-config default. */ public function defaultTemplate(): string { return $this->defaultTemplate; } /** * Whether theme template overrides are searched. */ public function themeOverrideEnabled(): bool { return $this->themeOverrideEnabled; } /** * Whether the §8.2 provider-badge slot is shown by default. */ public function showProviderBadgeByDefault(): bool { return $this->showProviderBadgeByDefault; } /** * Whether the §8.2 disclaimer slot is shown by default. */ public function showDisclaimerByDefault(): bool { return $this->showDisclaimerByDefault; } /** * Whether image lazy-loading is emitted. */ public function lazyLoadImages(): bool { return $this->lazyLoadImages; } /** * Placeholder image policy. */ public function placeholderImagePolicy(): string { return $this->placeholderImagePolicy; } /** * Default card design preset. */ public function defaultDesignPreset(): string { return $this->defaultDesignPreset; } /** * Default button layout. */ public function defaultButtonLayout(): string { return $this->defaultButtonLayout; } /** * Default button shape. */ public function defaultButtonShape(): string { return $this->defaultButtonShape; } /** * Default button tone. */ public function defaultButtonTone(): string { return $this->defaultButtonTone; } /** * Default card color scheme. */ public function defaultColorScheme(): string { return $this->defaultColorScheme; } /** * Produce a copy with `defaultTemplate` replaced. * * @param string $defaultTemplate New template key. * * @throws InvalidArgumentException When `$defaultTemplate` * is empty after trimming. */ public function withDefaultTemplate( string $defaultTemplate ): self { $clone = clone $this; $clone->defaultTemplate = self::normalizeDefaultTemplate( $defaultTemplate ); return $clone; } /** * Produce a copy with `themeOverrideEnabled` replaced. * * @param bool $themeOverrideEnabled New flag. */ public function withThemeOverrideEnabled( bool $themeOverrideEnabled ): self { $clone = clone $this; $clone->themeOverrideEnabled = $themeOverrideEnabled; return $clone; } /** * Produce a copy with `showProviderBadgeByDefault` replaced. * * @param bool $showProviderBadgeByDefault New flag. */ public function withShowProviderBadgeByDefault( bool $showProviderBadgeByDefault ): self { $clone = clone $this; $clone->showProviderBadgeByDefault = $showProviderBadgeByDefault; return $clone; } /** * Produce a copy with `showDisclaimerByDefault` replaced. * * @param bool $showDisclaimerByDefault New flag. */ public function withShowDisclaimerByDefault( bool $showDisclaimerByDefault ): self { $clone = clone $this; $clone->showDisclaimerByDefault = $showDisclaimerByDefault; return $clone; } /** * Produce a copy with `lazyLoadImages` replaced. * * @param bool $lazyLoadImages New flag. */ public function withLazyLoadImages( bool $lazyLoadImages ): self { $clone = clone $this; $clone->lazyLoadImages = $lazyLoadImages; return $clone; } /** * Produce a copy with `placeholderImagePolicy` replaced. * * @param string $placeholderImagePolicy New policy value. * * @throws InvalidArgumentException When `$placeholderImagePolicy` * is off the closed set. */ public function withPlaceholderImagePolicy( string $placeholderImagePolicy ): self { $clone = clone $this; $clone->placeholderImagePolicy = self::normalizePlaceholderImagePolicy( $placeholderImagePolicy ); return $clone; } /** * Produce a copy with `defaultColorScheme` replaced. * * @param string $defaultColorScheme New color-scheme value. * * @throws InvalidArgumentException When `$defaultColorScheme` * is off the closed set. */ public function withDefaultColorScheme( string $defaultColorScheme ): self { $clone = clone $this; $clone->defaultColorScheme = self::normalizeClosedSet( 'defaultColorScheme', $defaultColorScheme, self::COLOR_SCHEMES ); return $clone; } /** * Report whether this snapshot equals the supplied one. * * Equality is structural: every field must match under * strict comparison. * * @param self $other Other snapshot to compare against. */ public function equals( self $other ): bool { return $this->defaultTemplate === $other->defaultTemplate && $this->themeOverrideEnabled === $other->themeOverrideEnabled && $this->showProviderBadgeByDefault === $other->showProviderBadgeByDefault && $this->showDisclaimerByDefault === $other->showDisclaimerByDefault && $this->lazyLoadImages === $other->lazyLoadImages && $this->placeholderImagePolicy === $other->placeholderImagePolicy && $this->defaultDesignPreset === $other->defaultDesignPreset && $this->defaultButtonLayout === $other->defaultButtonLayout && $this->defaultButtonShape === $other->defaultButtonShape && $this->defaultButtonTone === $other->defaultButtonTone && $this->defaultColorScheme === $other->defaultColorScheme; } /** * Serialize this snapshot for OptionStore persistence. * * @return array */ public function toArray(): array { return array( 'default_template' => $this->defaultTemplate, 'theme_override_enabled' => $this->themeOverrideEnabled, 'show_provider_badge_by_default' => $this->showProviderBadgeByDefault, 'show_disclaimer_by_default' => $this->showDisclaimerByDefault, 'lazy_load_images' => $this->lazyLoadImages, 'placeholder_image_policy' => $this->placeholderImagePolicy, 'default_design_preset' => $this->defaultDesignPreset, 'default_button_layout' => $this->defaultButtonLayout, 'default_button_shape' => $this->defaultButtonShape, 'default_button_tone' => $this->defaultButtonTone, 'default_color_scheme' => $this->defaultColorScheme, ); } /** * Construct a snapshot from a {@see self::toArray()} payload. * * @param array $data Persisted payload. * * @throws InvalidArgumentException When `$data` is malformed. */ public static function fromArray( array $data ): self { $defaultTemplate = self::requireString( $data, 'default_template' ); $themeOverrideEnabled = self::requireBool( $data, 'theme_override_enabled' ); $showProviderBadgeByDefault = self::requireBool( $data, 'show_provider_badge_by_default' ); $showDisclaimerByDefault = self::requireBool( $data, 'show_disclaimer_by_default' ); $lazyLoadImages = self::requireBool( $data, 'lazy_load_images' ); $placeholderImagePolicy = self::requireString( $data, 'placeholder_image_policy' ); $defaultDesignPreset = self::optionalString( $data, 'default_design_preset', 'default' ); $defaultButtonLayout = self::optionalString( $data, 'default_button_layout', 'wrap' ); $defaultButtonShape = self::optionalString( $data, 'default_button_shape', 'rounded' ); $defaultButtonTone = self::optionalString( $data, 'default_button_tone', 'brand' ); $defaultColorScheme = self::optionalString( $data, 'default_color_scheme', 'light' ); return new self( $defaultTemplate, $themeOverrideEnabled, $showProviderBadgeByDefault, $showDisclaimerByDefault, $lazyLoadImages, $placeholderImagePolicy, $defaultDesignPreset, $defaultButtonLayout, $defaultButtonShape, $defaultButtonTone, $defaultColorScheme ); } /** * Validate and normalize the `defaultTemplate` field at * construction. * * @param string $defaultTemplate Raw template key. * * @throws InvalidArgumentException When empty after trimming. */ private static function normalizeDefaultTemplate( string $defaultTemplate ): string { $trimmed = trim( $defaultTemplate ); if ( $trimmed === '' ) { $message = 'RenderingDefaults defaultTemplate must be a non-empty string.'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } return $trimmed; } /** * Validate and normalize the `placeholderImagePolicy` field * at construction. * * @param string $placeholderImagePolicy Raw policy value. * * @throws InvalidArgumentException When off the closed set. */ private static function normalizePlaceholderImagePolicy( string $placeholderImagePolicy ): string { if ( ! in_array( $placeholderImagePolicy, self::PLACEHOLDER_POLICIES, true ) ) { $message = sprintf( 'RenderingDefaults placeholderImagePolicy must be one of %s.', implode( ', ', self::PLACEHOLDER_POLICIES ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } return $placeholderImagePolicy; } /** * Read a required string field from a {@see self::fromArray()} * payload. * * @param array $data Persisted payload. * @param string $key Field name. * * @throws InvalidArgumentException When the field is missing * or not a string. */ private static function requireString( array $data, string $key ): string { if ( ! array_key_exists( $key, $data ) || ! is_string( $data[ $key ] ) ) { $message = sprintf( 'RenderingDefaults payload must carry a string `%s`.', $key ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } return $data[ $key ]; } /** * Read an optional string field from a persisted payload. * * @param array $data Persisted payload. * @param string $key Field name. * @param string $fallback Fallback value. */ private static function optionalString( array $data, string $key, string $fallback ): string { $value = $data[ $key ] ?? null; return is_string( $value ) ? $value : $fallback; } /** * Validate a closed-set string field. * * @param string $field Field name. * @param string $value Candidate value. * @param list $allowed Allowed values. */ private static function normalizeClosedSet( string $field, string $value, array $allowed ): string { if ( ! in_array( $value, $allowed, true ) ) { $message = sprintf( 'RenderingDefaults %s must be one of %s.', $field, implode( ', ', $allowed ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } return $value; } /** * Read a required bool field from a {@see self::fromArray()} * payload. * * @param array $data Persisted payload. * @param string $key Field name. * * @throws InvalidArgumentException When the field is missing * or not a bool. */ private static function requireBool( array $data, string $key ): bool { if ( ! array_key_exists( $key, $data ) || ! is_bool( $data[ $key ] ) ) { $message = sprintf( 'RenderingDefaults payload must carry a bool `%s`.', $key ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } return $data[ $key ]; } }