Migración completa a Clean Architecture con componentes funcionales

- Reorganización de estructura: Admin/, Public/, Shared/, Schemas/
- 12 componentes migrados: TopNotificationBar, Navbar, CtaLetsTalk, Hero,
  FeaturedImage, TableOfContents, CtaBoxSidebar, SocialShare, CtaPost,
  RelatedPost, ContactForm, Footer
- Panel de administración con tabs Bootstrap 5 funcionales
- Schemas JSON para configuración de componentes
- Renderers dinámicos con CSSGeneratorService (cero CSS hardcodeado)
- FormBuilders para UI admin con Design System consistente
- Fix: Bootstrap JS cargado en header para tabs funcionales
- Fix: buildTextInput maneja valores mixed (bool/string)
- Eliminación de estructura legacy (src/, admin/, assets/css/componente-*)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
FrankZamora
2025-11-25 21:20:06 -06:00
parent 90de6df77c
commit 0846a3bf03
224 changed files with 21670 additions and 17816 deletions

View File

@@ -0,0 +1,247 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Persistence\WordPress;
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\Exceptions\ComponentNotFoundException;
/**
* WordPressComponentRepository - Implementación con WordPress/MySQL
*
* RESPONSABILIDAD: Persistir y recuperar componentes desde wp_roi_theme_components
*
* DEPENDENCIAS EXTERNAS:
* - wpdb (WordPress database abstraction)
* - Tabla wp_roi_theme_components (creada en Fase 2)
*
* CONVERSIÓN:
* - Entity → Array → MySQL (al guardar)
* - MySQL → Array → Entity (al recuperar)
*
* @package ROITheme\Infrastructure\Persistence\WordPress
*/
final class WordPressComponentRepository implements ComponentRepositoryInterface
{
private string $tableName;
/**
* @param \wpdb $wpdb WordPress database object
*/
public function __construct(
private \wpdb $wpdb
) {
$this->tableName = $this->wpdb->prefix . 'roi_theme_components';
}
/**
* Guardar componente
*
* INSERT si no existe, UPDATE si existe
*
* @param Component $component
* @return Component Componente guardado (con timestamps actualizados)
*/
public function save(Component $component): Component
{
$componentName = $component->name()->value();
// Verificar si ya existe
$existing = $this->findByName($componentName);
$data = [
'component_name' => $componentName,
'configuration' => json_encode($component->configuration()->toArray()),
'visibility' => json_encode($component->visibility()->toArray()),
'is_enabled' => $component->isEnabled() ? 1 : 0,
'schema_version' => $component->schemaVersion(),
'updated_at' => current_time('mysql')
];
if ($existing === null) {
// INSERT
$data['created_at'] = current_time('mysql');
$this->wpdb->insert(
$this->tableName,
$data,
['%s', '%s', '%s', '%d', '%s', '%s', '%s']
);
} else {
// UPDATE
$this->wpdb->update(
$this->tableName,
$data,
['component_name' => $componentName],
['%s', '%s', '%s', '%d', '%s', '%s'],
['%s']
);
}
// Return the saved component by fetching it from the database
return $this->getByName($component->name());
}
/**
* Buscar componente por nombre
*
* @param ComponentName $name Nombre del componente
* @return Component|null Null si no existe
*/
public function findByName(ComponentName $name): ?Component
{
$sql = $this->wpdb->prepare(
"SELECT * FROM {$this->tableName} WHERE component_name = %s LIMIT 1",
$name->value()
);
$row = $this->wpdb->get_row($sql, ARRAY_A);
if ($row === null) {
return null;
}
return $this->rowToEntity($row);
}
/**
* Obtener componente por nombre (lanza excepción si no existe)
*
* @param ComponentName $name
* @return Component
* @throws ComponentNotFoundException
*/
public function getByName(ComponentName $name): Component
{
$component = $this->findByName($name);
if ($component === null) {
throw ComponentNotFoundException::withName($name->value());
}
return $component;
}
/**
* Obtener todos los componentes
*
* @return Component[]
*/
public function findAll(): array
{
$sql = "SELECT * FROM {$this->tableName} ORDER BY component_name ASC";
$rows = $this->wpdb->get_results($sql, ARRAY_A);
if (empty($rows)) {
return [];
}
return array_map(
fn($row) => $this->rowToEntity($row),
$rows
);
}
/**
* Eliminar componente
*
* @param ComponentName $name Nombre del componente
* @return bool True si eliminó exitosamente
*/
public function delete(ComponentName $name): bool
{
$result = $this->wpdb->delete(
$this->tableName,
['component_name' => $name->value()],
['%s']
);
return $result !== false;
}
/**
* Obtener componentes habilitados
*
* @return Component[]
*/
public function findEnabled(): array
{
$sql = "SELECT * FROM {$this->tableName} WHERE is_enabled = 1 ORDER BY component_name ASC";
$rows = $this->wpdb->get_results($sql, ARRAY_A);
if (empty($rows)) {
return [];
}
return array_map(
fn($row) => $this->rowToEntity($row),
$rows
);
}
/**
* Verificar si existe un componente con el nombre dado
*
* @param ComponentName $name
* @return bool
*/
public function exists(ComponentName $name): bool
{
return $this->findByName($name) !== null;
}
/**
* Obtener cantidad total de componentes
*
* @return int
*/
public function count(): int
{
$sql = "SELECT COUNT(*) FROM {$this->tableName}";
return (int) $this->wpdb->get_var($sql);
}
/**
* Obtener componentes por grupo de configuración
*
* @param string $group Grupo de configuración (visibility, content, styles, general)
* @return Component[]
*/
public function findByConfigGroup(string $group): array
{
// For now, return all components as we don't have a specific column for groups
// This would require additional schema design
return $this->findAll();
}
/**
* Convertir fila de BD a Entity
*
* @param array $row Fila de la base de datos
* @return Component
*/
private function rowToEntity(array $row): Component
{
// Decodificar JSON
$configuration = json_decode($row['configuration'], true) ?? [];
$visibility = json_decode($row['visibility'], true) ?? [];
// Crear Value Objects
$name = new ComponentName($row['component_name']);
$config = ComponentConfiguration::fromArray($configuration);
$vis = ComponentVisibility::fromArray($visibility);
// Crear Entity
return new Component(
$name,
$config,
$vis,
(bool) $row['is_enabled'],
$row['schema_version']
);
}
}

