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:
38
Admin/Shared/Domain/Contracts/FieldMapperInterface.php
Normal file
38
Admin/Shared/Domain/Contracts/FieldMapperInterface.php
Normal 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;
|
||||
}
|
||||
145
Admin/Shared/Infrastructure/Api/Wordpress/AdminAjaxHandler.php
Normal file
145
Admin/Shared/Infrastructure/Api/Wordpress/AdminAjaxHandler.php
Normal 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.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user