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:
FrankZamora
2025-11-26 22:53:34 -06:00
parent a2548ab5c2
commit 90863cd8f5
92 changed files with 0 additions and 0 deletions

View File

View 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' => []
]);
}
}

View File

@@ -0,0 +1,272 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Domain\ValueObjects;
use ROITheme\Shared\Domain\Exceptions\InvalidComponentException;
/**
* ComponentContent - Value Object inmutable para contenido de componente
*
* RESPONSABILIDAD: Representar y validar el contenido textual y de CTA de un componente
*
* REGLAS DE NEGOCIO:
* - El mensaje de texto puede estar vacío (componentes sin texto)
* - Si existe CTA URL debe existir CTA text (y viceversa)
* - CTA URL debe ser una URL válida (formato http/https)
* - CTA text no puede estar vacío si existe CTA URL
* - Title es opcional
*
* CASOS DE USO:
* - Componente solo con mensaje de texto
* - Componente con mensaje y CTA
* - Componente con title, mensaje y CTA
* - Componente sin contenido textual (solo estilos/visibilidad)
*
* USO:
* ```php
* // Componente con mensaje y CTA
* $content = new ComponentContent(
* messageText: 'Welcome to our site!',
* ctaText: 'Learn More',
* ctaUrl: 'https://example.com/about'
* );
*
* // Componente solo con mensaje
* $content = ComponentContent::messageOnly('Hello World!');
*
* // Componente vacío
* $content = ComponentContent::empty();
* ```
*
* @package ROITheme\Shared\Domain\ValueObjects
*/
final readonly class ComponentContent
{
/**
* Constructor
*
* @param string $messageText Texto del mensaje principal
* @param string|null $ctaText Texto del Call-to-Action
* @param string|null $ctaUrl URL del Call-to-Action
* @param string|null $title Título del componente
* @throws InvalidComponentException
*/
public function __construct(
private string $messageText = '',
private ?string $ctaText = null,
private ?string $ctaUrl = null,
private ?string $title = null
) {
$this->validate();
}
/**
* Obtener mensaje de texto
*
* @return string
*/
public function messageText(): string
{
return $this->messageText;
}
/**
* Obtener texto del CTA
*
* @return string|null
*/
public function ctaText(): ?string
{
return $this->ctaText;
}
/**
* Obtener URL del CTA
*
* @return string|null
*/
public function ctaUrl(): ?string
{
return $this->ctaUrl;
}
/**
* Obtener título
*
* @return string|null
*/
public function title(): ?string
{
return $this->title;
}
/**
* Verificar si tiene mensaje de texto
*
* @return bool
*/
public function hasMessage(): bool
{
return !empty($this->messageText);
}
/**
* Verificar si tiene CTA
*
* @return bool
*/
public function hasCTA(): bool
{
return !empty($this->ctaText) && !empty($this->ctaUrl);
}
/**
* Verificar si tiene título
*
* @return bool
*/
public function hasTitle(): bool
{
return !empty($this->title);
}
/**
* Verificar si está completamente vacío
*
* @return bool
*/
public function isEmpty(): bool
{
return empty($this->messageText) &&
empty($this->ctaText) &&
empty($this->ctaUrl) &&
empty($this->title);
}
/**
* Convertir a array
*
* @return array
*/
public function toArray(): array
{
return array_filter([
'message_text' => $this->messageText,
'cta_text' => $this->ctaText,
'cta_url' => $this->ctaUrl,
'title' => $this->title
], fn($value) => $value !== null && $value !== '');
}
/**
* Validar reglas de negocio
*
* @throws InvalidComponentException
* @return void
*/
private function validate(): void
{
// Regla 1: Si existe CTA URL debe existir CTA text
if (!empty($this->ctaUrl) && empty($this->ctaText)) {
throw new InvalidComponentException('CTA URL requires CTA text');
}
// Regla 2: Si existe CTA text debe existir CTA URL
if (!empty($this->ctaText) && empty($this->ctaUrl)) {
throw new InvalidComponentException('CTA text requires CTA URL');
}
// Regla 3: CTA URL debe ser URL válida
if (!empty($this->ctaUrl) && !$this->isValidUrl($this->ctaUrl)) {
throw new InvalidComponentException(
sprintf('Invalid CTA URL format: %s', $this->ctaUrl)
);
}
}
/**
* Validar si una string es URL válida
*
* @param string $url
* @return bool
*/
private function isValidUrl(string $url): bool
{
// Debe comenzar con http:// o https://
if (!preg_match('/^https?:\/\//i', $url)) {
return false;
}
// Usar filter_var para validación completa
return filter_var($url, FILTER_VALIDATE_URL) !== false;
}
/**
* Factory: Contenido vacío
*
* @return self
*/
public static function empty(): self
{
return new self();
}
/**
* Factory: Solo mensaje
*
* @param string $message
* @return self
*/
public static function messageOnly(string $message): self
{
return new self(messageText: $message);
}
/**
* Factory: Mensaje con CTA
*
* @param string $message
* @param string $ctaText
* @param string $ctaUrl
* @return self
*/
public static function withCTA(string $message, string $ctaText, string $ctaUrl): self
{
return new self(
messageText: $message,
ctaText: $ctaText,
ctaUrl: $ctaUrl
);
}
/**
* Crear desde array
*
* @param array $data
* @return self
*/
public static function fromArray(array $data): self
{
return new self(
messageText: $data['message_text'] ?? '',
ctaText: $data['cta_text'] ?? null,
ctaUrl: $data['cta_url'] ?? null,
title: $data['title'] ?? null
);
}
/**
* Comparar con otro ComponentContent
*
* @param ComponentContent $other
* @return bool
*/
public function equals(ComponentContent $other): bool
{
return $this->messageText === $other->messageText &&
$this->ctaText === $other->ctaText &&
$this->ctaUrl === $other->ctaUrl &&
$this->title === $other->title;
}
}

