fix(structure): Correct case-sensitivity for Linux compatibility

Rename folders to match PHP PSR-4 autoloading conventions:
- schemas → Schemas
- shared → Shared
- Wordpress → WordPress (in all locations)

Fixes deployment issues on Linux servers where filesystem is case-sensitive.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
FrankZamora
2025-11-26 22:53:34 -06:00
parent a2548ab5c2
commit 90863cd8f5
92 changed files with 0 additions and 0 deletions

View File

View File

@@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Services;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
/**
* Class CSSGeneratorService
*
* Implementación concreta del generador de CSS.
* Convierte arrays de configuración de estilos en reglas CSS válidas y formateadas.
*
* Responsabilidades:
* - Generar string CSS a partir de selector y estilos
* - Convertir propiedades snake_case → kebab-case
* - Normalizar nombres de propiedades (text_color → color)
* - Formatear reglas CSS con indentación legible
* - Sanitizar valores para prevenir inyección
*
* @package ROITheme\Shared\Infrastructure\Services
*/
final class CSSGeneratorService implements CSSGeneratorInterface
{
/**
* Mapa de nombres de propiedades CSS normalizadas.
*
* @var array<string, string>
*/
private const PROPERTY_MAP = [
'text-color' => 'color',
'bg-color' => 'background-color',
];
/**
* {@inheritDoc}
*/
public function generate(string $selector, array $styles): string
{
if (empty($styles)) {
return '';
}
// Filtrar valores vacíos o null
$styles = $this->filterEmptyValues($styles);
if (empty($styles)) {
return '';
}
// Convertir array de estilos a propiedades CSS
$cssProperties = $this->buildCSSProperties($styles);
// Formatear regla CSS completa
return $this->formatCSSRule($selector, $cssProperties);
}
/**
* Filtra valores vacíos, null o que solo contienen espacios en blanco.
*
* @param array<string, mixed> $styles Array de estilos
* @return array<string, string> Array filtrado
*/
private function filterEmptyValues(array $styles): array
{
return array_filter(
$styles,
fn($value) => $value !== null && $value !== '' && trim((string)$value) !== ''
);
}
/**
* Convierte array de estilos a propiedades CSS formateadas.
*
* @param array<string, string> $styles Array de estilos
* @return array<int, string> Array de propiedades CSS formateadas
*/
private function buildCSSProperties(array $styles): array
{
$properties = [];
foreach ($styles as $property => $value) {
// Convertir snake_case a kebab-case
$cssProperty = $this->convertToKebabCase($property);
// Normalizar nombre de propiedad
$cssProperty = $this->normalizePropertyName($cssProperty);
// Sanitizar valor
$sanitizedValue = $this->sanitizeValue((string)$value);
// Agregar propiedad formateada
$properties[] = sprintf('%s: %s;', $cssProperty, $sanitizedValue);
}
return $properties;
}
/**
* Convierte snake_case a kebab-case.
*
* Ejemplos:
* - background_color → background-color
* - font_size → font-size
* - padding_top → padding-top
*
* @param string $property Nombre de propiedad en snake_case
* @return string Nombre de propiedad en kebab-case
*/
private function convertToKebabCase(string $property): string
{
return str_replace('_', '-', strtolower($property));
}
/**
* Normaliza nombres de propiedades CSS a su forma estándar.
*
* Mapea alias comunes a nombres de propiedades CSS estándar:
* - text-color → color
* - bg-color → background-color
*
* @param string $property Nombre de propiedad
* @return string Nombre de propiedad normalizado
*/
private function normalizePropertyName(string $property): string
{
return self::PROPERTY_MAP[$property] ?? $property;
}
/**
* Sanitiza valores CSS para prevenir inyección de código.
*
* Remueve tags HTML y caracteres potencialmente peligrosos,
* manteniendo valores CSS válidos como colores, unidades, etc.
*
* @param string $value Valor CSS sin sanitizar
* @return string Valor CSS sanitizado
*/
private function sanitizeValue(string $value): string
{
// Remover tags HTML
$value = strip_tags($value);
// Remover caracteres de control excepto espacios
$value = preg_replace('/[^\P{C}\s]/u', '', $value);
// Trim espacios
$value = trim($value);
return $value;
}
/**
* Formatea la regla CSS completa con selector y propiedades.
*
* Genera CSS con formato legible:
* ```css
* .selector {
* property: value;
* property2: value2;
* }
* ```
*
* @param string $selector Selector CSS
* @param array<int, string> $properties Array de propiedades formateadas
* @return string Regla CSS completa
*/
private function formatCSSRule(string $selector, array $properties): string
{
if (empty($properties)) {
return '';
}
return sprintf(
"%s {\n %s\n}",
$selector,
implode("\n ", $properties)
);
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Services;
use ROITheme\Component\Infrastructure\Persistence\Wordpress\WordPressComponentRepository;
use ROITheme\Component\Infrastructure\Persistence\Wordpress\WordPressDefaultsRepository;
/**
* CleanupService - Limpieza de componentes obsoletos
*
* RESPONSABILIDAD: Eliminar componentes que ya no existen en schema
*
* @package ROITheme\Infrastructure\Services
*/
final class CleanupService
{
public function __construct(
private WordPressComponentRepository $componentRepository,
private WordPressDefaultsRepository $defaultsRepository
) {}
/**
* Eliminar componentes que no tienen schema
*
* @return array ['removed' => array]
*/
public function removeObsolete(): array
{
// Obtener todos los componentes actuales
$components = $this->componentRepository->findAll();
// Obtener schemas disponibles
$schemas = $this->defaultsRepository->findAll();
$validNames = array_keys($schemas);
$removed = [];
foreach ($components as $component) {
$name = $component->name()->value();
// Si el componente no tiene schema, es obsoleto
if (!in_array($name, $validNames)) {
$this->componentRepository->delete($name);
$removed[] = $name;
}
}
return ['removed' => $removed];
}
}

View File

@@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Services;
use ROITheme\Shared\Infrastructure\Persistence\Wordpress\WordPressDefaultsRepository;
/**
* SchemaSyncService - Sincronizar schemas JSON → BD
*
* RESPONSABILIDAD: Leer schemas desde archivos JSON y sincronizar con BD
*
* FLUJO:
* 1. Leer archivos JSON de schemas
* 2. Comparar con BD actual
* 3. Agregar/Actualizar/Eliminar según diferencias
*
* @package ROITheme\Infrastructure\Services
*/
final class SchemaSyncService
{
private string $schemasPath;
public function __construct(
private WordPressDefaultsRepository $defaultsRepository,
string $schemasPath
) {
$this->schemasPath = rtrim($schemasPath, '/');
}
/**
* Sincronizar todos los schemas
*
* @return array ['success' => bool, 'data' => array]
*/
public function syncAll(): array
{
try {
// 1. Leer schemas desde JSON
$schemas = $this->readSchemasFromJson();
if (empty($schemas)) {
return [
'success' => false,
'error' => 'No schemas found in JSON files'
];
}
// 2. Obtener schemas actuales de BD
$currentSchemas = $this->defaultsRepository->findAll();
// 3. Determinar cambios
$schemaNames = array_keys($schemas);
$currentNames = array_keys($currentSchemas);
$toAdd = array_diff($schemaNames, $currentNames);
$toUpdate = array_intersect($schemaNames, $currentNames);
$toDelete = array_diff($currentNames, $schemaNames);
// 4. Aplicar cambios
$added = [];
$updated = [];
$deleted = [];
foreach ($toAdd as $name) {
$this->defaultsRepository->saveDefaults($name, $schemas[$name]);
$added[] = $name;
}
foreach ($toUpdate as $name) {
$this->defaultsRepository->updateDefaults($name, $schemas[$name]);
$updated[] = $name;
}
foreach ($toDelete as $name) {
$this->defaultsRepository->deleteDefaults($name);
$deleted[] = $name;
}
return [
'success' => true,
'data' => [
'added' => $added,
'updated' => $updated,
'deleted' => $deleted
]
];
} catch (\Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* Sincronizar un componente específico
*
* @param string $componentName
* @return array
*/
public function syncComponent(string $componentName): array
{
try {
$schemas = $this->readSchemasFromJson();
if (!isset($schemas[$componentName])) {
return [
'success' => false,
'error' => "Schema not found for: {$componentName}"
];
}
$this->defaultsRepository->saveDefaults($componentName, $schemas[$componentName]);
return [
'success' => true,
'data' => ['synced' => $componentName]
];
} catch (\Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* Leer schemas desde archivos JSON
*
* @return array Array asociativo [componentName => schema]
*/
private function readSchemasFromJson(): array
{
$schemas = [];
// Escanear directorio de schemas
$files = glob($this->schemasPath . '/*.json');
if (empty($files)) {
throw new \RuntimeException("No schema files found in: {$this->schemasPath}");
}
foreach ($files as $file) {
$content = file_get_contents($file);
$schema = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException('Invalid JSON in file ' . basename($file) . ': ' . json_last_error_msg());
}
if (!isset($schema['component_name'])) {
throw new \RuntimeException('Missing component_name in schema file: ' . basename($file));
}
$componentName = $schema['component_name'];
$schemas[$componentName] = $schema;
}
return $schemas;
}
}

View File

@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Services;
use ROITheme\Shared\Domain\Contracts\CacheServiceInterface;
/**
* WordPressCacheService - Cache con Transients API
*
* RESPONSABILIDAD: Gestionar cache de componentes
*
* IMPLEMENTACIÓN: WordPress Transients
* - get_transient()
* - set_transient()
* - delete_transient()
*
* VENTAJAS:
* - Compatible con object cache (Redis, Memcached)
* - Expiración automática
* - API simple
*
* @package ROITheme\Infrastructure\Services
*/
final class WordPressCacheService implements CacheServiceInterface
{
private const PREFIX = 'roi_theme_';
public function __construct(
private \wpdb $wpdb
) {}
/**
* Obtener valor del cache
*
* @param string $key Clave del cache
* @return mixed|null Valor o null si no existe/expiró
*/
public function get(string $key): mixed
{
$transient = get_transient($this->getFullKey($key));
// WordPress devuelve false si no existe
return $transient === false ? null : $transient;
}
/**
* Guardar valor en cache
*
* @param string $key Clave del cache
* @param mixed $value Valor a guardar
* @param int $expiration Tiempo de vida en segundos (default 1 hora)
* @return bool True si guardó exitosamente
*/
public function set(string $key, mixed $value, int $expiration = 3600): bool
{
return set_transient($this->getFullKey($key), $value, $expiration);
}
/**
* Eliminar entrada de cache
*
* @param string $key Clave del cache
* @return bool True si eliminó exitosamente
*/
public function delete(string $key): bool
{
return $this->invalidate($key);
}
/**
* Limpiar todo el cache
*
* @return bool
*/
public function flush(): bool
{
return $this->invalidateAll();
}
/**
* Invalidar (eliminar) entrada de cache
*
* @param string $key Clave del cache
* @return bool True si eliminó exitosamente
*/
public function invalidate(string $key): bool
{
return delete_transient($this->getFullKey($key));
}
/**
* Invalidar todo el cache de componentes
*
* @return bool
*/
public function invalidateAll(): bool
{
// Obtener todos los componentes
$components = $this->wpdb->get_col(
"SELECT DISTINCT component_name FROM {$this->wpdb->prefix}roi_theme_components"
);
$success = true;
foreach ($components as $componentName) {
$result = $this->invalidate("component_{$componentName}");
$success = $success && $result;
}
return $success;
}
/**
* Obtener clave completa con prefijo
*
* @param string $key
* @return string
*/
private function getFullKey(string $key): string
{
return self::PREFIX . $key;
}
}

View File

@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Services;
use ROITheme\Shared\Domain\Contracts\ValidationServiceInterface;
use ROITheme\Shared\Domain\ValidationResult;
use ROITheme\Component\Infrastructure\Persistence\Wordpress\WordPressDefaultsRepository;
/**
* WordPressValidationService - Validación contra schemas
*
* RESPONSABILIDAD: Validar y sanitizar datos de componentes
*
* ESTRATEGIA:
* 1. Obtener schema del componente desde BD
* 2. Validar estructura contra schema
* 3. Sanitizar datos usando funciones de WordPress
*
* @package ROITheme\Infrastructure\Services
*/
final class WordPressValidationService implements ValidationServiceInterface
{
public function __construct(
private WordPressDefaultsRepository $defaultsRepository
) {}
/**
* Validar datos contra schema
*
* @param array $data Datos a validar
* @param string $componentName Nombre del componente (para obtener schema)
* @return ValidationResult
*/
public function validate(array $data, string $componentName): ValidationResult
{
// 1. Obtener schema
$schema = $this->defaultsRepository->find($componentName);
if ($schema === null) {
return ValidationResult::failure([
"Schema not found for component: {$componentName}"
]);
}
// 2. Sanitizar datos primero
$sanitized = $this->sanitize($data, $componentName);
// 3. Validar estructura
$errors = [];
foreach ($sanitized as $groupName => $fields) {
// Verificar que el grupo existe en schema
if (!isset($schema[$groupName])) {
$errors[$groupName] = "Unknown group: {$groupName}";
continue;
}
// Validar cada campo del grupo
if (is_array($fields)) {
foreach ($fields as $key => $value) {
if (!isset($schema[$groupName][$key])) {
$errors["{$groupName}.{$key}"] = "Unknown field: {$groupName}.{$key}";
}
// Validaciones adicionales pueden agregarse aquí
// Por ejemplo, validar tipos, rangos, formatos, etc.
}
}
}
if (!empty($errors)) {
return ValidationResult::failure($errors);
}
return ValidationResult::success($sanitized);
}
/**
* Sanitizar datos recursivamente
*
* Usa funciones de WordPress según el tipo de dato
*
* @param array $data Datos a sanitizar
* @param string $componentName Nombre del componente
* @return array Datos sanitizados
*/
public function sanitize(array $data, string $componentName): array
{
$sanitized = [];
foreach ($data as $key => $value) {
if (is_array($value)) {
// Recursivo para arrays anidados
$sanitized[$key] = $this->sanitizeValue($value);
} else {
$sanitized[$key] = $this->sanitizeValue($value);
}
}
return $sanitized;
}
/**
* Sanitizar un valor individual
*
* @param mixed $value
* @return mixed
*/
private function sanitizeValue(mixed $value): mixed
{
if (is_array($value)) {
$sanitized = [];
foreach ($value as $k => $v) {
$sanitized[$k] = $this->sanitizeValue($v);
}
return $sanitized;
}
if (is_bool($value)) {
return (bool) $value;
}
if (is_numeric($value)) {
return is_float($value) ? (float) $value : (int) $value;
}
if (is_string($value) && filter_var($value, FILTER_VALIDATE_URL)) {
return esc_url_raw($value);
}
if (is_string($value)) {
return sanitize_text_field($value);
}
return $value;
}
/**
* Validar una URL
*
* @param string $url
* @return bool
*/
public function isValidUrl(string $url): bool
{
return filter_var($url, FILTER_VALIDATE_URL) !== false;
}
/**
* Validar un color hexadecimal
*
* @param string $color
* @return bool
*/
public function isValidColor(string $color): bool
{
return preg_match('/^#[0-9A-F]{6}$/i', $color) === 1;
}
/**
* Validar nombre de componente
*
* @param string $name
* @return bool
*/
public function isValidComponentName(string $name): bool
{
// Solo letras minúsculas, números y guiones bajos
return preg_match('/^[a-z0-9_]+$/', $name) === 1;
}
}