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:
0
Shared/Infrastructure/Api/WordPress/.gitkeep
Normal file
0
Shared/Infrastructure/Api/WordPress/.gitkeep
Normal file
215
Shared/Infrastructure/Api/WordPress/AjaxController.php
Normal file
215
Shared/Infrastructure/Api/WordPress/AjaxController.php
Normal file
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Infrastructure\Api\Wordpress;
|
||||
|
||||
use ROITheme\Shared\Application\UseCases\SaveComponent\SaveComponentUseCase;
|
||||
use ROITheme\Shared\Application\UseCases\SaveComponent\SaveComponentRequest;
|
||||
use ROITheme\Shared\Application\UseCases\GetComponent\GetComponentUseCase;
|
||||
use ROITheme\Shared\Application\UseCases\GetComponent\GetComponentRequest;
|
||||
use ROITheme\Shared\Application\UseCases\DeleteComponent\DeleteComponentUseCase;
|
||||
use ROITheme\Shared\Application\UseCases\DeleteComponent\DeleteComponentRequest;
|
||||
use ROITheme\Shared\Infrastructure\Di\DIContainer;
|
||||
|
||||
/**
|
||||
* AjaxController - Endpoints AJAX de WordPress
|
||||
*
|
||||
* RESPONSABILIDAD: Manejar HTTP (request/response)
|
||||
*
|
||||
* PRINCIPIOS:
|
||||
* - Single Responsibility: Solo maneja HTTP, delega lógica a Use Cases
|
||||
* - Dependency Inversion: Depende de Use Cases (Application)
|
||||
*
|
||||
* SEGURIDAD:
|
||||
* - Verifica nonce
|
||||
* - Verifica capabilities (manage_options)
|
||||
* - Sanitiza inputs
|
||||
*
|
||||
* @package ROITheme\Infrastructure\Api\WordPress
|
||||
*/
|
||||
final class AjaxController
|
||||
{
|
||||
private const NONCE_ACTION = 'roi_theme_admin_nonce';
|
||||
|
||||
public function __construct(
|
||||
private DIContainer $container
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Registrar hooks de WordPress
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
add_action('wp_ajax_roi_theme_save_component', [$this, 'saveComponent']);
|
||||
add_action('wp_ajax_roi_theme_get_component', [$this, 'getComponent']);
|
||||
add_action('wp_ajax_roi_theme_delete_component', [$this, 'deleteComponent']);
|
||||
add_action('wp_ajax_roi_theme_sync_schema', [$this, 'syncSchema']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint: Guardar componente
|
||||
*
|
||||
* POST /wp-admin/admin-ajax.php?action=roi_theme_save_component
|
||||
* Body: { nonce, component_name, data }
|
||||
*
|
||||
* @return void (usa wp_send_json_*)
|
||||
*/
|
||||
public function saveComponent(): void
|
||||
{
|
||||
// 1. Seguridad
|
||||
if (!$this->verifySecurity()) {
|
||||
wp_send_json_error([
|
||||
'message' => 'Security check failed'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Sanitizar input
|
||||
$componentName = sanitize_key($_POST['component_name'] ?? '');
|
||||
$data = $_POST['data'] ?? [];
|
||||
|
||||
if (empty($componentName)) {
|
||||
wp_send_json_error([
|
||||
'message' => 'Component name is required'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Crear Request DTO
|
||||
$request = new SaveComponentRequest($componentName, $data);
|
||||
|
||||
// 4. Ejecutar Use Case
|
||||
$useCase = new SaveComponentUseCase(
|
||||
$this->container->getComponentRepository(),
|
||||
$this->container->getValidationService(),
|
||||
$this->container->getCacheService()
|
||||
);
|
||||
|
||||
$response = $useCase->execute($request);
|
||||
|
||||
// 5. Respuesta HTTP
|
||||
if ($response->isSuccess()) {
|
||||
wp_send_json_success($response->getData());
|
||||
} else {
|
||||
wp_send_json_error([
|
||||
'message' => 'Validation failed',
|
||||
'errors' => $response->getErrors()
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint: Obtener componente
|
||||
*
|
||||
* GET /wp-admin/admin-ajax.php?action=roi_theme_get_component&component_name=xxx
|
||||
*/
|
||||
public function getComponent(): void
|
||||
{
|
||||
if (!$this->verifySecurity()) {
|
||||
wp_send_json_error(['message' => 'Security check failed'], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
$componentName = sanitize_key($_GET['component_name'] ?? '');
|
||||
|
||||
if (empty($componentName)) {
|
||||
wp_send_json_error(['message' => 'Component name is required'], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$request = new GetComponentRequest($componentName);
|
||||
|
||||
$useCase = new GetComponentUseCase(
|
||||
$this->container->getComponentRepository(),
|
||||
$this->container->getCacheService()
|
||||
);
|
||||
|
||||
$response = $useCase->execute($request);
|
||||
|
||||
if ($response->isSuccess()) {
|
||||
wp_send_json_success($response->getData());
|
||||
} else {
|
||||
wp_send_json_error(['message' => $response->getError()], 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint: Eliminar componente
|
||||
*
|
||||
* POST /wp-admin/admin-ajax.php?action=roi_theme_delete_component
|
||||
* Body: { nonce, component_name }
|
||||
*/
|
||||
public function deleteComponent(): void
|
||||
{
|
||||
if (!$this->verifySecurity()) {
|
||||
wp_send_json_error(['message' => 'Security check failed'], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
$componentName = sanitize_key($_POST['component_name'] ?? '');
|
||||
|
||||
if (empty($componentName)) {
|
||||
wp_send_json_error(['message' => 'Component name is required'], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$request = new DeleteComponentRequest($componentName);
|
||||
|
||||
$useCase = new DeleteComponentUseCase(
|
||||
$this->container->getComponentRepository(),
|
||||
$this->container->getCacheService()
|
||||
);
|
||||
|
||||
$response = $useCase->execute($request);
|
||||
|
||||
if ($response->isSuccess()) {
|
||||
wp_send_json_success(['message' => $response->getMessage()]);
|
||||
} else {
|
||||
wp_send_json_error(['message' => $response->getError()], 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint: Sincronizar schemas
|
||||
*
|
||||
* POST /wp-admin/admin-ajax.php?action=roi_theme_sync_schema
|
||||
*/
|
||||
public function syncSchema(): void
|
||||
{
|
||||
if (!$this->verifySecurity()) {
|
||||
wp_send_json_error(['message' => 'Security check failed'], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
$syncService = $this->container->getSchemaSyncService();
|
||||
$result = $syncService->syncAll();
|
||||
|
||||
if ($result['success']) {
|
||||
wp_send_json_success($result['data']);
|
||||
} else {
|
||||
wp_send_json_error(['message' => $result['error']], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar seguridad (nonce + capabilities)
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function verifySecurity(): bool
|
||||
{
|
||||
// Verificar nonce
|
||||
$nonce = $_REQUEST['nonce'] ?? '';
|
||||
|
||||
if (!wp_verify_nonce($nonce, self::NONCE_ACTION)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verificar permisos (solo administradores)
|
||||
if (!current_user_can('manage_options')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
305
Shared/Infrastructure/Api/WordPress/MigrationCommand.php
Normal file
305
Shared/Infrastructure/Api/WordPress/MigrationCommand.php
Normal file
@@ -0,0 +1,305 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Infrastructure\Api\Wordpress;
|
||||
|
||||
/**
|
||||
* WP-CLI Command para Sincronización de Schemas
|
||||
*
|
||||
* Responsabilidad: Sincronizar schemas JSON a base de datos
|
||||
*
|
||||
* COMANDOS DISPONIBLES:
|
||||
* - wp roi-theme sync-component [nombre] : Sincronizar un componente específico
|
||||
* - wp roi-theme sync-all-components : Sincronizar todos los componentes
|
||||
*
|
||||
* USO:
|
||||
* ```bash
|
||||
* # Sincronizar un componente específico
|
||||
* wp roi-theme sync-component top-notification-bar
|
||||
*
|
||||
* # Sincronizar todos los componentes
|
||||
* wp roi-theme sync-all-components
|
||||
* ```
|
||||
*
|
||||
* IMPORTANTE:
|
||||
* - Si un campo NO existe en BD → INSERT con valor default del JSON
|
||||
* - Si un campo YA existe → PRESERVA el valor del usuario, solo actualiza is_editable
|
||||
*/
|
||||
final class MigrationCommand
|
||||
{
|
||||
/**
|
||||
* Sincronizar un componente específico desde su schema JSON a la BD
|
||||
*
|
||||
* ## OPCIONES
|
||||
*
|
||||
* <component_name>
|
||||
* : Nombre del componente a sincronizar (sin extensión .json)
|
||||
*
|
||||
* ## EJEMPLOS
|
||||
*
|
||||
* # Sincronizar top-notification-bar
|
||||
* wp roi-theme sync-component top-notification-bar
|
||||
*
|
||||
* # Sincronizar navbar
|
||||
* wp roi-theme sync-component navbar
|
||||
*
|
||||
* @param array $args Argumentos posicionales
|
||||
* @param array $assoc_args Argumentos asociativos (--flags)
|
||||
* @return void
|
||||
*/
|
||||
public function sync_component(array $args, array $assoc_args): void
|
||||
{
|
||||
if (empty($args[0])) {
|
||||
\WP_CLI::error('Debes especificar el nombre del componente. Ejemplo: wp roi-theme sync-component top-notification-bar');
|
||||
return;
|
||||
}
|
||||
|
||||
$component_name = $args[0];
|
||||
$schemas_path = get_template_directory() . '/schemas';
|
||||
$schema_file = $schemas_path . '/' . $component_name . '.json';
|
||||
|
||||
if (!file_exists($schema_file)) {
|
||||
\WP_CLI::error("Schema no encontrado: {$schema_file}");
|
||||
return;
|
||||
}
|
||||
|
||||
\WP_CLI::line('');
|
||||
\WP_CLI::line("🔄 Sincronizando componente: {$component_name}");
|
||||
\WP_CLI::line('');
|
||||
|
||||
$result = $this->syncSchemaToDatabase($schema_file, $component_name);
|
||||
|
||||
\WP_CLI::line('');
|
||||
if ($result['success']) {
|
||||
\WP_CLI::success($result['message']);
|
||||
\WP_CLI::line('');
|
||||
\WP_CLI::line('📊 Estadísticas:');
|
||||
\WP_CLI::line(' ├─ Campos insertados: ' . $result['stats']['inserted']);
|
||||
\WP_CLI::line(' ├─ Campos actualizados: ' . $result['stats']['updated']);
|
||||
\WP_CLI::line(' ├─ Campos eliminados (obsoletos): ' . $result['stats']['deleted']);
|
||||
\WP_CLI::line(' └─ Total en schema: ' . $result['stats']['total']);
|
||||
\WP_CLI::line('');
|
||||
} else {
|
||||
\WP_CLI::error($result['message']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sincronizar todos los componentes desde schemas/ a la BD
|
||||
*
|
||||
* ## EJEMPLOS
|
||||
*
|
||||
* wp roi-theme sync-all-components
|
||||
*
|
||||
* @param array $args Argumentos posicionales
|
||||
* @param array $assoc_args Argumentos asociativos
|
||||
* @return void
|
||||
*/
|
||||
public function sync_all_components(array $args, array $assoc_args): void
|
||||
{
|
||||
$schemas_path = get_template_directory() . '/schemas';
|
||||
|
||||
if (!is_dir($schemas_path)) {
|
||||
\WP_CLI::error("Directorio de schemas no encontrado: {$schemas_path}");
|
||||
return;
|
||||
}
|
||||
|
||||
$schema_files = glob($schemas_path . '/*.json');
|
||||
|
||||
if (empty($schema_files)) {
|
||||
\WP_CLI::warning('No se encontraron schemas JSON en ' . $schemas_path);
|
||||
return;
|
||||
}
|
||||
|
||||
\WP_CLI::line('');
|
||||
\WP_CLI::line('🔄 Sincronizando todos los componentes...');
|
||||
\WP_CLI::line('');
|
||||
|
||||
$total_stats = [
|
||||
'components' => 0,
|
||||
'inserted' => 0,
|
||||
'updated' => 0,
|
||||
'total' => 0,
|
||||
'errors' => 0
|
||||
];
|
||||
|
||||
foreach ($schema_files as $schema_file) {
|
||||
$component_name = basename($schema_file, '.json');
|
||||
|
||||
\WP_CLI::line(" → Procesando: {$component_name}");
|
||||
|
||||
$result = $this->syncSchemaToDatabase($schema_file, $component_name);
|
||||
|
||||
if ($result['success']) {
|
||||
$total_stats['components']++;
|
||||
$total_stats['inserted'] += $result['stats']['inserted'];
|
||||
$total_stats['updated'] += $result['stats']['updated'];
|
||||
$total_stats['total'] += $result['stats']['total'];
|
||||
\WP_CLI::line(" ✓ {$result['message']}");
|
||||
} else {
|
||||
$total_stats['errors']++;
|
||||
\WP_CLI::line(" ✗ Error: {$result['message']}");
|
||||
}
|
||||
}
|
||||
|
||||
\WP_CLI::line('');
|
||||
\WP_CLI::success('Sincronización completada');
|
||||
\WP_CLI::line('');
|
||||
\WP_CLI::line('📊 Estadísticas totales:');
|
||||
\WP_CLI::line(' ├─ Componentes sincronizados: ' . $total_stats['components']);
|
||||
\WP_CLI::line(' ├─ Campos insertados: ' . $total_stats['inserted']);
|
||||
\WP_CLI::line(' ├─ Campos actualizados: ' . $total_stats['updated']);
|
||||
\WP_CLI::line(' ├─ Total procesado: ' . $total_stats['total']);
|
||||
\WP_CLI::line(' └─ Errores: ' . $total_stats['errors']);
|
||||
\WP_CLI::line('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sincronizar un schema JSON a la base de datos
|
||||
*
|
||||
* @param string $schema_file Ruta completa al archivo JSON
|
||||
* @param string $component_name Nombre del componente
|
||||
* @return array{success: bool, message: string, stats: array{inserted: int, updated: int, deleted: int, total: int}}
|
||||
*/
|
||||
private function syncSchemaToDatabase(string $schema_file, string $component_name): array
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
// Leer y decodificar JSON
|
||||
$json_content = file_get_contents($schema_file);
|
||||
if ($json_content === false) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Error al leer archivo JSON',
|
||||
'stats' => ['inserted' => 0, 'updated' => 0, 'deleted' => 0, 'total' => 0]
|
||||
];
|
||||
}
|
||||
|
||||
$schema = json_decode($json_content, true);
|
||||
if ($schema === null) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Error al decodificar JSON: ' . json_last_error_msg(),
|
||||
'stats' => ['inserted' => 0, 'updated' => 0, 'deleted' => 0, 'total' => 0]
|
||||
];
|
||||
}
|
||||
|
||||
$table = $wpdb->prefix . 'roi_theme_component_settings';
|
||||
$stats = ['inserted' => 0, 'updated' => 0, 'deleted' => 0, 'total' => 0];
|
||||
|
||||
// Iterar grupos y campos
|
||||
if (!isset($schema['groups']) || !is_array($schema['groups'])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Schema inválido: falta clave "groups"',
|
||||
'stats' => $stats
|
||||
];
|
||||
}
|
||||
|
||||
// Construir lista de campos válidos del schema
|
||||
$validFields = [];
|
||||
foreach ($schema['groups'] as $group_name => $group_data) {
|
||||
if (!isset($group_data['fields']) || !is_array($group_data['fields'])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($group_data['fields'] as $attribute_name => $field_config) {
|
||||
$validFields[] = $group_name . '::' . $attribute_name;
|
||||
}
|
||||
}
|
||||
|
||||
// PASO 1: Eliminar registros obsoletos (que no están en el schema)
|
||||
$existing_records = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT id, group_name, attribute_name FROM {$table} WHERE component_name = %s",
|
||||
$component_name
|
||||
));
|
||||
|
||||
foreach ($existing_records as $record) {
|
||||
$key = $record->group_name . '::' . $record->attribute_name;
|
||||
if (!in_array($key, $validFields, true)) {
|
||||
$wpdb->delete($table, ['id' => $record->id], ['%d']);
|
||||
$stats['deleted']++;
|
||||
}
|
||||
}
|
||||
|
||||
// PASO 2: Insertar/Actualizar campos del schema
|
||||
foreach ($schema['groups'] as $group_name => $group_data) {
|
||||
if (!isset($group_data['fields']) || !is_array($group_data['fields'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($group_data['fields'] as $attribute_name => $field_config) {
|
||||
$stats['total']++;
|
||||
|
||||
// Verificar si el campo ya existe
|
||||
$existing = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT id, attribute_value FROM {$table}
|
||||
WHERE component_name = %s
|
||||
AND group_name = %s
|
||||
AND attribute_name = %s",
|
||||
$component_name,
|
||||
$group_name,
|
||||
$attribute_name
|
||||
));
|
||||
|
||||
$is_editable = isset($field_config['editable']) ? (bool)$field_config['editable'] : false;
|
||||
$default_value = isset($field_config['default']) ? $field_config['default'] : '';
|
||||
|
||||
// Convertir valor a string para almacenamiento
|
||||
if (is_array($default_value) || is_object($default_value)) {
|
||||
$default_value = json_encode($default_value);
|
||||
} elseif (is_bool($default_value)) {
|
||||
$default_value = $default_value ? '1' : '0';
|
||||
} else {
|
||||
$default_value = (string)$default_value;
|
||||
}
|
||||
|
||||
if ($existing === null) {
|
||||
// INSERT: Campo nuevo, usar default del JSON
|
||||
$result = $wpdb->insert(
|
||||
$table,
|
||||
[
|
||||
'component_name' => $component_name,
|
||||
'group_name' => $group_name,
|
||||
'attribute_name' => $attribute_name,
|
||||
'attribute_value' => $default_value,
|
||||
'is_editable' => $is_editable ? 1 : 0
|
||||
],
|
||||
['%s', '%s', '%s', '%s', '%d']
|
||||
);
|
||||
|
||||
if ($result !== false) {
|
||||
$stats['inserted']++;
|
||||
}
|
||||
} else {
|
||||
// UPDATE: Campo existente, preservar valor del usuario, solo actualizar is_editable
|
||||
$result = $wpdb->update(
|
||||
$table,
|
||||
['is_editable' => $is_editable ? 1 : 0],
|
||||
[
|
||||
'component_name' => $component_name,
|
||||
'group_name' => $group_name,
|
||||
'attribute_name' => $attribute_name
|
||||
],
|
||||
['%d'],
|
||||
['%s', '%s', '%s']
|
||||
);
|
||||
|
||||
if ($result !== false) {
|
||||
$stats['updated']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Componente '{$component_name}' sincronizado correctamente",
|
||||
'stats' => $stats
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Registrar comando WP-CLI
|
||||
if (defined('WP_CLI') && WP_CLI) {
|
||||
\WP_CLI::add_command('roi-theme', MigrationCommand::class);
|
||||
}
|
||||
Reference in New Issue
Block a user