addInfo("Validando FormBuilder para: {$componentName}"); // Buscar FormBuilder en Admin/ $formBuilderPath = $this->findFormBuilderPath($componentName, $themePath); if ($formBuilderPath === null) { $result->addError("FormBuilder no encontrado en Admin/"); $pascalCaseName = str_replace('-', '', ucwords($componentName, '-')); $result->addInfo("Ubicación esperada: Admin/{$pascalCaseName}/Infrastructure/Ui/*FormBuilder.php"); return $result; } $result->addInfo("Archivo encontrado: {$formBuilderPath}"); // Leer contenido $content = file_get_contents($formBuilderPath); // Validaciones del FormBuilder $this->validateNamespaceAndClass($content, $componentName, $result); $this->validateRendererInjection($content, $result); $this->validateBuildFormMethod($content, $result); $this->validateModularity($content, $result); $this->validateRendererUsage($content, $result); $this->validateBootstrapCompliance($content, $result); $this->validateEscaping($content, $result); $this->validateNoDirectDatabaseAccess($content, $result); // Validaciones de integración con Admin Panel (CRÍTICAS) $this->validateComponentRegistration($componentName, $themePath, $result); $this->validateAjaxFieldMapping($componentName, $themePath, $content, $result); $this->validateResetButton($componentName, $content, $result); // Estadísticas $fileSize = filesize($formBuilderPath); $lineCount = substr_count($content, "\n") + 1; $buildMethodsCount = $this->countBuildMethods($content); $result->setStat('Archivo', basename($formBuilderPath)); $result->setStat('Líneas', $lineCount); $result->setStat('Métodos build*', $buildMethodsCount); $result->setStat('Tamaño', $fileSize . ' bytes'); if ($lineCount > 500) { $result->addWarning("Archivo excede 500 líneas ({$lineCount}) - considerar dividir en múltiples FormBuilders."); } elseif ($lineCount > 300) { $result->addInfo("ℹ️ Archivo tiene {$lineCount} líneas (recomendado: <300, aceptable: <500)"); } if ($buildMethodsCount < 2) { $result->addWarning("Solo {$buildMethodsCount} método(s) build*. FormBuilders modulares deben tener al menos 2-3 métodos privados."); } return $result; } private function findFormBuilderPath(string $componentName, string $themePath): ?string { // Convertir kebab-case a PascalCase (para carpetas y archivos) $pascalCaseName = str_replace('-', '', ucwords($componentName, '-')); // Buscar en Admin/ con PascalCase (PSR-4 standard) $adminPath = $themePath . '/Admin/' . $pascalCaseName . '/Infrastructure/Ui/' . $pascalCaseName . 'FormBuilder.php'; if (file_exists($adminPath)) { return $adminPath; } return null; } private function validateNamespaceAndClass(string $content, string $componentName, ValidationResult $result): void { $pascalCaseName = str_replace('-', '', ucwords($componentName, '-')); // Validar namespace (Ui en PascalCase, primera letra mayúscula) if (!preg_match('/namespace\s+ROITheme\\\\Admin\\\\' . preg_quote($pascalCaseName, '/') . '\\\\Infrastructure\\\\Ui;/', $content)) { $result->addError("Namespace incorrecto. Debe ser: ROITheme\\Admin\\{$pascalCaseName}\\Infrastructure\\Ui"); } // Validar clase final if (!preg_match('/final\s+class\s+' . preg_quote($pascalCaseName, '/') . 'FormBuilder/', $content)) { $result->addError("Clase debe ser: final class {$pascalCaseName}FormBuilder"); } } private function validateRendererInjection(string $content, ValidationResult $result): void { // Verificar que constructor recibe AdminDashboardRenderer if (!preg_match('/public\s+function\s+__construct\([^)]*AdminDashboardRenderer\s+\$renderer/s', $content)) { $result->addError("Constructor NO inyecta AdminDashboardRenderer"); } // Verificar propiedad privada if (!preg_match('/private\s+AdminDashboardRenderer\s+\$renderer/', $content)) { $result->addError("Falta propiedad: private AdminDashboardRenderer \$renderer"); } } private function validateBuildFormMethod(string $content, ValidationResult $result): void { // Verificar método buildForm existe if (!preg_match('/public\s+function\s+buildForm\s*\(\s*string\s+\$componentId\s*\)\s*:\s*string/', $content)) { $result->addError("Falta método: public function buildForm(string \$componentId): string"); } } private function validateModularity(string $content, ValidationResult $result): void { // Contar métodos privados build* $buildMethodsCount = $this->countBuildMethods($content); if ($buildMethodsCount === 0) { $result->addError("FormBuilder NO tiene métodos privados build* - Debe ser modular"); } elseif ($buildMethodsCount < 2) { $result->addWarning("Solo {$buildMethodsCount} método build* - Considerar más modularidad"); } else { $result->addInfo("✓ Modularidad: {$buildMethodsCount} métodos build* detectados"); } } private function countBuildMethods(string $content): int { preg_match_all('/private\s+function\s+build[A-Z][a-zA-Z]*\s*\(/', $content, $matches); return count($matches[0]); } private function validateRendererUsage(string $content, ValidationResult $result): void { // Verificar que usa $this->renderer->getFieldValue() if (!preg_match('/\$this->renderer->getFieldValue\s*\(/', $content)) { $result->addWarning("No se detectó uso de \$this->renderer->getFieldValue() - Verificar que obtiene datos del renderer"); } } private function validateBootstrapCompliance(string $content, ValidationResult $result): void { $bootstrapClasses = [ 'form-control' => false, 'form-select' => false, 'form-check' => false, 'card' => false, 'row' => false, ]; foreach ($bootstrapClasses as $class => $found) { // Buscar clase dentro de atributos class="..." o class='...' // Acepta: class="row", class="row g-3", class="form-check form-switch", etc. if (preg_match('/class=["\'][^"\']*\b' . preg_quote($class, '/') . '\b[^"\']*["\']/', $content)) { $bootstrapClasses[$class] = true; } } $usedClasses = array_keys(array_filter($bootstrapClasses)); if (count($usedClasses) === 0) { $result->addWarning("No se detectaron clases Bootstrap 5 - Verificar que usa Bootstrap"); } else { $result->addInfo("✓ Bootstrap 5: Usa " . implode(', ', $usedClasses)); } } private function validateEscaping(string $content, ValidationResult $result): void { $escapingUsed = false; if (preg_match('/esc_attr\s*\(/', $content)) { $escapingUsed = true; } if (preg_match('/esc_html\s*\(/', $content)) { $escapingUsed = true; } if (!$escapingUsed) { $result->addWarning("No se detectó uso de esc_attr() o esc_html() - Verificar escaping de valores"); } } private function validateNoDirectDatabaseAccess(string $content, ValidationResult $result): void { // Verificar que NO usa global $wpdb if (preg_match('/global\s+\$wpdb/', $content)) { $result->addError("FormBuilder usa 'global \$wpdb' - Acceso a BD debe estar en Repository o AdminDashboardRenderer"); } // Verificar que NO usa funciones directas de BD $dbFunctions = ['get_option', 'update_option', 'get_post_meta', 'update_post_meta']; foreach ($dbFunctions as $func) { if (preg_match('/\b' . $func . '\s*\(/', $content)) { $result->addError("FormBuilder usa '{$func}()' - Debe usar AdminDashboardRenderer, NO acceder BD directamente"); } } } /** * Valida que el componente está registrado en AdminDashboardRenderer::getComponents() */ private function validateComponentRegistration(string $componentName, string $themePath, ValidationResult $result): void { $dashboardRendererPath = $themePath . '/Admin/Infrastructure/Ui/AdminDashboardRenderer.php'; if (!file_exists($dashboardRendererPath)) { $result->addWarning("No se encontró AdminDashboardRenderer.php - No se puede validar registro del componente"); return; } $dashboardContent = file_get_contents($dashboardRendererPath); // Buscar si el componente está registrado en getComponents() // Busca patrones como: 'cta-lets-talk' => [ o "cta-lets-talk" => [ $pattern = '/[\'"]' . preg_quote($componentName, '/') . '[\'"]\s*=>\s*\[/'; if (!preg_match($pattern, $dashboardContent)) { $result->addError("Componente NO registrado en AdminDashboardRenderer::getComponents()"); $result->addInfo("Agregar en Admin/Infrastructure/Ui/AdminDashboardRenderer.php método getComponents():"); $result->addInfo("'{$componentName}' => ['id' => '{$componentName}', 'label' => '...', 'icon' => 'bi-...'],"); } else { $result->addInfo("✓ Componente registrado en AdminDashboardRenderer::getComponents()"); } } /** * Valida que existe mapeo AJAX para los campos del FormBuilder */ private function validateAjaxFieldMapping(string $componentName, string $themePath, string $formBuilderContent, ValidationResult $result): void { $ajaxHandlerPath = $themePath . '/Admin/Infrastructure/Api/Wordpress/AdminAjaxHandler.php'; if (!file_exists($ajaxHandlerPath)) { $result->addWarning("No se encontró AdminAjaxHandler.php - No se puede validar mapeo AJAX"); return; } $ajaxContent = file_get_contents($ajaxHandlerPath); // Extraer todos los IDs de campos del FormBuilder (id="...") preg_match_all('/id=["\']([a-zA-Z0-9_]+)["\']/', $formBuilderContent, $fieldMatches); $formFieldIds = array_unique($fieldMatches[1]); // Filtrar solo los IDs que corresponden a inputs (no contenedores) // Los IDs de inputs generalmente tienen un patrón como componentNameFieldName $fieldPrefix = $this->getFieldPrefix($componentName); $inputFieldIds = array_filter($formFieldIds, function($id) use ($fieldPrefix) { // Solo IDs que empiezan con el prefijo del componente return stripos($id, $fieldPrefix) === 0; }); if (empty($inputFieldIds)) { $result->addWarning("No se detectaron campos con prefijo '{$fieldPrefix}' en FormBuilder"); return; } // Verificar que cada campo tiene mapeo en getFieldMapping() $unmappedFields = []; foreach ($inputFieldIds as $fieldId) { // Buscar el ID en el contenido del AjaxHandler if (!preg_match('/[\'"]' . preg_quote($fieldId, '/') . '[\'"]\s*=>/', $ajaxContent)) { $unmappedFields[] = $fieldId; } } $totalFields = count($inputFieldIds); $mappedFields = $totalFields - count($unmappedFields); if (count($unmappedFields) > 0) { $result->addError("Mapeo AJAX incompleto: {$mappedFields}/{$totalFields} campos mapeados"); $result->addInfo("Campos sin mapeo en AdminAjaxHandler::getFieldMapping():"); foreach (array_slice($unmappedFields, 0, 5) as $field) { $result->addInfo(" - {$field}"); } if (count($unmappedFields) > 5) { $result->addInfo(" ... y " . (count($unmappedFields) - 5) . " más"); } } else { $result->addInfo("✓ Mapeo AJAX completo: {$totalFields}/{$totalFields} campos mapeados"); } } /** * Valida que el botón "Restaurar valores por defecto" sigue el patrón correcto * * El patrón correcto es: * - class="btn-reset-defaults" * - data-component="nombre-componente" * * NO usar id="reset*Defaults" (patrón antiguo hardcodeado) */ private function validateResetButton(string $componentName, string $content, ValidationResult $result): void { // Verificar patrón correcto: class="btn-reset-defaults" data-component="..." $correctPattern = '/class=["\'][^"\']*btn-reset-defaults[^"\']*["\'][^>]*data-component=["\']' . preg_quote($componentName, '/') . '["\']/'; $correctPatternAlt = '/data-component=["\']' . preg_quote($componentName, '/') . '["\'][^>]*class=["\'][^"\']*btn-reset-defaults[^"\']*["\']/'; $hasCorrectPattern = preg_match($correctPattern, $content) || preg_match($correctPatternAlt, $content); // Verificar patrón incorrecto: id="reset*Defaults" (hardcodeado) $incorrectPattern = '/id=["\']reset[A-Za-z]+Defaults["\']/'; $hasIncorrectPattern = preg_match($incorrectPattern, $content); if ($hasIncorrectPattern && !$hasCorrectPattern) { $result->addError("Botón reset usa patrón hardcodeado (id=\"reset*Defaults\")"); $result->addInfo("Cambiar a: class=\"btn-reset-defaults\" data-component=\"{$componentName}\""); } elseif (!$hasCorrectPattern) { $result->addWarning("No se detectó botón 'Restaurar valores por defecto' con patrón dinámico"); $result->addInfo("Agregar: "); } else { $result->addInfo("✓ Botón reset con patrón dinámico correcto (data-component=\"{$componentName}\")"); } } /** * Obtiene el prefijo de campos para un componente * Permite prefijos personalizados más cortos para componentes con nombres largos */ private function getFieldPrefix(string $componentName): string { // Mapeo de prefijos personalizados (más cortos/legibles) $customPrefixes = [ 'top-notification-bar' => 'topBar', ]; if (isset($customPrefixes[$componentName])) { return $customPrefixes[$componentName]; } // Por defecto: convertir kebab-case a camelCase $pascalCaseName = str_replace('-', '', ucwords($componentName, '-')); return lcfirst($pascalCaseName); } public function getPhaseNumber(): int|string { return 4; } public function getPhaseDescription(): string { return 'FormBuilders (UI Admin)'; } }