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

@@ -0,0 +1 @@

View File

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,427 @@
# Capa de Aplicación (Application Layer)
## 📋 Propósito
La **Capa de Aplicación** contiene los **Use Cases** que orquestan la lógica de negocio de la aplicación. Actúa como punto de entrada coordinando el flujo entre la UI/API y la Capa de Dominio.
Esta capa es el **corazón de la arquitectura limpia**, encapsulando las reglas de negocio específicas de la aplicación sin conocer detalles de implementación.
## 🎯 Responsabilidades
### ✅ QUÉ HACE
- **Orquestar flujo de la aplicación**: Coordina la secuencia de operaciones necesarias para completar un caso de uso
- **Coordinar llamadas entre capas**: Actúa como intermediario entre la presentación y el dominio
- **Transformar datos (DTOs)**: Utiliza Data Transfer Objects para encapsular entrada/salida
- **Manejar transacciones**: Coordina operaciones que deben ejecutarse de forma atómica
- **Validar entrada**: Verifica que los datos de entrada sean válidos antes de procesarlos
- **Gestionar cache**: Coordina estrategias de cache para optimizar rendimiento
### ❌ QUÉ NO HACE
- **NO contiene lógica de negocio**: La lógica de dominio vive en la Capa de Dominio
- **NO conoce detalles de implementación**: Solo depende de interfaces, nunca de implementaciones concretas
- **NO accede directamente a BD**: Usa repositorios a través de interfaces
- **NO maneja detalles de UI**: No conoce si la petición viene de REST, CLI o WordPress Admin
## 📁 Estructura
```
Application/
├── UseCases/
│ ├── SaveComponent/
│ │ ├── SaveComponentUseCase.php (Orquestador)
│ │ ├── SaveComponentRequest.php (DTO entrada)
│ │ └── SaveComponentResponse.php (DTO salida)
│ ├── GetComponent/
│ │ ├── GetComponentUseCase.php
│ │ ├── GetComponentRequest.php
│ │ └── GetComponentResponse.php
│ ├── SyncSchema/
│ │ ├── SyncSchemaUseCase.php
│ │ ├── SyncSchemaRequest.php
│ │ └── SyncSchemaResponse.php
│ └── DeleteComponent/
│ ├── DeleteComponentUseCase.php
│ ├── DeleteComponentRequest.php
│ └── DeleteComponentResponse.php
└── README.md (este archivo)
```
## 🔧 Use Cases Disponibles
### 1. SaveComponent
**Propósito**: Guardar o actualizar la configuración de un componente.
**Flujo**:
1. Validar datos de entrada
2. Crear/actualizar entidad Component
3. Persistir en repositorio
4. Invalidar cache
5. Retornar confirmación
**Ejemplo de uso**:
```php
use ROITheme\Shared\Application\UseCases\SaveComponent\SaveComponentUseCase;
use ROITheme\Shared\Application\UseCases\SaveComponent\SaveComponentRequest;
$useCase = new SaveComponentUseCase($repository, $validator, $cache);
$request = new SaveComponentRequest('top_bar', [
'background_color' => '#ffffff',
'text_color' => '#000000',
'show_logo' => true
]);
$response = $useCase->execute($request);
if ($response->isSuccess()) {
echo "Componente guardado exitosamente";
} else {
echo "Error: " . $response->getError();
}
```
### 2. GetComponent
**Propósito**: Obtener la configuración de un componente con cache-first strategy.
**Flujo**:
1. Verificar cache
2. Si existe en cache → retornar
3. Si no → consultar repositorio
4. Guardar en cache
5. Retornar datos
**Ejemplo de uso**:
```php
use ROITheme\Shared\Application\UseCases\GetComponent\GetComponentUseCase;
use ROITheme\Shared\Application\UseCases\GetComponent\GetComponentRequest;
$useCase = new GetComponentUseCase($repository, $cache);
$request = new GetComponentRequest('top_bar');
$response = $useCase->execute($request);
if ($response->isSuccess()) {
$data = $response->getData();
// Procesar datos del componente
} else {
echo "Error: " . $response->getError();
}
```
### 3. SyncSchema
**Propósito**: Sincronizar múltiples componentes desde un archivo JSON de schemas.
**Flujo**:
1. Leer archivo JSON
2. Validar formato
3. Procesar cada componente del schema
4. Identificar cambios (agregados/actualizados/eliminados)
5. Retornar resumen de cambios
**Ejemplo de uso**:
```php
use ROITheme\Shared\Application\UseCases\SyncSchema\SyncSchemaUseCase;
use ROITheme\Shared\Application\UseCases\SyncSchema\SyncSchemaRequest;
$useCase = new SyncSchemaUseCase($componentRepo, $defaultsRepo);
$request = new SyncSchemaRequest('/path/to/schemas.json');
$response = $useCase->execute($request);
if ($response->isSuccess()) {
echo $response->getSummary();
// Output: "Added: 2, Updated: 3, Deleted: 1"
} else {
foreach ($response->getErrors() as $error) {
echo "Error: {$error}\n";
}
}
```
### 4. DeleteComponent
**Propósito**: Eliminar un componente del sistema.
**Flujo**:
1. Verificar que el componente existe
2. Eliminar de repositorio
3. Invalidar cache
4. Retornar confirmación
**Ejemplo de uso**:
```php
use ROITheme\Shared\Application\UseCases\DeleteComponent\DeleteComponentUseCase;
use ROITheme\Shared\Application\UseCases\DeleteComponent\DeleteComponentRequest;
$useCase = new DeleteComponentUseCase($repository, $cache);
$request = new DeleteComponentRequest('old_component');
$response = $useCase->execute($request);
if ($response->isSuccess()) {
echo $response->getMessage();
} else {
echo "Error: " . $response->getError();
}
```
## 🏛️ Principios Arquitectónicos
### Regla de Dependencias
```
Application Layer
↓ (depende de)
Domain Layer (solo interfaces)
NO depende de Infrastructure Layer
```
La capa de aplicación **solo** puede depender de la capa de dominio, y únicamente de sus **interfaces**, nunca de implementaciones concretas.
### Dependency Inversion Principle
**✅ CORRECTO**:
```php
namespace ROITheme\Shared\Application\UseCases\SaveComponent;
use ROITheme\Shared\Domain\Contracts\ComponentRepositoryInterface;
use ROITheme\Shared\Domain\Contracts\ValidationServiceInterface;
final class SaveComponentUseCase
{
public function __construct(
private ComponentRepositoryInterface $repository, // ✅ Interfaz
private ValidationServiceInterface $validator, // ✅ Interfaz
private CacheServiceInterface $cache // ✅ Interfaz
) {}
}
```
**❌ INCORRECTO**:
```php
use ROITheme\Infrastructure\Persistence\WordPress\WordPressComponentRepository;
final class SaveComponentUseCase
{
public function __construct(
private WordPressComponentRepository $repository // ❌ Implementación concreta
) {}
}
```
### DTOs Inmutables
Todos los Request y Response utilizan `readonly class` para garantizar inmutabilidad:
```php
final readonly class SaveComponentRequest
{
public function __construct(
private string $componentName,
private array $data
) {}
// Solo getters, sin setters
public function getComponentName(): string
{
return $this->componentName;
}
}
```
### Factory Methods en Responses
Los responses utilizan factory methods para encapsular lógica de creación:
```php
final readonly class SaveComponentResponse
{
// Constructor privado
private function __construct(
private bool $success,
private ?string $error
) {}
// Factory methods públicos
public static function success(): self
{
return new self(true, null);
}
public static function failure(string $error): self
{
return new self(false, $error);
}
}
```
## 🧪 Testing
### Estrategia de Testing
Todos los Use Cases tienen tests unitarios con mocks de dependencias:
```php
namespace ROITheme\Tests\Unit\Application\UseCases;
use PHPUnit\Framework\TestCase;
use ROITheme\Shared\Application\UseCases\SaveComponent\SaveComponentUseCase;
use ROITheme\Shared\Domain\Contracts\ComponentRepositoryInterface;
use ROITheme\Shared\Domain\Contracts\ValidationServiceInterface;
class SaveComponentUseCaseTest extends TestCase
{
private ComponentRepositoryInterface $repository;
private ValidationServiceInterface $validator;
private SaveComponentUseCase $useCase;
protected function setUp(): void
{
$this->repository = $this->createMock(ComponentRepositoryInterface::class);
$this->validator = $this->createMock(ValidationServiceInterface::class);
$this->useCase = new SaveComponentUseCase(
$this->repository,
$this->validator,
$this->cache
);
}
public function test_saves_component_successfully(): void
{
// Test implementation
}
}
```
### Ubicación de Tests
```
tests/Unit/Application/UseCases/
├── SaveComponentUseCaseTest.php
├── GetComponentUseCaseTest.php
├── SyncSchemaUseCaseTest.php
└── DeleteComponentUseCaseTest.php
```
### Cobertura Esperada
- **Objetivo**: 95%
- **Mínimo aceptable**: 90%
### Ejecutar Tests
```bash
# Todos los tests de Application
cd _planeacion/roi-theme/_testing-suite
vendor/bin/phpunit tests/Unit/Application
# Test específico
vendor/bin/phpunit tests/Unit/Application/UseCases/SaveComponentUseCaseTest.php
# Con cobertura
vendor/bin/phpunit tests/Unit/Application --coverage-text
# Reporte HTML de cobertura
vendor/bin/phpunit tests/Unit/Application --coverage-html coverage
```
## 🔍 Requisitos Técnicos
Todos los archivos de la capa de aplicación deben cumplir:
### 1. Strict Types
```php
<?php
declare(strict_types=1); // ✅ Obligatorio en todos los archivos
```
### 2. Namespace Correcto
```php
namespace ROITheme\Shared\Application\UseCases\{UseCase};
```
Patrón: **Context-First**`ROITheme\Shared\Application\...`
### 3. PHPDoc Completo
```php
/**
* SaveComponentUseCase - Guardar/actualizar componente
*
* RESPONSABILIDAD: Orquestar guardado de componente
* ...
*
* @package ROITheme\Shared\Application\UseCases\SaveComponent
*/
final class SaveComponentUseCase
{
// ...
}
```
### 4. Type Hints Estrictos
```php
// ✅ CORRECTO
public function execute(SaveComponentRequest $request): SaveComponentResponse
{
// ...
}
// ❌ INCORRECTO
public function execute($request) // Sin type hints
{
// ...
}
```
## 📚 Referencias
- **Documentación de Fase**: `_planeacion/roi-theme/_MIGRACION-CLEAN-ARCHITECTURE/Fase-05/FASE-05-PLAN-IMPLEMENTACION.md`
- **Clean Architecture**: Robert C. Martin
- **Dependency Inversion Principle**: SOLID Principles
## ✅ Checklist de Validación
Para validar que un Use Case cumple con los estándares:
- [ ] Usa `declare(strict_types=1)`
- [ ] Namespace correcto: `ROITheme\Shared\Application\UseCases\{UseCase}`
- [ ] PHPDoc completo en clase
- [ ] Solo depende de interfaces del Domain
- [ ] Request es `final readonly class`
- [ ] Response es `final readonly class` con factory methods
- [ ] UseCase es `final class`
- [ ] Constructor injection de dependencias
- [ ] Método `execute()` con type hints
- [ ] Tests unitarios con >= 90% cobertura
- [ ] Sin dependencias de Infrastructure
## 🚀 Próximos Pasos
Después de completar la Capa de Aplicación:
1. **Fase 6**: Infrastructure Layer (Repositorios, Adaptadores)
2. **Fase 7**: API Layer (REST Endpoints)
3. **Fase 8**: Integration Tests
4. **Fase 9**: Migración gradual del código legacy
---
**Mantenedor**: ROI Theme Development Team
**Última actualización**: 2025-01-19
**Versión**: 1.0.0

