Cambios incluidos: - Actualización de copy/textos en 7 schemas JSON - Mejoras en AdminAjaxHandler con mapeos adicionales - Refactorización de FormBuilders y Renderers - Correcciones en dashboard admin JS - Nuevo ContactFormRenderer funcional NOTA: Este commit sirve como respaldo antes de corregir inconsistencias de case en namespaces (API→Api, WordPress→Wordpress) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
389 lines
11 KiB
PHP
389 lines
11 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace ROITheme\Shared\Domain\ValueObjects;
|
|
|
|
use ROITheme\Shared\Domain\Exceptions\InvalidComponentException;
|
|
|
|
/**
|
|
* ComponentConfiguration - Value Object inmutable para configuración de componente
|
|
*
|
|
* RESPONSABILIDAD: Representar la configuración completa de un componente agrupada por categorías
|
|
*
|
|
* REGLAS DE NEGOCIO:
|
|
* - La configuración se organiza en grupos: visibility, content, styles, general
|
|
* - Cada grupo contiene pares clave-valor
|
|
* - Los valores pueden ser: string, boolean, integer, float, array
|
|
* - Si existe cta_url debe existir cta_text (y viceversa)
|
|
* - Los colores deben estar en formato hexadecimal válido
|
|
*
|
|
* INVARIANTES:
|
|
* - Una vez creada, la configuración no puede cambiar (inmutable)
|
|
* - La estructura de grupos siempre está presente (aunque vacía)
|
|
* - Los tipos de datos se preservan correctamente
|
|
*
|
|
* ESTRUCTURA:
|
|
* ```php
|
|
* [
|
|
* 'visibility' => [
|
|
* '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)
|
|
];
|
|
|
|
/**
|
|
* @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' => []
|
|
]);
|
|
}
|
|
}
|