refactor(admin): Migrate AdminAjaxHandler to Clean Architecture

- Move AdminAjaxHandler to Admin/Shared/Infrastructure/Api/Wordpress/
- Create FieldMapperInterface for decentralized field mapping
- Create FieldMapperRegistry for module discovery
- Create FieldMapperProvider for auto-registration of 12 mappers
- Add FieldMappers for all components:
  - ContactFormFieldMapper (46 fields)
  - CtaBoxSidebarFieldMapper (32 fields)
  - CtaLetsTalkFieldMapper
  - CtaPostFieldMapper
  - FeaturedImageFieldMapper (15 fields)
  - FooterFieldMapper (31 fields)
  - HeroFieldMapper
  - NavbarFieldMapper
  - RelatedPostFieldMapper (34 fields)
  - SocialShareFieldMapper
  - TableOfContentsFieldMapper
  - TopNotificationBarFieldMapper (17 fields)
- Update functions.php bootstrap with FieldMapperProvider
- AdminAjaxHandler reduced from ~700 to 145 lines
- Follows SRP, OCP, DIP principles

BACKUP BEFORE: Removing CTA A/B Testing legacy system

🤖 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 20:18:55 -06:00
parent 1a4d9d8c08
commit 4f11c2c312
19 changed files with 1199 additions and 703 deletions

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\Shared\Domain\Contracts;
/**
* Contrato para mapeo de campos de formulario a atributos de BD
*
* RESPONSABILIDAD:
* - Definir el mapeo de field IDs a grupos/atributos
* - Cada modulo implementa su propio mapper
*
* PRINCIPIOS:
* - ISP: Interfaz pequena (2 metodos)
* - DIP: Capas superiores dependen de esta abstraccion
*/
interface FieldMapperInterface
{
/**
* Retorna el nombre del componente que mapea
*
* @return string Nombre en kebab-case (ej: 'cta-box-sidebar')
*/
public function getComponentName(): string;
/**
* Retorna el mapeo de field IDs a grupo/atributo
*
* @return array<string, array{group: string, attribute: string}>
*
* Ejemplo:
* [
* 'ctaTitle' => ['group' => 'content', 'attribute' => 'title'],
* 'ctaEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
* ]
*/
public function getFieldMapping(): array;
}

View File