View File

@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Domain\ValueObjects;
use ROITheme\Shared\Domain\Exceptions\InvalidComponentException;
/**
* ComponentName - Value Object inmutable para nombre de componente
*
* RESPONSABILIDAD: Representar y validar el nombre de un componente del tema
*
* REGLAS DE NEGOCIO:
* - El nombre no puede estar vacío
* - El nombre debe tener entre 3 y 50 caracteres
* - El nombre solo puede contener letras minúsculas, números, guiones bajos y guiones
* - El nombre debe comenzar con una letra
*
* INVARIANTES:
* - Una vez creado, el nombre no puede cambiar (inmutable)
* - El nombre siempre está en formato normalizado (lowercase, sin espacios)
*
* USO:
* ```php
* $name = new ComponentName('top_bar');
* echo $name->value(); // "top_bar"
* echo $name->toString(); // "top_bar"
* ```
*
* @package ROITheme\Shared\Domain\ValueObjects
*/
final readonly class ComponentName
{
/**
* Longitud mínima del nombre
*/
private const MIN_LENGTH = 3;
/**
* Longitud máxima del nombre
*/
private const MAX_LENGTH = 50;
/**
* Patrón regex para validar formato del nombre
* - Debe comenzar con letra minúscula
* - Puede contener letras minúsculas, números, guiones bajos y guiones
*/
private const PATTERN = '/^[a-z][a-z0-9_-]*$/';
/**
* @param string $value Nombre del componente
* @throws InvalidComponentException Si el nombre no cumple las reglas de negocio
*/
public function __construct(private string $value)
{
$this->validate();
}
/**
* Obtener el valor del nombre
*
* @return string
*/
public function value(): string
{
return $this->value;
}
/**
* Convertir a string
*
* @return string
*/
public function toString(): string
{
return $this->value;
}
/**
* Comparar con otro ComponentName
*
* @param ComponentName $other
* @return bool
*/
public function equals(ComponentName $other): bool
{
return $this->value === $other->value;
}
/**
* Validar reglas de negocio del nombre
*
* @throws InvalidComponentException
* @return void
*/
private function validate(): void
{
// Regla 1: No puede estar vacío
if (empty($this->value)) {
throw new InvalidComponentException('Component name cannot be empty');
}
// Regla 2: Longitud entre MIN y MAX
$length = strlen($this->value);
if ($length < self::MIN_LENGTH || $length > self::MAX_LENGTH) {
throw new InvalidComponentException(
sprintf(
'Component name must be between %d and %d characters (got %d)',
self::MIN_LENGTH,
self::MAX_LENGTH,
$length
)
);
}
// Regla 3: Formato válido (lowercase, números, guiones bajos, comienza con letra)
if (!preg_match(self::PATTERN, $this->value)) {
throw new InvalidComponentException(
'Component name must start with a letter and contain only lowercase letters, numbers, underscores, and hyphens'
);
}
}
/**
* Crear desde string (factory method)
*
* @param string $value
* @return self
*/
public static function fromString(string $value): self
{
return new self($value);
}
/**
* Normalizar string a formato de nombre válido
*
* Convierte:
* - "Top Bar" → "top_bar"
* - "FOOTER-CTA" → "footer_cta"
* - " sidebar " → "sidebar"
*
* @param string $value
* @return self
*/
public static function fromNormalized(string $value): self
{
// Trim espacios
$normalized = trim($value);
// Convertir a minúsculas
$normalized = strtolower($normalized);
// Reemplazar espacios y guiones por guiones bajos
$normalized = str_replace([' ', '-'], '_', $normalized);
// Eliminar caracteres no permitidos
$normalized = preg_replace('/[^a-z0-9_]/', '', $normalized);
// Eliminar guiones bajos duplicados
$normalized = preg_replace('/_+/', '_', $normalized);
// Eliminar guiones bajos al inicio/final
$normalized = trim($normalized, '_');
return new self($normalized);
}
}

