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:
@@ -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']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user