View File

@@ -0,0 +1,208 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Persistence\WordPress;
use ROITheme\Shared\Domain\Contracts\ComponentSettingsRepositoryInterface;
/**
* Implementaci<63>n de repositorio de configuraciones usando WordPress/MySQL
*
* Infrastructure Layer - WordPress specific
* Trabaja con la tabla wp_roi_theme_component_settings
*
* @package ROITheme\Shared\Infrastructure\Persistence\WordPress
*/
final class WordPressComponentSettingsRepository implements ComponentSettingsRepositoryInterface
{
private string $tableName;
public function __construct(
private \wpdb $wpdb
) {
$this->tableName = $this->wpdb->prefix . 'roi_theme_component_settings';
}
/**
* {@inheritDoc}
*/
public function getComponentSettings(string $componentName): array
{
$sql = $this->wpdb->prepare(
"SELECT group_name, attribute_name, attribute_value
FROM {$this->tableName}
WHERE component_name = %s
ORDER BY group_name, attribute_name",
$componentName
);
$rows = $this->wpdb->get_results($sql, ARRAY_A);
if (empty($rows)) {
return [];
}
// Agrupar por grupo
$settings = [];
foreach ($rows as $row) {
$groupName = $row['group_name'];
$attributeName = $row['attribute_name'];
$value = $row['attribute_value'];
// Convertir valores seg<65>n tipo
$value = $this->unserializeValue($value);
if (!isset($settings[$groupName])) {
$settings[$groupName] = [];
}
$settings[$groupName][$attributeName] = $value;
}
return $settings;
}
/**
* {@inheritDoc}
*/
public function saveComponentSettings(string $componentName, array $settings): int
{
$updated = 0;
foreach ($settings as $groupName => $attributes) {
foreach ($attributes as $attributeName => $value) {
if ($this->saveFieldValue($componentName, $groupName, $attributeName, $value)) {
$updated++;
}
}
}
return $updated;
}
/**
* {@inheritDoc}
*/
public function getFieldValue(string $componentName, string $groupName, string $attributeName): mixed
{
$sql = $this->wpdb->prepare(
"SELECT attribute_value
FROM {$this->tableName}
WHERE component_name = %s
AND group_name = %s
AND attribute_name = %s
LIMIT 1",
$componentName,
$groupName,
$attributeName
);
$value = $this->wpdb->get_var($sql);
if ($value === null) {
return null;
}
return $this->unserializeValue($value);
}
/**
* {@inheritDoc}
*/
public function saveFieldValue(string $componentName, string $groupName, string $attributeName, mixed $value): bool
{
// Serializar valor
$serializedValue = $this->serializeValue($value);
// Intentar actualizar
$result = $this->wpdb->update(
$this->tableName,
['attribute_value' => $serializedValue],
[
'component_name' => $componentName,
'group_name' => $groupName,
'attribute_name' => $attributeName
],
['%s'],
['%s', '%s', '%s']
);
return $result !== false;
}
/**
* {@inheritDoc}
*/
public function resetToDefaults(string $componentName, string $schemaPath): int
{
if (!file_exists($schemaPath)) {
return 0;
}
$schema = json_decode(file_get_contents($schemaPath), true);
if (!$schema || !isset($schema['groups'])) {
return 0;
}
$updated = 0;
// Iterar grupos y campos del schema
foreach ($schema['groups'] as $groupName => $group) {
foreach ($group['fields'] as $fieldName => $field) {
$defaultValue = $field['default'] ?? '';
if ($this->saveFieldValue($componentName, $groupName, $fieldName, $defaultValue)) {
$updated++;
}
}
}
return $updated;
}
/**
* Serializa un valor para guardarlo en la BD
*
* @param mixed $value
* @return string
*/
private function serializeValue(mixed $value): string
{
if (is_bool($value)) {
return $value ? '1' : '0';
}
if (is_array($value)) {
return json_encode($value);
}
return (string) $value;
}
/**
* Deserializa un valor desde la BD
*
* @param string $value
* @return mixed
*/
private function unserializeValue(string $value): mixed
{
// Intentar decodificar JSON
if (str_starts_with($value, '{') || str_starts_with($value, '[')) {
$decoded = json_decode($value, true);
if (json_last_error() === JSON_ERROR_NONE) {
return $decoded;
}
}
// Convertir booleanos
if ($value === '1' || $value === '0') {
return $value === '1';
}
// Devolver como est<73>
return $value;
}
}

