fix(structure): Correct case-sensitivity for Linux compatibility
Rename folders to match PHP PSR-4 autoloading conventions: - schemas → Schemas - shared → Shared - Wordpress → WordPress (in all locations) Fixes deployment issues on Linux servers where filesystem is case-sensitive. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
388
Shared/Domain/ValueObjects/ComponentConfiguration.php
Normal file
388
Shared/Domain/ValueObjects/ComponentConfiguration.php
Normal file
@@ -0,0 +1,388 @@
|
||||
<?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' => []
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user