View File

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Application\UseCases\DeleteComponent;
/**
* DeleteComponentRequest - DTO para eliminar componente
*
* @package ROITheme\Shared\Application\UseCases\DeleteComponent
*/
final readonly class DeleteComponentRequest
{
public function __construct(
private string $componentName
) {}
public function getComponentName(): string
{
return $this->componentName;
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Application\UseCases\DeleteComponent;
/**
* DeleteComponentResponse - DTO de respuesta para eliminación
*
* @package ROITheme\Shared\Application\UseCases\DeleteComponent
*/
final readonly class DeleteComponentResponse
{
private function __construct(
private bool $success,
private ?string $message,
private ?string $error
) {}
public function isSuccess(): bool
{
return $this->success;
}
public function getMessage(): ?string
{
return $this->message;
}
public function getError(): ?string
{
return $this->error;
}
public static function success(string $message): self
{
return new self(true, $message, null);
}
public static function failure(string $error): self
{
return new self(false, null, $error);
}
public function toArray(): array
{
return [
'success' => $this->success,
'message' => $this->message,
'error' => $this->error
];
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Application\UseCases\DeleteComponent;
use ROITheme\Shared\Domain\Contracts\ComponentRepositoryInterface;
use ROITheme\Shared\Domain\Contracts\CacheServiceInterface;
use ROITheme\Shared\Domain\ValueObjects\ComponentName;
/**
* DeleteComponentUseCase - Eliminar componente
*
* RESPONSABILIDAD: Orquestar eliminación de componente
*
* FLUJO:
* 1. Verificar que existe
* 2. Eliminar de BD
* 3. Invalidar cache
* 4. Retornar confirmación
*
* @package ROITheme\Shared\Application\UseCases\DeleteComponent
*/
final class DeleteComponentUseCase
{
public function __construct(
private ComponentRepositoryInterface $repository,
private CacheServiceInterface $cache
) {}
public function execute(DeleteComponentRequest $request): DeleteComponentResponse
{
try {
$componentNameString = $request->getComponentName();
$componentName = new ComponentName($componentNameString);
// 1. Verificar que existe
$component = $this->repository->findByName($componentName);
if ($component === null) {
return DeleteComponentResponse::failure(
"Component '{$componentNameString}' not found"
);
}
// 2. Eliminar
$deleted = $this->repository->delete($componentName);
if (!$deleted) {
return DeleteComponentResponse::failure(
"Failed to delete component '{$componentNameString}'"
);
}
// 3. Invalidar cache
$this->cache->delete("component_{$componentNameString}");
// 4. Retornar éxito
return DeleteComponentResponse::success(
"Component '{$componentNameString}' deleted successfully"
);
} catch (\Exception $e) {
return DeleteComponentResponse::failure(
'Unexpected error: ' . $e->getMessage()
);
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Application\UseCases\GetComponent;
/**
* GetComponentRequest - DTO de entrada para obtener componente
*
* RESPONSABILIDAD: Encapsular el nombre del componente a obtener
*
* USO:
* ```php
* $request = new GetComponentRequest('top_bar');
* ```
*
* @package ROITheme\Shared\Application\UseCases\GetComponent
*/
final readonly class GetComponentRequest
{
/**
* @param string $componentName Nombre del componente a obtener
*/
public function __construct(
private string $componentName
) {}
/**
* Obtener nombre del componente
*
* @return string
*/
public function getComponentName(): string
{
return $this->componentName;
}
/**
* Factory method: Crear desde string
*
* @param string $componentName
* @return self
*/
public static function fromString(string $componentName): self
{
return new self($componentName);
}
}

View File

@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Application\UseCases\GetComponent;
/**
* GetComponentResponse - DTO de salida para obtener componente
*
* RESPONSABILIDAD: Encapsular resultado de obtener un componente
*
* PATRÓN: Success/Failure
* - Éxito: success=true, data contiene el componente
* - Fallo: success=false, error contiene mensaje de error
*
* USO:
* ```php
* $response = GetComponentResponse::success($componentData);
* if ($response->isSuccess()) {
* $data = $response->getData();
* }
* ```
*
* @package ROITheme\Shared\Application\UseCases\GetComponent
*/
final readonly class GetComponentResponse
{
/**
* Constructor privado - usar factory methods
*
* @param bool $success Indica si la operación fue exitosa
* @param mixed $data Datos del componente (solo si success=true)
* @param string|null $error Mensaje de error (solo si success=false)
*/
private function __construct(
private bool $success,
private mixed $data,
private ?string $error
) {}
/**
* Verificar si la operación fue exitosa
*
* @return bool
*/
public function isSuccess(): bool
{
return $this->success;
}
/**
* Obtener datos del componente
*
* Solo válido si isSuccess() === true
*
* @return mixed
*/
public function getData(): mixed
{
return $this->data;
}
/**
* Obtener mensaje de error
*
* Solo válido si isSuccess() === false
*
* @return string|null
*/
public function getError(): ?string
{
return $this->error;
}
/**
* Factory method: Crear respuesta exitosa
*
* @param mixed $data Datos del componente
* @return self
*/
public static function success(mixed $data): self
{
return new self(true, $data, null);
}
/**
* Factory method: Crear respuesta de fallo
*
* @param string $error Mensaje de error
* @return self
*/
public static function failure(string $error): self
{
return new self(false, null, $error);
}
/**
* Convertir a array para serialización
*
* @return array
*/
public function toArray(): array
{
return [
'success' => $this->success,
'data' => $this->data,
'error' => $this->error
];
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Application\UseCases\GetComponent;
use ROITheme\Shared\Domain\Contracts\ComponentRepositoryInterface;
use ROITheme\Shared\Domain\Contracts\CacheServiceInterface;
use ROITheme\Shared\Domain\Exceptions\ComponentNotFoundException;
use ROITheme\Shared\Domain\ValueObjects\ComponentName;
/**
* GetComponentUseCase - Caso de Uso para obtener componente
*
* RESPONSABILIDAD: Orquestar la lógica de obtener un componente
*
* FLUJO:
* 1. Intentar obtener del cache
* 2. Si no está en cache, obtener del repositorio
* 3. Si no existe, lanzar excepción
* 4. Guardar en cache
* 5. Retornar respuesta
*
* OPTIMIZACIÓN:
* - Cache-first strategy para reducir queries a BD
* - TTL de 1 hora en cache
*
* USO:
* ```php
* $useCase = new GetComponentUseCase($repository, $cache);
* $request = new GetComponentRequest('top_bar');
* $response = $useCase->execute($request);
* ```
*
* @package ROITheme\Shared\Application\UseCases\GetComponent
*/
final class GetComponentUseCase
{
private const CACHE_TTL = 3600; // 1 hora
/**
* @param ComponentRepositoryInterface $repository Repositorio de componentes
* @param CacheServiceInterface $cache Servicio de cache
*/
public function __construct(
private ComponentRepositoryInterface $repository,
private CacheServiceInterface $cache
) {}
/**
* Ejecutar Use Case
*
* @param GetComponentRequest $request Datos de entrada
* @return GetComponentResponse Respuesta (éxito o fallo)
*/
public function execute(GetComponentRequest $request): GetComponentResponse
{
try {
$componentNameString = $request->getComponentName();
$componentName = new ComponentName($componentNameString);
$cacheKey = $this->getCacheKey($componentNameString);
// 1. Intentar obtener del cache
$cached = $this->cache->get($cacheKey);
if ($cached !== null) {
return GetComponentResponse::success($cached);
}
// 2. Si no está en cache, obtener del repositorio
$component = $this->repository->findByName($componentName);
if ($component === null) {
throw new ComponentNotFoundException(
"Component '{$componentNameString}' not found"
);
}
$data = $component->toArray();
// 3. Guardar en cache
$this->cache->set($cacheKey, $data, self::CACHE_TTL);
// 4. Retornar respuesta exitosa
return GetComponentResponse::success($data);
} catch (ComponentNotFoundException $e) {
return GetComponentResponse::failure($e->getMessage());
} catch (\Exception $e) {
return GetComponentResponse::failure(
'Unexpected error: ' . $e->getMessage()
);
}
}
/**
* Generar key de cache para componente
*
* @param string $componentName
* @return string
*/
private function getCacheKey(string $componentName): string
{
return "component_{$componentName}";
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Application\UseCases\GetComponentSettings;
use ROITheme\Shared\Domain\Contracts\ComponentSettingsRepositoryInterface;
/**
* Caso de uso para obtener las configuraciones de un componente
*
* Application Layer - Orquesta la l<>gica de negocio
*
* @package ROITheme\Shared\Application\UseCases\GetComponentSettings
*/
final class GetComponentSettingsUseCase
{
public function __construct(
private ComponentSettingsRepositoryInterface $repository
) {
}
/**
* Ejecuta el caso de uso
*
* @param string $componentName Nombre del componente
* @return array<string, array<string, mixed>> Configuraciones agrupadas por grupo
*/
public function execute(string $componentName): array
{
// Validar entrada
if (empty($componentName)) {
return [];
}
// Obtener configuraciones del repositorio
$settings = $this->repository->getComponentSettings($componentName);
// Si no hay configuraciones, devolver array vac<61>o
if (empty($settings)) {
return [];
}
return $settings;
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Application\UseCases\SaveComponent;
/**
* SaveComponentRequest - DTO de entrada para guardar componente
*
* RESPONSABILIDAD: Encapsular los datos de entrada para el Use Case de guardar un componente
*
* CARACTER<45>STICAS:
* - Inmutable
* - Sin l<>gica de negocio
* - Validaci<63>n b<>sica de tipos (PHP har<61> type checking)
*
* USO:
* ```php
* $request = new SaveComponentRequest('top_bar', [
* 'configuration' => ['content' => ['message_text' => 'Welcome']],
* 'visibility' => ['desktop' => true, 'mobile' => true],
* 'is_enabled' => true
* ]);
* ```
*
* @package ROITheme\Shared\Application\UseCases\SaveComponent
*/
final readonly class SaveComponentRequest
{
/**
* @param string $componentName Nombre del componente a guardar (e.g., 'top_bar', 'footer_cta')
* @param array $data Datos del componente (configuration, visibility, is_enabled, schema_version)
*/
public function __construct(
private string $componentName,
private array $data
) {}
/**
* Obtener nombre del componente
*
* @return string
*/
public function getComponentName(): string
{
return $this->componentName;
}
/**
* Obtener datos del componente
*
* @return array
*/
public function getData(): array
{
return $this->data;
}
/**
* Factory method: Crear desde array
*
* <20>til para crear desde datos POST/JSON
*
* @param array $data Array con keys 'component_name' y 'data'
* @return self
*/
public static function fromArray(array $data): self
{
return new self(
$data['component_name'] ?? '',
$data['data'] ?? []
);
}
}

View File

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Application\UseCases\SaveComponent;
/**
* SaveComponentResponse - DTO de salida para guardar componente
*
* RESPONSABILIDAD: Encapsular el resultado del Use Case (<28>xito o fallo)
*
* PATR<54>N: Success/Failure
* - <20>xito: success=true, data contiene el componente guardado
* - Fallo: success=false, errors contiene array de errores
*
* CARACTER<45>STICAS:
* - Inmutable
* - Constructor privado (usar factory methods)
* - Factory methods: success() y failure()
*
* USO:
* ```php
* // <20>xito
* $response = SaveComponentResponse::success(['name' => 'top_bar', ...]);
* if ($response->isSuccess()) {
* $data = $response->getData();
* }
*
* // Fallo
* $response = SaveComponentResponse::failure(['Error de validaci<63>n']);
* if (!$response->isSuccess()) {
* $errors = $response->getErrors();
* }
* ```
*
* @package ROITheme\Shared\Application\UseCases\SaveComponent
*/
final readonly class SaveComponentResponse
{
/**
* Constructor privado - usar factory methods
*
* @param bool $success Indica si la operaci<63>n fue exitosa
* @param mixed $data Datos del componente guardado (solo si success=true)
* @param array|null $errors Array de errores (solo si success=false)
*/
private function __construct(
private bool $success,
private mixed $data,
private ?array $errors
) {}
/**
* Verificar si la operaci<63>n fue exitosa
*
* @return bool
*/
public function isSuccess(): bool
{
return $this->success;
}
/**
* Obtener datos del componente guardado
*
* Solo v<>lido si isSuccess() === true
*
* @return mixed
*/
public function getData(): mixed
{
return $this->data;
}
/**
* Obtener errores
*
* Solo v<>lido si isSuccess() === false
*
* @return array|null
*/
public function getErrors(): ?array
{
return $this->errors;
}
/**
* Factory method: Crear respuesta exitosa
*
* @param mixed $data Datos del componente guardado
* @return self
*/
public static function success(mixed $data): self
{
return new self(true, $data, null);
}
/**
* Factory method: Crear respuesta de fallo
*
* @param array $errors Array de mensajes de error
* @return self
*/
public static function failure(array $errors): self
{
return new self(false, null, $errors);
}
/**
* Convertir a array para serializaci<63>n (JSON, etc.)
*
* @return array
*/
public function toArray(): array
{
return [
'success' => $this->success,
'data' => $this->data,
'errors' => $this->errors
];
}
}

View File

@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Application\UseCases\SaveComponent;
use ROITheme\Shared\Domain\Entities\Component;
use ROITheme\Shared\Domain\ValueObjects\ComponentName;
use ROITheme\Shared\Domain\ValueObjects\ComponentConfiguration;
use ROITheme\Shared\Domain\ValueObjects\ComponentVisibility;
use ROITheme\Shared\Domain\Contracts\ComponentRepositoryInterface;
use ROITheme\Shared\Domain\Contracts\ValidationServiceInterface;
use ROITheme\Shared\Domain\Contracts\CacheServiceInterface;
use ROITheme\Shared\Domain\Exceptions\InvalidComponentException;
/**
* SaveComponentUseCase - Caso de Uso para guardar componente
*
* RESPONSABILIDAD: Orquestar la lógica de guardar un componente
*
* FLUJO:
* 1. Validar datos contra schema
* 2. Obtener datos sanitizados del resultado de validación
* 3. Crear entidad de dominio (Component)
* 4. Persistir en repositorio
* 5. Invalidar cache
* 6. Retornar respuesta
*
* PRINCIPIOS APLICADOS:
* - Single Responsibility: Solo orquesta, no tiene lógica de negocio
* - Dependency Inversion: Depende de interfaces, no implementaciones
* - Open/Closed: Extensible via cambio de implementaciones
*
* USO:
* ```php
* $useCase = new SaveComponentUseCase($repository, $validator, $cache);
* $request = new SaveComponentRequest('top_bar', $data);
* $response = $useCase->execute($request);
*
* if ($response->isSuccess()) {
* // Éxito
* }
* ```
*
* @package ROITheme\Shared\Application\UseCases\SaveComponent
*/
final class SaveComponentUseCase
{
/**
* @param ComponentRepositoryInterface $repository Repositorio de componentes
* @param ValidationServiceInterface $validator Servicio de validación
* @param CacheServiceInterface $cache Servicio de cache
*/
public function __construct(
private ComponentRepositoryInterface $repository,
private ValidationServiceInterface $validator,
private CacheServiceInterface $cache
) {}
/**
* Ejecutar Use Case
*
* @param SaveComponentRequest $request Datos de entrada
* @return SaveComponentResponse Respuesta (éxito o fallo)
*/
public function execute(SaveComponentRequest $request): SaveComponentResponse
{
try {
// 1. Validar datos contra schema
$validationResult = $this->validator->validate(
$request->getData(),
$request->getComponentName()
);
if (!$validationResult->isValid()) {
return SaveComponentResponse::failure($validationResult->getErrors());
}
// 2. Obtener datos sanitizados del resultado de validación
$sanitized = $validationResult->getSanitizedData();
// 3. Crear entidad de dominio
$component = $this->createComponent($request->getComponentName(), $sanitized);
// 4. Persistir (save() retorna Component guardado)
$savedComponent = $this->repository->save($component);
// 5. Invalidar cache
$this->cache->delete("component_{$request->getComponentName()}");
// 6. Retornar respuesta exitosa
return SaveComponentResponse::success($savedComponent->toArray());
} catch (InvalidComponentException $e) {
// Errores de dominio (validación de reglas de negocio)
return SaveComponentResponse::failure([$e->getMessage()]);
} catch (\Exception $e) {
// Errores inesperados
return SaveComponentResponse::failure([
'Unexpected error: ' . $e->getMessage()
]);
}
}
/**
* Crear entidad Component desde datos validados
*
* @param string $name Nombre del componente
* @param array $data Datos validados y sanitizados
* @return Component
* @throws InvalidComponentException Si los datos no cumplen invariantes
*/
private function createComponent(string $name, array $data): Component
{
// Extraer o usar valores por defecto
$isEnabled = $data['is_enabled'] ?? true;
$schemaVersion = $data['schema_version'] ?? '1.0.0';
// Extraer grupos de configuración (visibility, content, styles, general)
$configData = [
'visibility' => $data['visibility'] ?? [],
'content' => $data['content'] ?? [],
'styles' => $data['styles'] ?? [],
'general' => $data['general'] ?? []
];
// Los datos de visibility se manejan aparte en ComponentVisibility
$visibilityData = $data['visibility'] ?? [];
// Crear Value Objects
$componentName = new ComponentName($name);
$configuration = ComponentConfiguration::fromArray($configData);
$visibility = ComponentVisibility::fromArray($visibilityData);
// Crear entidad Component
return new Component(
$componentName,
$configuration,
$visibility,
$isEnabled,
$schemaVersion
);
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Application\UseCases\SaveComponentSettings;
use ROITheme\Shared\Domain\Contracts\ComponentSettingsRepositoryInterface;
/**
* Caso de uso para guardar las configuraciones de un componente
*
* Application Layer - Orquesta la l<>gica de negocio
*
* @package ROITheme\Shared\Application\UseCases\SaveComponentSettings
*/
final class SaveComponentSettingsUseCase
{
public function __construct(
private ComponentSettingsRepositoryInterface $repository
) {
}
/**
* Ejecuta el caso de uso
*
* @param string $componentName Nombre del componente
* @param array<string, array<string, mixed>> $settings Configuraciones a guardar
* @return int N<>mero de campos actualizados
*/
public function execute(string $componentName, array $settings): int
{
// Validar entrada
if (empty($componentName) || empty($settings)) {
return 0;
}
// Guardar configuraciones usando el repositorio
$updated = $this->repository->saveComponentSettings($componentName, $settings);
return $updated;
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Application\UseCases\SyncSchema;
/**
* SyncSchemaRequest - DTO de entrada para sincronizar schemas
*
* RESPONSABILIDAD: Encapsular ruta del archivo JSON de schemas
*
* USO:
* ```php
* $request = new SyncSchemaRequest('/path/to/schemas.json');
* ```
*
* @package ROITheme\Shared\Application\UseCases\SyncSchema
*/
final readonly class SyncSchemaRequest
{
/**
* @param string $schemaFilePath Ruta al archivo JSON de schemas
*/
public function __construct(
private string $schemaFilePath
) {}
/**
* Obtener ruta del archivo de schemas
*
* @return string
*/
public function getSchemaFilePath(): string
{
return $this->schemaFilePath;
}
}

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Application\UseCases\SyncSchema;
/**
* SyncSchemaResponse - DTO de salida para sincronizar schemas
*
* RESPONSABILIDAD: Encapsular resultado de sincronización
*
* DATOS INCLUIDOS:
* - success: bool
* - componentsAdded: array de nombres agregados
* - componentsUpdated: array de nombres actualizados
* - componentsDeleted: array de nombres eliminados
* - errors: array de errores
*
* @package ROITheme\Shared\Application\UseCases\SyncSchema
*/
final readonly class SyncSchemaResponse
{
/**
* @param bool $success Indica si la sincronización fue exitosa
* @param array $componentsAdded Componentes agregados
* @param array $componentsUpdated Componentes actualizados
* @param array $componentsDeleted Componentes eliminados
* @param array $errors Errores ocurridos
*/
private function __construct(
private bool $success,
private array $componentsAdded,
private array $componentsUpdated,
private array $componentsDeleted,
private array $errors
) {}
public function isSuccess(): bool
{
return $this->success;
}
public function getComponentsAdded(): array
{
return $this->componentsAdded;
}
public function getComponentsUpdated(): array
{
return $this->componentsUpdated;
}
public function getComponentsDeleted(): array
{
return $this->componentsDeleted;
}
public function getErrors(): array
{
return $this->errors;
}
/**
* Factory method: Sincronización exitosa
*/
public static function success(
array $added,
array $updated,
array $deleted
): self {
return new self(true, $added, $updated, $deleted, []);
}
/**
* Factory method: Sincronización con errores
*/
public static function failure(array $errors): self
{
return new self(false, [], [], [], $errors);
}
/**
* Convertir a array
*/
public function toArray(): array
{
return [
'success' => $this->success,
'components_added' => $this->componentsAdded,
'components_updated' => $this->componentsUpdated,
'components_deleted' => $this->componentsDeleted,
'errors' => $this->errors
];
}
/**
* Obtener resumen de cambios
*/
public function getSummary(): string
{
$added = count($this->componentsAdded);
$updated = count($this->componentsUpdated);
$deleted = count($this->componentsDeleted);
return sprintf(
'Added: %d, Updated: %d, Deleted: %d',
$added,
$updated,
$deleted
);
}
}

View File

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Application\UseCases\SyncSchema;
use ROITheme\Shared\Domain\Contracts\ComponentRepositoryInterface;
use ROITheme\Shared\Domain\Contracts\DefaultRepositoryInterface;
use ROITheme\Shared\Domain\ValueObjects\ComponentName;
use ROITheme\Shared\Domain\ValueObjects\ComponentConfiguration;
/**
* SyncSchemaUseCase - Sincronizar schemas JSON → BD
*
* RESPONSABILIDAD: Orquestar sincronización de componentes desde archivo JSON
*
* FLUJO:
* 1. Leer archivo JSON
* 2. Validar estructura
* 3. Comparar con BD (agregar/actualizar/eliminar)
* 4. Persistir cambios
* 5. Retornar resumen
*
* @package ROITheme\Shared\Application\UseCases\SyncSchema
*/
final class SyncSchemaUseCase
{
public function __construct(
private ComponentRepositoryInterface $componentRepository,
private DefaultRepositoryInterface $defaultsRepository
) {}
/**
* Ejecutar sincronización
*/
public function execute(SyncSchemaRequest $request): SyncSchemaResponse
{
try {
// 1. Leer y parsear JSON
$schemas = $this->readSchemas($request->getSchemaFilePath());
if (empty($schemas)) {
return SyncSchemaResponse::failure(['No schemas found in file']);
}
// 2. Obtener componentes actuales de BD
$currentComponents = $this->componentRepository->findAll();
$currentNames = array_map(
fn($c) => $c->name()->value(),
$currentComponents
);
$schemaNames = array_keys($schemas);
// 3. Determinar cambios
$toAdd = array_diff($schemaNames, $currentNames);
$toUpdate = array_intersect($schemaNames, $currentNames);
$toDelete = array_diff($currentNames, $schemaNames);
// 4. Aplicar cambios
$added = $this->addComponents($toAdd, $schemas);
$updated = $this->updateComponents($toUpdate, $schemas);
$deleted = $this->deleteComponents($toDelete);
// 5. Retornar resumen
return SyncSchemaResponse::success($added, $updated, $deleted);
} catch (\Exception $e) {
return SyncSchemaResponse::failure([$e->getMessage()]);
}
}
/**
* Leer schemas desde archivo JSON
*/
private function readSchemas(string $filePath): array
{
if (!file_exists($filePath)) {
throw new \RuntimeException("Schema file not found: {$filePath}");
}
$content = file_get_contents($filePath);
$schemas = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException('Invalid JSON: ' . json_last_error_msg());
}
return $schemas;
}
/**
* Agregar nuevos componentes
*/
private function addComponents(array $names, array $schemas): array
{
$added = [];
foreach ($names as $name) {
$schema = $schemas[$name];
$componentName = new ComponentName($name);
$configuration = ComponentConfiguration::fromArray($schema);
$this->defaultsRepository->save($componentName, $configuration);
$added[] = $name;
}
return $added;
}
/**
* Actualizar componentes existentes
*/
private function updateComponents(array $names, array $schemas): array
{
$updated = [];
foreach ($names as $name) {
$schema = $schemas[$name];
$componentName = new ComponentName($name);
$configuration = ComponentConfiguration::fromArray($schema);
$this->defaultsRepository->save($componentName, $configuration);
$updated[] = $name;
}
return $updated;
}
/**
* Eliminar componentes obsoletos
*/
private function deleteComponents(array $names): array
{
$deleted = [];
foreach ($names as $name) {
$this->defaultsRepository->deleteDefaults($name);
$deleted[] = $name;
}
return $deleted;
}
}