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:
0
Shared/Domain/Contracts/.gitkeep
Normal file
0
Shared/Domain/Contracts/.gitkeep
Normal file
11
Shared/Domain/Contracts/AjaxControllerInterface.php
Normal file
11
Shared/Domain/Contracts/AjaxControllerInterface.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Interface para controlador AJAX
|
||||
*/
|
||||
interface AjaxControllerInterface {
|
||||
// Se implementará completamente en Fase 5
|
||||
}
|
||||
61
Shared/Domain/Contracts/CSSGeneratorInterface.php
Normal file
61
Shared/Domain/Contracts/CSSGeneratorInterface.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Interface CSSGeneratorInterface
|
||||
*
|
||||
* Contrato para servicios que generan CSS a partir de configuraciones de componentes.
|
||||
* Define el comportamiento esperado para la generación de reglas CSS sin depender
|
||||
* de implementaciones específicas o frameworks.
|
||||
*
|
||||
* Responsabilidades:
|
||||
* - Generar CSS válido a partir de un selector y estilos
|
||||
* - Formatear reglas CSS correctamente
|
||||
* - Convertir nombres de propiedades (snake_case → kebab-case)
|
||||
*
|
||||
* NO responsable de:
|
||||
* - Media queries (manejado por Renderer con clases Bootstrap)
|
||||
* - Visibilidad responsive (manejado por Renderer)
|
||||
* - Persistencia o caché de CSS
|
||||
*
|
||||
* @package ROITheme\Shared\Domain\Contracts
|
||||
*/
|
||||
interface CSSGeneratorInterface
|
||||
{
|
||||
/**
|
||||
* Genera una regla CSS completa a partir de un selector y sus estilos.
|
||||
*
|
||||
* Convierte un array de estilos en una regla CSS válida y formateada.
|
||||
* Los nombres de propiedades en snake_case se convierten automáticamente
|
||||
* a kebab-case según el estándar CSS.
|
||||
*
|
||||
* Ejemplo:
|
||||
* ```php
|
||||
* $styles = [
|
||||
* 'background_color' => '#FF8600',
|
||||
* 'text_color' => '#FFFFFF',
|
||||
* 'font_size' => '1rem',
|
||||
* 'padding' => '1rem 0'
|
||||
* ];
|
||||
*
|
||||
* $css = $generator->generate('.navbar', $styles);
|
||||
*
|
||||
* // Resultado:
|
||||
* // .navbar {
|
||||
* // background-color: #FF8600;
|
||||
* // color: #FFFFFF;
|
||||
* // font-size: 1rem;
|
||||
* // padding: 1rem 0;
|
||||
* // }
|
||||
* ```
|
||||
*
|
||||
* @param string $selector Selector CSS (ej: '.navbar', '#header', 'body')
|
||||
* @param array<string, string> $styles Array asociativo de propiedades CSS y sus valores
|
||||
* Formato: ['property_name' => 'value']
|
||||
*
|
||||
* @return string Regla CSS completa y formateada, o string vacío si no hay estilos
|
||||
*/
|
||||
public function generate(string $selector, array $styles): string;
|
||||
}
|
||||
11
Shared/Domain/Contracts/CacheServiceInterface.php
Normal file
11
Shared/Domain/Contracts/CacheServiceInterface.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Interface para servicio de cache
|
||||
*/
|
||||
interface CacheServiceInterface {
|
||||
// Se implementará completamente en Fase 5
|
||||
}
|
||||
11
Shared/Domain/Contracts/CleanupServiceInterface.php
Normal file
11
Shared/Domain/Contracts/CleanupServiceInterface.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Interface para servicio de limpieza
|
||||
*/
|
||||
interface CleanupServiceInterface {
|
||||
// Se implementará completamente en Fase 5
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Contracts;
|
||||
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Domain\ValueObjects\ComponentName;
|
||||
use ROITheme\Shared\Domain\ValueObjects\ComponentConfiguration;
|
||||
|
||||
/**
|
||||
* ComponentDefaultsRepositoryInterface - Contrato para valores por defecto de componentes
|
||||
*
|
||||
* RESPONSABILIDAD: Definir contrato para gestión de configuración por defecto de componentes
|
||||
*
|
||||
* PROPÓSITO:
|
||||
* - Almacenar configuración "factory" de cada tipo de componente
|
||||
* - Permitir resetear componentes a valores por defecto
|
||||
* - Proporcionar plantillas para nuevos componentes
|
||||
*
|
||||
* CASO DE USO:
|
||||
* ```php
|
||||
* // Crear nuevo componente top_bar con valores por defecto
|
||||
* $defaults = $defaultsRepo->getByName(ComponentName::fromString('top_bar'));
|
||||
* $component = new Component(
|
||||
* ComponentName::fromString('top_bar'),
|
||||
* $defaults // configuración por defecto
|
||||
* );
|
||||
*
|
||||
* // Resetear componente a valores por defecto
|
||||
* $component = $component->updateConfiguration($defaults);
|
||||
* ```
|
||||
*
|
||||
* @package ROITheme\Shared\Domain\Contracts
|
||||
*/
|
||||
interface ComponentDefaultsRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Obtener configuración por defecto de un componente
|
||||
*
|
||||
* @param ComponentName $name Nombre del componente
|
||||
* @return ComponentConfiguration Configuración por defecto
|
||||
*/
|
||||
public function getByName(ComponentName $name): ComponentConfiguration;
|
||||
|
||||
/**
|
||||
* Guardar configuración por defecto para un componente
|
||||
*
|
||||
* @param ComponentName $name Nombre del componente
|
||||
* @param ComponentConfiguration $configuration Configuración por defecto
|
||||
* @return void
|
||||
*/
|
||||
public function save(ComponentName $name, ComponentConfiguration $configuration): void;
|
||||
|
||||
/**
|
||||
* Verificar si existen defaults para un componente
|
||||
*
|
||||
* @param ComponentName $name
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(ComponentName $name): bool;
|
||||
|
||||
/**
|
||||
* Obtener todos los defaults
|
||||
*
|
||||
* @return array<string, ComponentConfiguration> Array asociativo nombre => configuración
|
||||
*/
|
||||
public function findAll(): array;
|
||||
|
||||
/**
|
||||
* Eliminar defaults de un componente
|
||||
*
|
||||
* @param ComponentName $name
|
||||
* @return bool True si se eliminó, false si no existía
|
||||
*/
|
||||
public function delete(ComponentName $name): bool;
|
||||
}
|
||||
11
Shared/Domain/Contracts/ComponentManagerInterface.php
Normal file
11
Shared/Domain/Contracts/ComponentManagerInterface.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Interface para facade ComponentManager
|
||||
*/
|
||||
interface ComponentManagerInterface {
|
||||
// Se implementará completamente en Fase 5
|
||||
}
|
||||
124
Shared/Domain/Contracts/ComponentRepositoryInterface.php
Normal file
124
Shared/Domain/Contracts/ComponentRepositoryInterface.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Contracts;
|
||||
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Domain\ValueObjects\ComponentName;
|
||||
use ROITheme\Shared\Domain\Exceptions\ComponentNotFoundException;
|
||||
|
||||
/**
|
||||
* ComponentRepositoryInterface - Contrato para persistencia de componentes
|
||||
*
|
||||
* RESPONSABILIDAD: Definir contrato para operaciones CRUD de componentes
|
||||
*
|
||||
* INVERSIÓN DE DEPENDENCIAS:
|
||||
* - Esta interfaz está en Domain (núcleo)
|
||||
* - Infrastructure implementa esta interfaz
|
||||
* - Application depende de esta interfaz (NO de la implementación)
|
||||
*
|
||||
* IMPLEMENTACIONES ESPERADAS:
|
||||
* - WordPressComponentRepository (usa wpdb y tablas WP)
|
||||
* - InMemoryComponentRepository (para testing)
|
||||
* - FileSystemComponentRepository (futuro: archivos JSON)
|
||||
*
|
||||
* PRINCIPIOS:
|
||||
* - Métodos retornan entidades de dominio (Component)
|
||||
* - Parámetros son Value Objects o primitivos
|
||||
* - Excepciones son de dominio
|
||||
* - Sin conocimiento de WordPress/BD
|
||||
*
|
||||
* USO:
|
||||
* ```php
|
||||
* // En Application Layer
|
||||
* class SaveComponentUseCase {
|
||||
* public function __construct(
|
||||
* private ComponentRepositoryInterface $repository
|
||||
* ) {}
|
||||
*
|
||||
* public function execute(SaveComponentRequest $request): void {
|
||||
* $component = new Component(...);
|
||||
* $this->repository->save($component);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @package ROITheme\Shared\Domain\Contracts
|
||||
*/
|
||||
interface ComponentRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Guardar o actualizar un componente
|
||||
*
|
||||
* Si el componente ya existe (por nombre), se actualiza.
|
||||
* Si no existe, se crea.
|
||||
*
|
||||
* @param Component $component Componente a guardar
|
||||
* @return Component Componente guardado (con timestamps actualizados)
|
||||
*/
|
||||
public function save(Component $component): Component;
|
||||
|
||||
/**
|
||||
* Buscar componente por nombre
|
||||
*
|
||||
* @param ComponentName $name Nombre del componente
|
||||
* @return Component|null Componente encontrado o null
|
||||
*/
|
||||
public function findByName(ComponentName $name): ?Component;
|
||||
|
||||
/**
|
||||
* Obtener componente por nombre (lanza excepción si no existe)
|
||||
*
|
||||
* @param ComponentName $name
|
||||
* @return Component
|
||||
* @throws ComponentNotFoundException
|
||||
*/
|
||||
public function getByName(ComponentName $name): Component;
|
||||
|
||||
/**
|
||||
* Obtener todos los componentes
|
||||
*
|
||||
* @return Component[] Array de componentes
|
||||
*/
|
||||
public function findAll(): array;
|
||||
|
||||
/**
|
||||
* Obtener componentes habilitados
|
||||
*
|
||||
* @return Component[] Array de componentes habilitados
|
||||
*/
|
||||
public function findEnabled(): array;
|
||||
|
||||
/**
|
||||
* Verificar si existe un componente con el nombre dado
|
||||
*
|
||||
* @param ComponentName $name
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(ComponentName $name): bool;
|
||||
|
||||
/**
|
||||
* Eliminar un componente
|
||||
*
|
||||
* @param ComponentName $name Nombre del componente a eliminar
|
||||
* @return bool True si se eliminó, false si no existía
|
||||
*/
|
||||
public function delete(ComponentName $name): bool;
|
||||
|
||||
/**
|
||||
* Obtener cantidad total de componentes
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int;
|
||||
|
||||
/**
|
||||
* Obtener componentes por grupo de configuración
|
||||
*
|
||||
* Ejemplo: Obtener todos los componentes que tienen configuración de 'content'
|
||||
*
|
||||
* @param string $group Grupo de configuración (visibility, content, styles, general)
|
||||
* @return Component[]
|
||||
*/
|
||||
public function findByConfigGroup(string $group): array;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Interface para el repositorio de configuraciones de componentes
|
||||
*
|
||||
* Domain Layer - Define el contrato para acceso a datos de configuraci<63>n
|
||||
* Trabaja con la tabla wp_roi_theme_component_settings (normalizada)
|
||||
*
|
||||
* Esta interfaz es gen<65>rica y funciona para todos los componentes del tema.
|
||||
*
|
||||
* @package ROITheme\Shared\Domain\Contracts
|
||||
*/
|
||||
interface ComponentSettingsRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Obtiene todas las configuraciones de un componente agrupadas por grupo
|
||||
*
|
||||
* @param string $componentName Nombre del componente (ej: 'top-notification-bar')
|
||||
* @return array<string, array<string, mixed>> Configuraciones agrupadas
|
||||
* Ejemplo: [
|
||||
* 'visibility' => ['enabled' => true, 'show_on_mobile' => true, ...],
|
||||
* 'content' => ['icon_class' => 'bi-star', 'label_text' => 'Nuevo', ...],
|
||||
* 'styles' => ['background_color' => '#0E2337', ...]
|
||||
* ]
|
||||
*/
|
||||
public function getComponentSettings(string $componentName): array;
|
||||
|
||||
/**
|
||||
* Guarda las configuraciones de un componente
|
||||
*
|
||||
* @param string $componentName Nombre del componente
|
||||
* @param array<string, array<string, mixed>> $settings Configuraciones agrupadas
|
||||
* Ejemplo: [
|
||||
* 'visibility' => ['enabled' => true],
|
||||
* 'content' => ['icon_class' => 'bi-star']
|
||||
* ]
|
||||
* @return int N<>mero de campos actualizados
|
||||
*/
|
||||
public function saveComponentSettings(string $componentName, array $settings): int;
|
||||
|
||||
/**
|
||||
* Obtiene el valor de un campo espec<65>fico
|
||||
*
|
||||
* @param string $componentName Nombre del componente
|
||||
* @param string $groupName Nombre del grupo
|
||||
* @param string $attributeName Nombre del atributo
|
||||
* @return mixed|null Valor del campo o null si no existe
|
||||
*/
|
||||
public function getFieldValue(string $componentName, string $groupName, string $attributeName): mixed;
|
||||
|
||||
/**
|
||||
* Guarda el valor de un campo espec<65>fico
|
||||
*
|
||||
* @param string $componentName Nombre del componente
|
||||
* @param string $groupName Nombre del grupo
|
||||
* @param string $attributeName Nombre del atributo
|
||||
* @param mixed $value Valor a guardar
|
||||
* @return bool True si se guard<72> correctamente
|
||||
*/
|
||||
public function saveFieldValue(string $componentName, string $groupName, string $attributeName, mixed $value): bool;
|
||||
|
||||
/**
|
||||
* Restaura un componente a sus valores por defecto desde el schema JSON
|
||||
*
|
||||
* @param string $componentName Nombre del componente
|
||||
* @param string $schemaPath Ruta al archivo schema JSON
|
||||
* @return int N<>mero de campos restaurados
|
||||
*/
|
||||
public function resetToDefaults(string $componentName, string $schemaPath): int;
|
||||
}
|
||||
11
Shared/Domain/Contracts/ConfigurationServiceInterface.php
Normal file
11
Shared/Domain/Contracts/ConfigurationServiceInterface.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Interface para servicio de configuración
|
||||
*/
|
||||
interface ConfigurationServiceInterface {
|
||||
// Se implementará completamente en Fase 5
|
||||
}
|
||||
11
Shared/Domain/Contracts/DefaultRepositoryInterface.php
Normal file
11
Shared/Domain/Contracts/DefaultRepositoryInterface.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Interface para repositorio de defaults
|
||||
*/
|
||||
interface DefaultRepositoryInterface {
|
||||
// Se implementará completamente en Fase 5
|
||||
}
|
||||
55
Shared/Domain/Contracts/RendererInterface.php
Normal file
55
Shared/Domain/Contracts/RendererInterface.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Contracts;
|
||||
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
|
||||
/**
|
||||
* RendererInterface - Contrato para renderizadores de componentes
|
||||
*
|
||||
* RESPONSABILIDAD: Definir el contrato que deben cumplir todos los renderizadores
|
||||
* de componentes para generar HTML a partir de los datos del componente.
|
||||
*
|
||||
* PRINCIPIOS:
|
||||
* - Interface Segregation: Una sola responsabilidad - renderizar HTML
|
||||
* - Dependency Inversion: Depender de abstracción, no de implementación
|
||||
*
|
||||
* USO:
|
||||
* ```php
|
||||
* final class MyRenderer implements RendererInterface
|
||||
* {
|
||||
* public function render(Component $component): string
|
||||
* {
|
||||
* $data = $component->getData();
|
||||
* // Generar HTML
|
||||
* return $html;
|
||||
* }
|
||||
*
|
||||
* public function supports(string $componentType): bool
|
||||
* {
|
||||
* return $componentType === 'my-component';
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @package ROITheme\Domain\Component
|
||||
*/
|
||||
interface RendererInterface
|
||||
{
|
||||
/**
|
||||
* Renderizar un componente a HTML
|
||||
*
|
||||
* @param Component $component Componente a renderizar
|
||||
* @return string HTML generado
|
||||
*/
|
||||
public function render(Component $component): string;
|
||||
|
||||
/**
|
||||
* Verificar si este renderizador soporta un tipo de componente
|
||||
*
|
||||
* @param string $componentType Tipo de componente (ej: 'navbar', 'footer')
|
||||
* @return bool True si soporta el tipo, false en caso contrario
|
||||
*/
|
||||
public function supports(string $componentType): bool;
|
||||
}
|
||||
11
Shared/Domain/Contracts/SchemaSyncServiceInterface.php
Normal file
11
Shared/Domain/Contracts/SchemaSyncServiceInterface.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Interface para servicio de sincronización de schemas
|
||||
*/
|
||||
interface SchemaSyncServiceInterface {
|
||||
// Se implementará completamente en Fase 5
|
||||
}
|
||||
11
Shared/Domain/Contracts/TransactionManagerInterface.php
Normal file
11
Shared/Domain/Contracts/TransactionManagerInterface.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Interface para gestión de transacciones de base de datos
|
||||
*/
|
||||
interface TransactionManagerInterface {
|
||||
// Se implementará completamente en Fase 5
|
||||
}
|
||||
199
Shared/Domain/Contracts/ValidationServiceInterface.php
Normal file
199
Shared/Domain/Contracts/ValidationServiceInterface.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* ValidationServiceInterface - Contrato para servicio de validación
|
||||
*
|
||||
* RESPONSABILIDAD: Definir contrato para validación de datos de entrada
|
||||
*
|
||||
* PROPÓSITO:
|
||||
* - Validar datos antes de crear entidades de dominio
|
||||
* - Sanitizar entrada del usuario
|
||||
* - Retornar errores de validación estructurados
|
||||
*
|
||||
* UBICACIÓN EN ARQUITECTURA:
|
||||
* - Interfaz definida en Domain
|
||||
* - Implementación en Infrastructure (puede usar WordPress functions)
|
||||
* - Usado por Application Layer (Use Cases)
|
||||
*
|
||||
* IMPLEMENTACIONES ESPERADAS:
|
||||
* - WordPressValidationService (usa sanitize_*, wp_kses, etc.)
|
||||
* - StrictValidationService (validación más estricta para prod)
|
||||
* - LenientValidationService (validación más permisiva para dev)
|
||||
*
|
||||
* USO:
|
||||
* ```php
|
||||
* // En Application Layer
|
||||
* class SaveComponentUseCase {
|
||||
* public function __construct(
|
||||
* private ValidationServiceInterface $validator
|
||||
* ) {}
|
||||
*
|
||||
* public function execute(SaveComponentRequest $request): void {
|
||||
* $result = $this->validator->validate(
|
||||
* $request->getData(),
|
||||
* $request->getComponentName()
|
||||
* );
|
||||
*
|
||||
* if (!$result->isValid()) {
|
||||
* throw new ValidationException($result->getErrors());
|
||||
* }
|
||||
*
|
||||
* // ... crear componente con datos validados
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @package ROITheme\Shared\Domain\Contracts
|
||||
*/
|
||||
interface ValidationServiceInterface
|
||||
{
|
||||
/**
|
||||
* Validar datos de un componente
|
||||
*
|
||||
* @param array $data Datos a validar
|
||||
* @param string $componentName Nombre del componente (para reglas específicas)
|
||||
* @return ValidationResult Resultado de validación
|
||||
*/
|
||||
public function validate(array $data, string $componentName): ValidationResult;
|
||||
|
||||
/**
|
||||
* Sanitizar datos de entrada
|
||||
*
|
||||
* @param array $data Datos a sanitizar
|
||||
* @param string $componentName Nombre del componente
|
||||
* @return array Datos sanitizados
|
||||
*/
|
||||
public function sanitize(array $data, string $componentName): array;
|
||||
|
||||
/**
|
||||
* Validar una URL
|
||||
*
|
||||
* @param string $url URL a validar
|
||||
* @return bool
|
||||
*/
|
||||
public function isValidUrl(string $url): bool;
|
||||
|
||||
/**
|
||||
* Validar un color hexadecimal
|
||||
*
|
||||
* @param string $color Color a validar (ej: #000000)
|
||||
* @return bool
|
||||
*/
|
||||
public function isValidColor(string $color): bool;
|
||||
|
||||
/**
|
||||
* Validar nombre de componente
|
||||
*
|
||||
* @param string $name Nombre a validar
|
||||
* @return bool
|
||||
*/
|
||||
public function isValidComponentName(string $name): bool;
|
||||
}
|
||||
|
||||
/**
|
||||
* ValidationResult - Value Object para resultado de validación
|
||||
*
|
||||
* RESPONSABILIDAD: Encapsular resultado de una validación
|
||||
*
|
||||
* @package ROITheme\Shared\Domain\Contracts
|
||||
*/
|
||||
final readonly class ValidationResult
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param bool $isValid Si la validación pasó
|
||||
* @param array $errors Array de errores (campo => mensaje)
|
||||
* @param array $sanitizedData Datos sanitizados
|
||||
*/
|
||||
public function __construct(
|
||||
private bool $isValid,
|
||||
private array $errors = [],
|
||||
private array $sanitizedData = []
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Verificar si la validación pasó
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
return $this->isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener errores de validación
|
||||
*
|
||||
* @return array Array asociativo campo => mensaje
|
||||
*/
|
||||
public function getErrors(): array
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener datos sanitizados
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getSanitizedData(): array
|
||||
{
|
||||
return $this->sanitizedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si hay error en campo específico
|
||||
*
|
||||
* @param string $field
|
||||
* @return bool
|
||||
*/
|
||||
public function hasError(string $field): bool
|
||||
{
|
||||
return isset($this->errors[$field]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener error de un campo específico
|
||||
*
|
||||
* @param string $field
|
||||
* @return string|null
|
||||
*/
|
||||
public function getError(string $field): ?string
|
||||
{
|
||||
return $this->errors[$field] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory: Resultado exitoso
|
||||
*
|
||||
* @param array $sanitizedData
|
||||
* @return self
|
||||
*/
|
||||
public static function success(array $sanitizedData): self
|
||||
{
|
||||
return new self(
|
||||
isValid: true,
|
||||
errors: [],
|
||||
sanitizedData: $sanitizedData
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory: Resultado con errores
|
||||
*
|
||||
* @param array $errors
|
||||
* @return self
|
||||
*/
|
||||
public static function failure(array $errors): self
|
||||
{
|
||||
return new self(
|
||||
isValid: false,
|
||||
errors: $errors,
|
||||
sanitizedData: []
|
||||
);
|
||||
}
|
||||
}
|
||||
335
Shared/Domain/Entities/Component.php
Normal file
335
Shared/Domain/Entities/Component.php
Normal file
@@ -0,0 +1,335 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Entities;
|
||||
|
||||
use ROITheme\Shared\Domain\ValueObjects\ComponentName;
|
||||
use ROITheme\Shared\Domain\ValueObjects\ComponentConfiguration;
|
||||
use ROITheme\Shared\Domain\ValueObjects\ComponentVisibility;
|
||||
use ROITheme\Shared\Domain\Exceptions\InvalidComponentException;
|
||||
|
||||
/**
|
||||
* Component - Entidad del Dominio
|
||||
*
|
||||
* RESPONSABILIDAD: Representar un componente del tema con toda su lógica de negocio
|
||||
*
|
||||
* INVARIANTES (Reglas que SIEMPRE deben cumplirse):
|
||||
* 1. Un componente siempre tiene un nombre válido
|
||||
* 2. Un componente siempre tiene configuración (puede estar vacía)
|
||||
* 3. Si tiene CTA URL debe tener CTA text (validado en ComponentConfiguration)
|
||||
* 4. Los colores siempre están en formato hexadecimal válido
|
||||
* 5. El timestamp de updated_at es siempre >= created_at
|
||||
*
|
||||
* ENTIDAD vs VALUE OBJECT:
|
||||
* - Component es ENTIDAD porque tiene identidad (puede cambiar configuración pero sigue siendo el mismo componente)
|
||||
* - ComponentName es VALUE OBJECT (dos nombres iguales son indistinguibles)
|
||||
* - ComponentConfiguration es VALUE OBJECT (inmutable)
|
||||
*
|
||||
* USO:
|
||||
* ```php
|
||||
* $component = new Component(
|
||||
* ComponentName::fromString('top_bar'),
|
||||
* ComponentConfiguration::fromFlat([
|
||||
* 'enabled' => true,
|
||||
* 'message_text' => 'Welcome!'
|
||||
* ]),
|
||||
* ComponentVisibility::allDevices()
|
||||
* );
|
||||
*
|
||||
* // Actualizar configuración (inmutabilidad)
|
||||
* $updated = $component->updateConfiguration(
|
||||
* $component->configuration()->withValue('content', 'message_text', 'Hello!')
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @package ROITheme\Shared\Domain\Entities
|
||||
*/
|
||||
final class Component
|
||||
{
|
||||
/**
|
||||
* @var ComponentName Nombre del componente (inmutable)
|
||||
*/
|
||||
private ComponentName $name;
|
||||
|
||||
/**
|
||||
* @var ComponentConfiguration Configuración del componente
|
||||
*/
|
||||
private ComponentConfiguration $configuration;
|
||||
|
||||
/**
|
||||
* @var ComponentVisibility Visibilidad del componente
|
||||
*/
|
||||
private ComponentVisibility $visibility;
|
||||
|
||||
/**
|
||||
* @var bool Componente habilitado
|
||||
*/
|
||||
private bool $isEnabled;
|
||||
|
||||
/**
|
||||
* @var string Versión del schema
|
||||
*/
|
||||
private string $schemaVersion;
|
||||
|
||||
/**
|
||||
* @var \DateTimeImmutable Fecha de creación
|
||||
*/
|
||||
private \DateTimeImmutable $createdAt;
|
||||
|
||||
/**
|
||||
* @var \DateTimeImmutable Fecha de última actualización
|
||||
*/
|
||||
private \DateTimeImmutable $updatedAt;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param ComponentName $name
|
||||
* @param ComponentConfiguration $configuration
|
||||
* @param ComponentVisibility $visibility
|
||||
* @param bool $isEnabled
|
||||
* @param string $schemaVersion
|
||||
* @param \DateTimeImmutable|null $createdAt
|
||||
* @param \DateTimeImmutable|null $updatedAt
|
||||
* @throws InvalidComponentException Si se violan invariantes
|
||||
*/
|
||||
public function __construct(
|
||||
ComponentName $name,
|
||||
ComponentConfiguration $configuration,
|
||||
ComponentVisibility $visibility,
|
||||
bool $isEnabled = true,
|
||||
string $schemaVersion = '1.0.0',
|
||||
?\DateTimeImmutable $createdAt = null,
|
||||
?\DateTimeImmutable $updatedAt = null
|
||||
) {
|
||||
$this->name = $name;
|
||||
$this->configuration = $configuration;
|
||||
$this->visibility = $visibility;
|
||||
$this->isEnabled = $isEnabled;
|
||||
$this->schemaVersion = $schemaVersion;
|
||||
$this->createdAt = $createdAt ?? new \DateTimeImmutable();
|
||||
$this->updatedAt = $updatedAt ?? new \DateTimeImmutable();
|
||||
|
||||
$this->validateInvariants();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener nombre del componente
|
||||
*
|
||||
* @return ComponentName
|
||||
*/
|
||||
public function name(): ComponentName
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener configuración del componente
|
||||
*
|
||||
* @return ComponentConfiguration
|
||||
*/
|
||||
public function configuration(): ComponentConfiguration
|
||||
{
|
||||
return $this->configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener visibilidad del componente
|
||||
*
|
||||
* @return ComponentVisibility
|
||||
*/
|
||||
public function visibility(): ComponentVisibility
|
||||
{
|
||||
return $this->visibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si el componente está habilitado
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->isEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener versión del schema
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function schemaVersion(): string
|
||||
{
|
||||
return $this->schemaVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener fecha de creación
|
||||
*
|
||||
* @return \DateTimeImmutable
|
||||
*/
|
||||
public function createdAt(): \DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener fecha de última actualización
|
||||
*
|
||||
* @return \DateTimeImmutable
|
||||
*/
|
||||
public function updatedAt(): \DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear nuevo componente con configuración actualizada
|
||||
* (Inmutabilidad: retorna nueva instancia)
|
||||
*
|
||||
* @param ComponentConfiguration $configuration Nueva configuración
|
||||
* @return self Nueva instancia
|
||||
*/
|
||||
public function updateConfiguration(ComponentConfiguration $configuration): self
|
||||
{
|
||||
return new self(
|
||||
$this->name,
|
||||
$configuration,
|
||||
$this->visibility,
|
||||
$this->isEnabled,
|
||||
$this->schemaVersion,
|
||||
$this->createdAt,
|
||||
new \DateTimeImmutable()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear nuevo componente con visibilidad actualizada
|
||||
* (Inmutabilidad: retorna nueva instancia)
|
||||
*
|
||||
* @param ComponentVisibility $visibility Nueva visibilidad
|
||||
* @return self Nueva instancia
|
||||
*/
|
||||
public function updateVisibility(ComponentVisibility $visibility): self
|
||||
{
|
||||
return new self(
|
||||
$this->name,
|
||||
$this->configuration,
|
||||
$visibility,
|
||||
$this->isEnabled,
|
||||
$this->schemaVersion,
|
||||
$this->createdAt,
|
||||
new \DateTimeImmutable()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear nuevo componente habilitado
|
||||
* (Inmutabilidad: retorna nueva instancia)
|
||||
*
|
||||
* @return self Nueva instancia con isEnabled=true
|
||||
*/
|
||||
public function enable(): self
|
||||
{
|
||||
return new self(
|
||||
$this->name,
|
||||
$this->configuration,
|
||||
$this->visibility,
|
||||
true,
|
||||
$this->schemaVersion,
|
||||
$this->createdAt,
|
||||
new \DateTimeImmutable()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear nuevo componente deshabilitado
|
||||
* (Inmutabilidad: retorna nueva instancia)
|
||||
*
|
||||
* @return self Nueva instancia con isEnabled=false
|
||||
*/
|
||||
public function disable(): self
|
||||
{
|
||||
return new self(
|
||||
$this->name,
|
||||
$this->configuration,
|
||||
$this->visibility,
|
||||
false,
|
||||
$this->schemaVersion,
|
||||
$this->createdAt,
|
||||
new \DateTimeImmutable()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparar con otro componente
|
||||
* (Dos componentes son iguales si tienen el mismo nombre)
|
||||
*
|
||||
* @param Component $other Otro componente
|
||||
* @return bool True si son el mismo componente
|
||||
*/
|
||||
public function equals(Component $other): bool
|
||||
{
|
||||
return $this->name->equals($other->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener datos del componente para renderizado
|
||||
* Retorna la configuración completa en formato array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getData(): array
|
||||
{
|
||||
return $this->configuration->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertir a array para serialización
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name->value(),
|
||||
'configuration' => $this->configuration->all(),
|
||||
'visibility' => $this->visibility->toArray(),
|
||||
'is_enabled' => $this->isEnabled,
|
||||
'schema_version' => $this->schemaVersion,
|
||||
'created_at' => $this->createdAt->format('Y-m-d H:i:s'),
|
||||
'updated_at' => $this->updatedAt->format('Y-m-d H:i:s')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertir a string para debugging
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf(
|
||||
'Component(%s, enabled=%s, schema=%s)',
|
||||
$this->name->value(),
|
||||
$this->isEnabled ? 'true' : 'false',
|
||||
$this->schemaVersion
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validar invariantes del componente
|
||||
*
|
||||
* @throws InvalidComponentException Si se viola algún invariante
|
||||
* @return void
|
||||
*/
|
||||
private function validateInvariants(): void
|
||||
{
|
||||
// Invariante: updated_at >= created_at
|
||||
if ($this->updatedAt < $this->createdAt) {
|
||||
throw new InvalidComponentException(
|
||||
'Updated timestamp cannot be before created timestamp'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
0
Shared/Domain/Exceptions/.gitkeep
Normal file
0
Shared/Domain/Exceptions/.gitkeep
Normal file
69
Shared/Domain/Exceptions/ComponentNotFoundException.php
Normal file
69
Shared/Domain/Exceptions/ComponentNotFoundException.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Exceptions;
|
||||
|
||||
/**
|
||||
* ComponentNotFoundException - Excepción de Dominio para componente no encontrado
|
||||
*
|
||||
* RESPONSABILIDAD: Representar errores cuando un componente solicitado no existe
|
||||
*
|
||||
* CUÁNDO LANZAR:
|
||||
* - Búsqueda de componente por nombre que no existe en el repositorio
|
||||
* - Intento de actualizar/eliminar componente inexistente
|
||||
* - Referencia a componente que fue eliminado
|
||||
*
|
||||
* USO:
|
||||
* ```php
|
||||
* $component = $repository->findByName($name);
|
||||
*
|
||||
* if ($component === null) {
|
||||
* throw ComponentNotFoundException::withName($name);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @package ROITheme\Shared\Domain\Exceptions
|
||||
*/
|
||||
class ComponentNotFoundException extends \RuntimeException
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $message Mensaje de error
|
||||
* @param int $code Código de error (opcional)
|
||||
* @param \Throwable|null $previous Excepción anterior (opcional)
|
||||
*/
|
||||
public function __construct(
|
||||
string $message,
|
||||
int $code = 0,
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear excepción para componente no encontrado por nombre
|
||||
*
|
||||
* @param string $componentName
|
||||
* @return self
|
||||
*/
|
||||
public static function withName(string $componentName): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('Component "%s" not found', $componentName)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear excepción para componente no encontrado por ID
|
||||
*
|
||||
* @param int $componentId
|
||||
* @return self
|
||||
*/
|
||||
public static function withId(int $componentId): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('Component with ID %d not found', $componentId)
|
||||
);
|
||||
}
|
||||
}
|
||||
98
Shared/Domain/Exceptions/InvalidComponentException.php
Normal file
98
Shared/Domain/Exceptions/InvalidComponentException.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Exceptions;
|
||||
|
||||
/**
|
||||
* InvalidComponentException - Excepción de Dominio para componentes inválidos
|
||||
*
|
||||
* RESPONSABILIDAD: Representar errores de validación de reglas de negocio de componentes
|
||||
*
|
||||
* CUÁNDO LANZAR:
|
||||
* - Nombre de componente inválido (formato, longitud)
|
||||
* - Configuración inválida (CTA URL sin CTA text, colores mal formateados)
|
||||
* - Visibility inválida (combinaciones no permitidas)
|
||||
* - Content inválido (URLs mal formateadas, textos vacíos cuando requeridos)
|
||||
* - Cualquier violación de invariantes del dominio
|
||||
*
|
||||
* USO:
|
||||
* ```php
|
||||
* if (empty($name)) {
|
||||
* throw new InvalidComponentException('Component name cannot be empty');
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @package ROITheme\Shared\Domain\Exceptions
|
||||
*/
|
||||
class InvalidComponentException extends \DomainException
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $message Mensaje de error
|
||||
* @param int $code Código de error (opcional)
|
||||
* @param \Throwable|null $previous Excepción anterior (opcional)
|
||||
*/
|
||||
public function __construct(
|
||||
string $message,
|
||||
int $code = 0,
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear excepción para nombre inválido
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $reason
|
||||
* @return self
|
||||
*/
|
||||
public static function invalidName(string $name, string $reason): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('Invalid component name "%s": %s', $name, $reason)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear excepción para configuración inválida
|
||||
*
|
||||
* @param string $componentName
|
||||
* @param string $reason
|
||||
* @return self
|
||||
*/
|
||||
public static function invalidConfiguration(string $componentName, string $reason): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('Invalid configuration for component "%s": %s', $componentName, $reason)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear excepción para CTA inválido
|
||||
*
|
||||
* @param string $componentName
|
||||
* @return self
|
||||
*/
|
||||
public static function ctaUrlRequiresText(string $componentName): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('Component "%s" has CTA URL but missing CTA text', $componentName)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear excepción para color inválido
|
||||
*
|
||||
* @param string $colorKey
|
||||
* @param string $value
|
||||
* @return self
|
||||
*/
|
||||
public static function invalidColor(string $colorKey, string $value): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('Invalid color format for "%s": "%s". Expected hexadecimal (e.g., #000000)', $colorKey, $value)
|
||||
);
|
||||
}
|
||||
}
|
||||
127
Shared/Domain/README.md
Normal file
127
Shared/Domain/README.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Capa de Dominio - Shared (Fundación)
|
||||
|
||||
## Propósito
|
||||
|
||||
La capa de Dominio de `shared/` contiene la **lógica de negocio compartida** que utilizan todos los contextos
|
||||
(admin y public). Es la fundación del proyecto y no debe tener dependencias de frameworks, librerías externas, o
|
||||
capas superiores.
|
||||
|
||||
## Principios
|
||||
|
||||
1. **Sin dependencias externas**: No depende de WordPress, plugins, o librerías
|
||||
2. **Lógica pura**: Solo reglas de negocio y objetos de dominio
|
||||
3. **Inmutabilidad**: Los Value Objects son inmutables
|
||||
4. **Validación**: Los objetos se validan a sí mismos
|
||||
|
||||
## Estructura
|
||||
|
||||
```
|
||||
shared/Domain/
|
||||
├── ValueObjects/ # Value Objects compartidos (ComponentID, SettingValue, etc.)
|
||||
├── Exceptions/ # Excepciones de dominio (InvalidComponentException, etc.)
|
||||
└── Contracts/ # Interfaces de repositorios y servicios de dominio
|
||||
```
|
||||
|
||||
## Ejemplos de Uso
|
||||
|
||||
### Value Objects
|
||||
|
||||
Value Objects que representan conceptos del dominio:
|
||||
|
||||
```php
|
||||
namespace ROITheme\Shared\Domain\ValueObjects;
|
||||
|
||||
final class ComponentID
|
||||
{
|
||||
private int $value;
|
||||
|
||||
public function __construct(int $value)
|
||||
{
|
||||
if ($value <= 0) {
|
||||
throw new \InvalidArgumentException('Component ID must be positive');
|
||||
}
|
||||
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function value(): int
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function equals(ComponentID $other): bool
|
||||
{
|
||||
return $this->value === $other->value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Excepciones
|
||||
|
||||
Excepciones específicas del dominio:
|
||||
|
||||
```php
|
||||
namespace ROITheme\Shared\Domain\Exceptions;
|
||||
|
||||
class InvalidComponentException extends \DomainException
|
||||
{
|
||||
public static function withId(int $id): self
|
||||
{
|
||||
return new self("Component with ID {$id} is invalid");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Contracts (Interfaces)
|
||||
|
||||
Interfaces que definen comportamientos:
|
||||
|
||||
```php
|
||||
namespace ROITheme\Shared\Domain\Contracts;
|
||||
|
||||
interface ComponentRepositoryInterface
|
||||
{
|
||||
public function findById(ComponentID $id): ?Component;
|
||||
public function save(Component $component): void;
|
||||
}
|
||||
```
|
||||
|
||||
## Reglas de Dependencia
|
||||
|
||||
✅ **PUEDE** depender de:
|
||||
- Otros objetos dentro de `shared/Domain/`
|
||||
- SPL (Standard PHP Library)
|
||||
- Nada más
|
||||
|
||||
❌ **NO PUEDE** depender de:
|
||||
- `shared/Application/`
|
||||
- `shared/Infrastructure/`
|
||||
- `admin/` o `public/`
|
||||
- WordPress functions
|
||||
- Librerías externas
|
||||
|
||||
## Testing
|
||||
|
||||
Los objetos de esta capa se testean con **tests unitarios puros**, sin necesidad de WordPress:
|
||||
|
||||
```php
|
||||
// tests/Unit/Shared/Domain/ValueObjects/ComponentIDTest.php
|
||||
public function test_creates_valid_component_id()
|
||||
{
|
||||
$id = new ComponentID(123);
|
||||
$this->assertEquals(123, $id->value());
|
||||
}
|
||||
```
|
||||
|
||||
## Cuándo Agregar Código Aquí
|
||||
|
||||
Agrega código a `shared/Domain/` cuando:
|
||||
- Es lógica de negocio pura (sin WordPress)
|
||||
- Es compartido por admin/ y public/
|
||||
- Es un concepto fundamental del dominio
|
||||
- Necesita alta cohesión y bajo acoplamiento
|
||||
|
||||
No agregues aquí:
|
||||
- Código que depende de WordPress
|
||||
- Lógica específica de un solo contexto
|
||||
- Implementaciones concretas de servicios
|
||||
0
Shared/Domain/ValueObjects/.gitkeep
Normal file
0
Shared/Domain/ValueObjects/.gitkeep
Normal file
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' => []
|
||||
]);
|
||||
}
|
||||
}
|
||||
272
Shared/Domain/ValueObjects/ComponentContent.php
Normal file
272
Shared/Domain/ValueObjects/ComponentContent.php
Normal 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;
|
||||
}
|
||||
}
|
||||
169
Shared/Domain/ValueObjects/ComponentName.php
Normal file
169
Shared/Domain/ValueObjects/ComponentName.php
Normal 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);
|
||||
}
|
||||
}
|
||||
266
Shared/Domain/ValueObjects/ComponentVisibility.php
Normal file
266
Shared/Domain/ValueObjects/ComponentVisibility.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user