Files
roi-theme/Shared/Infrastructure/Validators/Phase02Validator.php
FrankZamora 90863cd8f5 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>
2025-11-26 22:53:34 -06:00

216 lines
7.6 KiB
PHP

<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Validators;
/**
* Validador de Fase 02: Sincronización JSON→BD
*
* Valida que:
* - Schema JSON existe y es válido
* - Tabla de BD existe
* - Todos los campos del JSON están sincronizados en BD
* - No hay campos huérfanos en BD
* - is_editable coincide entre JSON y BD
* - No hay duplicados
*/
final class Phase02Validator implements PhaseValidatorInterface
{
private const TABLE_NAME = 'wp_roi_theme_component_settings';
public function validate(string $componentName, string $themePath): ValidationResult
{
global $wpdb;
$result = new ValidationResult();
$result->addInfo("Validando sincronización JSON→BD para: {$componentName}");
// 1. Verificar que schema JSON existe
$schemaPath = $themePath . '/Schemas/' . $componentName . '.json';
if (!file_exists($schemaPath)) {
$result->addError("Schema JSON no encontrado: {$schemaPath}");
$result->addInfo("Ejecutar primero: wp roi-theme sync-component {$componentName}");
return $result;
}
// 2. Parsear JSON
$jsonContent = file_get_contents($schemaPath);
$schema = json_decode($jsonContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$result->addError("JSON inválido: " . json_last_error_msg());
return $result;
}
// 3. Verificar que tabla existe
$tableName = $wpdb->prefix . 'roi_theme_component_settings';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$tableExists = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = %s",
$tableName
));
if ($tableExists == 0) {
$result->addError("Tabla '{$tableName}' no existe en la base de datos");
$result->addInfo("La tabla debería crearse automáticamente en functions.php");
return $result;
}
// 4. Obtener todos los campos del JSON
$jsonFields = $this->extractFieldsFromSchema($schema);
$totalJsonFields = count($jsonFields);
// 5. Obtener todos los registros de BD para este componente
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$dbRecords = $wpdb->get_results($wpdb->prepare(
"SELECT component_name, group_name, attribute_name, is_editable FROM {$tableName} WHERE component_name = %s",
$componentName
), ARRAY_A);
$totalDbRecords = count($dbRecords);
// 6. Validar sincronización
$this->validateSync($componentName, $jsonFields, $dbRecords, $result);
// 7. Validar no hay duplicados
$this->validateNoDuplicates($componentName, $tableName, $wpdb, $result);
// Estadísticas
$result->setStat('Schema JSON', "schemas/{$componentName}.json");
$result->setStat('Campos en JSON', $totalJsonFields);
$result->setStat('Registros en BD', $totalDbRecords);
$result->setStat('Tabla BD', $tableName);
return $result;
}
/**
* Extrae todos los campos del schema JSON
*
* @param array $schema
* @return array Array de arrays con ['group' => '', 'attribute' => '', 'editable' => bool]
*/
private function extractFieldsFromSchema(array $schema): array
{
$fields = [];
if (!isset($schema['groups'])) {
return $fields;
}
foreach ($schema['groups'] as $groupName => $group) {
if (!isset($group['fields'])) {
continue;
}
foreach ($group['fields'] as $attributeName => $field) {
$fields[] = [
'group' => $groupName,
'attribute' => $attributeName,
'editable' => $field['editable'] ?? false,
];
}
}
return $fields;
}
private function validateSync(string $componentName, array $jsonFields, array $dbRecords, ValidationResult $result): void
{
// Crear índice de registros de BD para búsqueda rápida
$dbIndex = [];
foreach ($dbRecords as $record) {
$key = $record['group_name'] . '.' . $record['attribute_name'];
$dbIndex[$key] = $record;
}
// Validar que cada campo del JSON está en BD
$missingInDb = [];
$editableMismatch = [];
foreach ($jsonFields as $field) {
$key = $field['group'] . '.' . $field['attribute'];
if (!isset($dbIndex[$key])) {
$missingInDb[] = $key;
} else {
// Validar is_editable coincide
$dbEditable = (bool) $dbIndex[$key]['is_editable'];
$jsonEditable = $field['editable'];
if ($dbEditable !== $jsonEditable) {
$editableMismatch[] = "{$key} (JSON: " . ($jsonEditable ? 'true' : 'false') .
", BD: " . ($dbEditable ? 'true' : 'false') . ")";
}
// Remover de índice para detectar huérfanos
unset($dbIndex[$key]);
}
}
// Campos faltantes en BD
if (!empty($missingInDb)) {
foreach ($missingInDb as $field) {
$result->addError("Campo '{$field}' existe en JSON pero NO en BD");
}
$result->addInfo("Ejecutar: wp roi-theme sync-component {$componentName}");
}
// Campos huérfanos en BD (no están en JSON)
if (!empty($dbIndex)) {
foreach ($dbIndex as $key => $record) {
$result->addWarning("Campo '{$key}' existe en BD pero NO en JSON (campo huérfano)");
}
}
// is_editable no coincide
if (!empty($editableMismatch)) {
foreach ($editableMismatch as $mismatch) {
$result->addError("Campo {$mismatch} tiene is_editable diferente entre JSON y BD");
}
$result->addInfo("Ejecutar: wp roi-theme sync-component {$componentName}");
}
// Si todo está sincronizado
if (empty($missingInDb) && empty($editableMismatch) && empty($dbIndex)) {
$result->addInfo("✓ Todos los campos están sincronizados correctamente");
}
}
private function validateNoDuplicates(string $componentName, string $tableName, $wpdb, ValidationResult $result): void
{
// Buscar duplicados
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$duplicates = $wpdb->get_results($wpdb->prepare(
"SELECT component_name, group_name, attribute_name, COUNT(*) as count
FROM {$tableName}
WHERE component_name = %s
GROUP BY component_name, group_name, attribute_name
HAVING count > 1",
$componentName
), ARRAY_A);
if (!empty($duplicates)) {
foreach ($duplicates as $dup) {
$result->addError(
"Duplicado en BD: {$dup['group_name']}.{$dup['attribute_name']} " .
"({$dup['count']} registros)"
);
}
$result->addInfo("El constraint UNIQUE debería prevenir duplicados. Revisar integridad de BD.");
}
}
public function getPhaseNumber(): int|string
{
return 2;
}
public function getPhaseDescription(): string
{
return 'JSON→DB Sync';
}
}