Files
roi-theme/Shared/Infrastructure/Validators/Phase04Validator.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

366 lines
15 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Validators;
/**
* Validador de Fase 04: FormBuilders (UI Admin)
*
* Valida que:
* - FormBuilder existe en ubicación correcta (admin/)
* - Namespace y clase correctos
* - Inyecta AdminDashboardRenderer
* - Tiene método buildForm()
* - Es modular (métodos privados build*)
* - Usa AdminDashboardRenderer correctamente
* - Usa Bootstrap 5
* - Escaping correcto
* - NO accede directamente a BD
*/
final class Phase04Validator implements PhaseValidatorInterface
{
public function validate(string $componentName, string $themePath): ValidationResult
{
$result = new ValidationResult();
$result->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: <button class=\"btn-reset-defaults\" data-component=\"{$componentName}\">...</button>");
} 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)';
}
}