View File

@@ -0,0 +1,266 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Domain\ValueObjects;
use ROITheme\Shared\Domain\Exceptions\InvalidComponentException;
/**
* ComponentVisibility - Value Object inmutable para visibilidad de componente
*
* RESPONSABILIDAD: Representar y validar la visibilidad de un componente en diferentes dispositivos
*
* REGLAS DE NEGOCIO:
* - Un componente puede estar habilitado o deshabilitado globalmente
* - Un componente puede tener visibilidad específica por dispositivo (desktop, tablet, mobile)
* - Si está deshabilitado globalmente, las visibilidades por dispositivo no importan
* - Al menos un dispositivo debe tener visibilidad si está habilitado globalmente
*
* CASOS DE USO:
* - Componente visible solo en desktop
* - Componente visible solo en mobile
* - Componente visible en todos los dispositivos
* - Componente completamente deshabilitado
*
* USO:
* ```php
* // Visible en todos los dispositivos
* $visibility = ComponentVisibility::allDevices();
*
* // Visible solo en desktop
* $visibility = ComponentVisibility::desktopOnly();
*
* // Personalizado
* $visibility = new ComponentVisibility(
* enabled: true,
* visibleDesktop: true,
* visibleTablet: true,
* visibleMobile: false
* );
* ```
*
* @package ROITheme\Shared\Domain\ValueObjects
*/
final readonly class ComponentVisibility
{
/**
* Constructor
*
* @param bool $enabled Componente habilitado globalmente
* @param bool $visibleDesktop Visible en desktop
* @param bool $visibleTablet Visible en tablet
* @param bool $visibleMobile Visible en mobile
* @throws InvalidComponentException
*/
public function __construct(
private bool $enabled,
private bool $visibleDesktop,
private bool $visibleTablet,
private bool $visibleMobile
) {
$this->validate();
}
/**
* Verificar si está habilitado globalmente
*
* @return bool
*/
public function isEnabled(): bool
{
return $this->enabled;
}
/**
* Verificar si es visible en desktop
*
* @return bool
*/
public function isVisibleOnDesktop(): bool
{
return $this->enabled && $this->visibleDesktop;
}
/**
* Verificar si es visible en tablet
*
* @return bool
*/
public function isVisibleOnTablet(): bool
{
return $this->enabled && $this->visibleTablet;
}
/**
* Verificar si es visible en mobile
*
* @return bool
*/
public function isVisibleOnMobile(): bool
{
return $this->enabled && $this->visibleMobile;
}
/**
* Verificar si es visible en al menos un dispositivo
*
* @return bool
*/
public function isVisibleOnAnyDevice(): bool
{
return $this->enabled && (
$this->visibleDesktop ||
$this->visibleTablet ||
$this->visibleMobile
);
}
/**
* Verificar si es visible en todos los dispositivos
*
* @return bool
*/
public function isVisibleOnAllDevices(): bool
{
return $this->enabled &&
$this->visibleDesktop &&
$this->visibleTablet &&
$this->visibleMobile;
}
/**
* Convertir a array
*
* @return array
*/
public function toArray(): array
{
return [
'enabled' => $this->enabled,
'visible_desktop' => $this->visibleDesktop,
'visible_tablet' => $this->visibleTablet,
'visible_mobile' => $this->visibleMobile
];
}
/**
* Validar reglas de negocio
*
* @throws InvalidComponentException
* @return void
*/
private function validate(): void
{
// Regla: Si está habilitado, al menos un dispositivo debe tener visibilidad
if ($this->enabled && !$this->visibleDesktop && !$this->visibleTablet && !$this->visibleMobile) {
throw new InvalidComponentException(
'Component is enabled but not visible on any device. At least one device must be visible.'
);
}
}
/**
* Factory: Componente deshabilitado
*
* @return self
*/
public static function disabled(): self
{
return new self(
enabled: false,
visibleDesktop: false,
visibleTablet: false,
visibleMobile: false
);
}
/**
* Factory: Componente visible en todos los dispositivos
*
* @return self
*/
public static function allDevices(): self
{
return new self(
enabled: true,
visibleDesktop: true,
visibleTablet: true,
visibleMobile: true
);
}
/**
* Factory: Componente visible solo en desktop
*
* @return self
*/
public static function desktopOnly(): self
{
return new self(
enabled: true,
visibleDesktop: true,
visibleTablet: false,
visibleMobile: false
);
}
/**
* Factory: Componente visible solo en mobile
*
* @return self
*/
public static function mobileOnly(): self
{
return new self(
enabled: true,
visibleDesktop: false,
visibleTablet: false,
visibleMobile: true
);
}
/**
* Factory: Componente visible solo en tablet
*
* @return self
*/
public static function tabletOnly(): self
{
return new self(
enabled: true,
visibleDesktop: false,
visibleTablet: true,
visibleMobile: false
);
}
/**
* Crear desde array
*
* @param array $data
* @return self
*/
public static function fromArray(array $data): self
{
return new self(
enabled: (bool) ($data['enabled'] ?? false),
visibleDesktop: (bool) ($data['visible_desktop'] ?? true),
visibleTablet: (bool) ($data['visible_tablet'] ?? true),
visibleMobile: (bool) ($data['visible_mobile'] ?? true)
);
}
/**
* Comparar con otro ComponentVisibility
*
* @param ComponentVisibility $other
* @return bool
*/
public function equals(ComponentVisibility $other): bool
{
return $this->enabled === $other->enabled &&
$this->visibleDesktop === $other->visibleDesktop &&
$this->visibleTablet === $other->visibleTablet &&
$this->visibleMobile === $other->visibleMobile;
}
}