[ * 'enabled' => true, * 'visible_desktop' => true, * 'visible_mobile' => false * ], * 'content' => [ * 'message_text' => 'Welcome!', * 'cta_text' => 'Click here', * 'cta_url' => 'https://example.com' * ], * 'styles' => [ * 'background_color' => '#000000', * 'text_color' => '#ffffff', * 'height' => 50 * ], * 'general' => [ * 'priority' => 10, * 'version' => '1.0.0' * ] * ] * ``` * * @package ROITheme\Shared\Domain\ValueObjects */ final readonly class ComponentConfiguration { /** * Grupos de configuración válidos * * Basado en 10.00-flujo-de-trabajo-fuente-de-verdad.md líneas 304-348 * 12 grupos estándar + grupos adicionales en uso */ private const VALID_GROUPS = [ // 12 Grupos estándar del flujo de trabajo 'visibility', // Control de visibilidad 'content', // Textos y contenido 'typography', // Estilos de texto y semántica HTML 'colors', // Paleta de colores 'spacing', // Márgenes y padding 'visual_effects', // Efectos visuales decorativos 'behavior', // Comportamiento interactivo 'layout', // Estructura y posicionamiento 'links', // Enlaces y URLs 'icons', // Configuración de íconos 'media', // Multimedia 'forms', // Formularios // Grupos adicionales/legacy en uso 'styles', // Legacy: combina colors, spacing, visual_effects 'general', // Configuración general del componente 'menu', // Específico para navbar 'categories', // Categorización 'title', // Título del componente 'networks', // Específico para social-share // Grupos específicos para contact-form 'contact_info', // Info de contacto (teléfono, email, ubicación) 'form_labels', // Labels y placeholders del formulario 'integration', // Configuración de webhook 'messages', // Mensajes de éxito/error/validación // Grupos específicos para footer 'widget_1', // Widget 1 del footer (menú) 'widget_1b', // Widget 1B del footer (menú secundario columna 1) 'widget_2', // Widget 2 del footer (menú) 'widget_3', // Widget 3 del footer (menú) 'newsletter', // Sección newsletter del footer 'footer_bottom', // Pie del footer (copyright) // Sistema de visibilidad por página '_page_visibility', // Visibilidad por tipo de página (home, posts, pages, archives, search) ]; /** * @param array $configuration Configuración agrupada * @throws InvalidComponentException */ public function __construct(private array $configuration) { $this->validate(); } /** * Obtener toda la configuración * * @return array */ public function all(): array { return $this->configuration; } /** * Obtener configuración de un grupo específico * * @param string $group Nombre del grupo (visibility, content, styles, general) * @return array */ public function getGroup(string $group): array { return $this->configuration[$group] ?? []; } /** * Obtener valor de una clave específica dentro de un grupo * * @param string $group Nombre del grupo * @param string $key Clave de configuración * @param mixed $default Valor por defecto si no existe * @return mixed */ public function get(string $group, string $key, mixed $default = null): mixed { return $this->configuration[$group][$key] ?? $default; } /** * Verificar si existe una clave en un grupo * * @param string $group * @param string $key * @return bool */ public function has(string $group, string $key): bool { return isset($this->configuration[$group][$key]); } /** * Verificar si el componente tiene CTA URL * * @return bool */ public function hasCTAURL(): bool { return $this->has('content', 'cta_url') && !empty($this->get('content', 'cta_url')); } /** * Verificar si el componente tiene CTA text * * @return bool */ public function hasCTAText(): bool { return $this->has('content', 'cta_text') && !empty($this->get('content', 'cta_text')); } /** * Verificar si el componente está habilitado * * @return bool */ public function isEnabled(): bool { return (bool) $this->get('visibility', 'enabled', false); } /** * Crear nueva configuración con un valor actualizado * (Inmutabilidad: retorna nueva instancia) * * @param string $group * @param string $key * @param mixed $value * @return self */ public function withValue(string $group, string $key, mixed $value): self { $newConfiguration = $this->configuration; if (!isset($newConfiguration[$group])) { $newConfiguration[$group] = []; } $newConfiguration[$group][$key] = $value; return new self($newConfiguration); } /** * Crear nueva configuración con un grupo actualizado * (Inmutabilidad: retorna nueva instancia) * * @param string $group * @param array $values * @return self */ public function withGroup(string $group, array $values): self { $newConfiguration = $this->configuration; $newConfiguration[$group] = $values; return new self($newConfiguration); } /** * Validar reglas de negocio de la configuración * * @throws InvalidComponentException * @return void */ private function validate(): void { // Regla 1: Debe ser un array if (!is_array($this->configuration)) { throw new InvalidComponentException('Configuration must be an array'); } // Regla 2: Los grupos deben ser válidos foreach (array_keys($this->configuration) as $group) { if (!in_array($group, self::VALID_GROUPS, true)) { throw new InvalidComponentException( sprintf( 'Invalid configuration group "%s". Valid groups are: %s', $group, implode(', ', self::VALID_GROUPS) ) ); } } // Regla 3: Si existe cta_url debe existir cta_text (y viceversa) if ($this->hasCTAURL() && !$this->hasCTAText()) { throw new InvalidComponentException('CTA URL requires CTA text'); } if ($this->hasCTAText() && !$this->hasCTAURL()) { throw new InvalidComponentException('CTA text requires CTA URL'); } // Regla 4: Los colores deben ser hexadecimales válidos $this->validateColors(); } /** * Validar que los colores estén en formato hexadecimal * * @throws InvalidComponentException * @return void */ private function validateColors(): void { $styles = $this->getGroup('styles'); foreach ($styles as $key => $value) { if (str_ends_with($key, '_color') && !empty($value)) { if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $value)) { throw new InvalidComponentException( sprintf( 'Color "%s" must be in hexadecimal format (e.g., #000000). Got: %s', $key, $value ) ); } } } } /** * Crear desde array flat (legacy) * * Convierte de: * ```php * [ * 'enabled' => true, * 'message_text' => 'Hello', * 'background_color' => '#000' * ] * ``` * * A estructura agrupada. * * @param array $flatConfig * @return self */ public static function fromFlat(array $flatConfig): self { $grouped = [ 'visibility' => [], 'content' => [], 'styles' => [], 'general' => [] ]; foreach ($flatConfig as $key => $value) { $group = self::inferGroup($key); $grouped[$group][$key] = $value; } return new self($grouped); } /** * Crear desde array agrupado * * Espera estructura ya agrupada: * ```php * [ * 'visibility' => ['enabled' => true, ...], * 'content' => ['message_text' => 'Hello', ...], * 'styles' => ['background_color' => '#000', ...], * 'general' => ['priority' => 10, ...] * ] * ``` * * @param array $groupedConfig Configuración ya agrupada * @return self */ public static function fromArray(array $groupedConfig): self { // Si ya está agrupado, usarlo directamente return new self($groupedConfig); } /** * Inferir grupo desde clave (heurística) * * @param string $key * @return string */ private static function inferGroup(string $key): string { // Visibility if (in_array($key, ['enabled', 'visible_desktop', 'visible_mobile', 'visible_tablet'], true)) { return 'visibility'; } // Content if (str_starts_with($key, 'message_') || str_starts_with($key, 'cta_') || str_starts_with($key, 'title_')) { return 'content'; } // Styles if (str_ends_with($key, '_color') || str_ends_with($key, '_height') || str_ends_with($key, '_width') || str_ends_with($key, '_size') || str_ends_with($key, '_font')) { return 'styles'; } // Fallback return 'general'; } /** * Crear configuración vacía * * @return self */ public static function empty(): self { return new self([ 'visibility' => [], 'content' => [], 'styles' => [], 'general' => [] ]); } }