@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\Shared\Infrastructure\Api\Wordpress;
use ROITheme\Shared\Application\UseCases\SaveComponentSettings\SaveComponentSettingsUseCase;
use ROITheme\Admin\Shared\Infrastructure\FieldMapping\FieldMapperRegistry;
/**
* Handler para peticiones AJAX del panel de administracion
*
* RESPONSABILIDAD:
* - Manejar HTTP (request/response)
* - Delegar mapeo a FieldMapperRegistry
* - NO contiene logica de mapeo
*
* PRINCIPIOS:
* - SRP: Solo maneja HTTP
* - OCP: Nuevos componentes no requieren modificar esta clase
* - DIP: Depende de abstracciones (FieldMapperRegistry)
*/
final class AdminAjaxHandler
{
public function __construct(
private readonly ?SaveComponentSettingsUseCase $saveComponentSettingsUseCase = null,
private readonly ?FieldMapperRegistry $fieldMapperRegistry = null
) {}
public function register(): void
{
add_action('wp_ajax_roi_save_component_settings', [$this, 'saveComponentSettings']);
add_action('wp_ajax_roi_reset_component_defaults', [$this, 'resetComponentDefaults']);
}
public function saveComponentSettings(): void
{
check_ajax_referer('roi_admin_dashboard', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'No tienes permisos para realizar esta accion.']);
}
$component = sanitize_text_field($_POST['component'] ?? '');
$settings = json_decode(stripslashes($_POST['settings'] ?? '{}'), true);
if (empty($component) || empty($settings)) {
wp_send_json_error(['message' => 'Datos incompletos.']);
}
// Obtener mapper del modulo correspondiente
if ($this->fieldMapperRegistry === null || !$this->fieldMapperRegistry->hasMapper($component)) {
wp_send_json_error([
'message' => "No existe mapper para el componente: {$component}"
]);
}
$mapper = $this->fieldMapperRegistry->getMapper($component);
$fieldMapping = $mapper->getFieldMapping();
// Mapear settings usando el mapper del modulo
$mappedSettings = $this->mapSettings($settings, $fieldMapping);
// Guardar usando Use Case
if ($this->saveComponentSettingsUseCase !== null) {
$updated = $this->saveComponentSettingsUseCase->execute($component, $mappedSettings);
wp_send_json_success([
'message' => sprintf('Se guardaron %d campos correctamente.', $updated)
]);
} else {
wp_send_json_error(['message' => 'Error: Use Case no disponible.']);
}
}
/**
* Mapea settings de field IDs a grupos/atributos
*/
private function mapSettings(array $settings, array $fieldMapping): array
{
$mappedSettings = [];
foreach ($settings as $fieldId => $value) {
if (!isset($fieldMapping[$fieldId])) {
continue;
}
$mapping = $fieldMapping[$fieldId];
$groupName = $mapping['group'];
$attributeName = $mapping['attribute'];
if (!isset($mappedSettings[$groupName])) {
$mappedSettings[$groupName] = [];
}
$mappedSettings[$groupName][$attributeName] = $value;
}
return $mappedSettings;
}
public function resetComponentDefaults(): void
{
// Verificar nonce
check_ajax_referer('roi_admin_dashboard', 'nonce');
// Verificar permisos
if (!current_user_can('manage_options')) {
wp_send_json_error([
'message' => 'No tienes permisos para realizar esta accion.'
]);
}
// Obtener componente
$component = sanitize_text_field($_POST['component'] ?? '');
if (empty($component)) {
wp_send_json_error([
'message' => 'Componente no especificado.'
]);
}
// Ruta al schema JSON
$schemaPath = get_template_directory() . '/Schemas/' . $component . '.json';
if (!file_exists($schemaPath)) {
wp_send_json_error([
'message' => 'Schema del componente no encontrado.'
]);
}
// Usar repositorio para restaurar valores
if ($this->saveComponentSettingsUseCase !== null) {
global $wpdb;
$repository = new \ROITheme\Shared\Infrastructure\Persistence\Wordpress\WordPressComponentSettingsRepository($wpdb);
$updated = $repository->resetToDefaults($component, $schemaPath);
wp_send_json_success([
'message' => sprintf('Se restauraron %d campos a sus valores por defecto.', $updated)
]);
} else {
wp_send_json_error([
'message' => 'Error: Repositorio no disponible.'
]);
}
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\Shared\Infrastructure\FieldMapping;
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
/**
* Provider para auto-registro de Field Mappers
*
* RESPONSABILIDAD:
* - Descubrir automaticamente FieldMappers en cada modulo
* - Registrarlos en el FieldMapperRegistry
*
* BENEFICIO:
* - Agregar nuevo componente = crear FieldMapper (sin tocar functions.php)
* - Eliminar componente = borrar carpeta (limpieza automatica)
*/
final class FieldMapperProvider
{
private const MODULES = [
'TopNotificationBar',
'Navbar',
'CtaLetsTalk',
'Hero',
'FeaturedImage',
'TableOfContents',
'CtaBoxSidebar',
'SocialShare',
'CtaPost',
'RelatedPost',
'ContactForm',
'Footer',
];
public function __construct(
private readonly FieldMapperRegistry $registry
) {}
/**
* Registra todos los FieldMappers disponibles
*/
public function registerAll(): void
{
foreach (self::MODULES as $module) {
$this->registerIfExists($module);
}
}
/**
* Registra un mapper si existe la clase
*/
private function registerIfExists(string $module): void
{
$className = sprintf(
'ROITheme\\Admin\\%s\\Infrastructure\\FieldMapping\\%sFieldMapper',
$module,
$module
);
if (class_exists($className)) {
$mapper = new $className();
if ($mapper instanceof FieldMapperInterface) {
$this->registry->register($mapper);
}
}
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\Shared\Infrastructure\FieldMapping;
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
/**
* Registro central de Field Mappers
*
* RESPONSABILIDAD:
* - Registrar mappers de cada modulo
* - Resolver mapper por nombre de componente
*
* PRINCIPIOS:
* - OCP: Nuevos mappers se registran sin modificar esta clase
* - SRP: Solo gestiona el registro, no contiene mapeos
*/
final class FieldMapperRegistry
{
/** @var array<string, FieldMapperInterface> */
private array $mappers = [];
/**
* Registra un mapper
*/
public function register(FieldMapperInterface $mapper): void
{
$this->mappers[$mapper->getComponentName()] = $mapper;
}
/**
* Obtiene un mapper por nombre de componente
*
* @throws \InvalidArgumentException Si no existe mapper para el componente
*/
public function getMapper(string $componentName): FieldMapperInterface
{
if (!isset($this->mappers[$componentName])) {
throw new \InvalidArgumentException(
"No field mapper registered for component: {$componentName}"
);
}
return $this->mappers[$componentName];
}
/**
* Verifica si existe mapper para un componente
*/
public function hasMapper(string $componentName): bool
{
return isset($this->mappers[$componentName]);
}
/**
* Obtiene todos los mappers registrados
*
* @return array<string, FieldMapperInterface>
*/
public function getAllMappers(): array
{
return $this->mappers;
}
}