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>
216 lines
7.6 KiB
PHP
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';
|
|
}
|
|
}
|