View File

@@ -0,0 +1,202 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Persistence\WordPress;
use ROITheme\Shared\Domain\Contracts\ComponentDefaultsRepositoryInterface;
use ROITheme\Shared\Domain\ValueObjects\ComponentName;
use ROITheme\Shared\Domain\ValueObjects\ComponentConfiguration;
/**
* WordPressDefaultsRepository - Defaults/Schemas desde MySQL
*
* RESPONSABILIDAD: Gestionar schemas de componentes (estructura, validaciones, defaults)
*
* TABLA: wp_roi_theme_defaults
*
* @package ROITheme\Infrastructure\Persistence\WordPress
*/
final class WordPressDefaultsRepository implements ComponentDefaultsRepositoryInterface
{
private string $tableName;
public function __construct(
private \wpdb $wpdb
) {
$this->tableName = $this->wpdb->prefix . 'roi_theme_defaults';
}
/**
* 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
{
$sql = $this->wpdb->prepare(
"SELECT default_schema FROM {$this->tableName} WHERE component_name = %s LIMIT 1",
$name->value()
);
$result = $this->wpdb->get_var($sql);
if ($result === null) {
// Return empty configuration if no defaults found
return ComponentConfiguration::fromArray([]);
}
$schema = json_decode($result, true) ?? [];
return ComponentConfiguration::fromArray($schema);
}
/**
* 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
{
$existing = $this->exists($name);
$data = [
'component_name' => $name->value(),
'default_schema' => json_encode($configuration->all()),
'updated_at' => current_time('mysql')
];
if (!$existing) {
// INSERT
$data['created_at'] = current_time('mysql');
$this->wpdb->insert(
$this->tableName,
$data,
['%s', '%s', '%s', '%s']
);
} else {
// UPDATE
$this->wpdb->update(
$this->tableName,
$data,
['component_name' => $name->value()],
['%s', '%s', '%s'],
['%s']
);
}
}
/**
* Verificar si existen defaults para un componente
*
* @param ComponentName $name
* @return bool
*/
public function exists(ComponentName $name): bool
{
$sql = $this->wpdb->prepare(
"SELECT COUNT(*) FROM {$this->tableName} WHERE component_name = %s",
$name->value()
);
return (int) $this->wpdb->get_var($sql) > 0;
}
/**
* Obtener todos los defaults
*
* @return array<string, ComponentConfiguration> Array asociativo nombre => configuración
*/
public function findAll(): array
{
$sql = "SELECT component_name, default_schema FROM {$this->tableName}";
$rows = $this->wpdb->get_results($sql, ARRAY_A);
$defaults = [];
foreach ($rows as $row) {
$schema = json_decode($row['default_schema'], true) ?? [];
$defaults[$row['component_name']] = ComponentConfiguration::fromArray($schema);
}
return $defaults;
}
/**
* 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
{
$result = $this->wpdb->delete(
$this->tableName,
['component_name' => $name->value()],
['%s']
);
return $result !== false && $result > 0;
}
/**
* Obtener schema/defaults de un componente (método legacy)
*
* @deprecated Use getByName() instead
* @param string $componentName
* @return array|null Schema del componente o null si no existe
*/
public function find(string $componentName): ?array
{
$name = new ComponentName($componentName);
if (!$this->exists($name)) {
return null;
}
$config = $this->getByName($name);
return $config->all();
}
/**
* Guardar schema/defaults de un componente (método legacy)
*
* @deprecated Use save() instead
* @param string $componentName
* @param array $schema
* @return bool
*/
public function saveDefaults(string $componentName, array $schema): bool
{
$name = new ComponentName($componentName);
$config = ComponentConfiguration::fromArray($schema);
$this->save($name, $config);
return true;
}
/**
* Actualizar schema existente (método legacy)
*
* @deprecated Use save() instead
* @param string $componentName
* @param array $schema
* @return bool
*/
public function updateDefaults(string $componentName, array $schema): bool
{
return $this->saveDefaults($componentName, $schema);
}
/**
* Eliminar defaults de un componente (método legacy)
*
* @deprecated Use delete() instead
* @param string $componentName
* @return bool
*/
public function deleteDefaults(string $componentName): bool
{
$name = new ComponentName($componentName);
return $this->delete($name);
}
}