addInfo("Validando Renderer para: {$componentName}"); // Determinar contexto (admin o public) - intentar ambos $rendererPath = $this->findRendererPath($componentName, $themePath); if ($rendererPath === null) { $result->addError("Renderer no encontrado en Public/ ni Admin/"); $pascalCaseName = str_replace('-', '', ucwords($componentName, '-')); $result->addInfo("Ubicación esperada: Public/{$pascalCaseName}/Infrastructure/Ui/*Renderer.php"); return $result; } $result->addInfo("Archivo encontrado: {$rendererPath}"); // Leer contenido del archivo $content = file_get_contents($rendererPath); // Validaciones $this->validateNamespaceAndClass($content, $componentName, $result); // Validaciones especiales para componentes de inyeccion $isInjectionComponent = in_array($componentName, self::INJECTION_COMPONENTS, true); if ($isInjectionComponent) { $result->addInfo("✓ Componente de inyección - validaciones CSS/visibility omitidas"); } else { $this->validateCSSGeneratorInjection($content, $result); $this->validateNoCSSHardcoded($content, $result); // CRÍTICO $this->validateGetVisibilityClassesMethod($content, $result); } $this->validateRenderMethod($content, $componentName, $result); $this->validateEscaping($content, $result); $this->validateNoDirectDatabaseAccess($content, $result); // Estadísticas $fileSize = filesize($rendererPath); $lineCount = substr_count($content, "\n") + 1; $result->setStat('Archivo', basename($rendererPath)); $result->setStat('Líneas', $lineCount); $result->setStat('Tamaño', $fileSize . ' bytes'); if ($lineCount > 500) { $result->addWarning("Archivo excede 500 líneas ({$lineCount}) - considerar refactorizar."); } elseif ($lineCount > 300) { $result->addInfo("ℹ️ Archivo tiene {$lineCount} líneas (recomendado: <300, aceptable: <500)"); } return $result; } private function findRendererPath(string $componentName, string $themePath): ?string { // Convertir kebab-case a PascalCase (para carpetas y archivos) $pascalCaseName = str_replace('-', '', ucwords($componentName, '-')); // Intentar en Public/ con PascalCase (PSR-4 standard) $publicPath = $themePath . '/Public/' . $pascalCaseName . '/Infrastructure/Ui/' . $pascalCaseName . 'Renderer.php'; if (file_exists($publicPath)) { return $publicPath; } // Intentar en Admin/ con PascalCase (PSR-4 standard) $adminPath = $themePath . '/Admin/' . $pascalCaseName . '/Infrastructure/Ui/' . $pascalCaseName . 'Renderer.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\\\\(Public|Admin)\\\\' . preg_quote($pascalCaseName, '/') . '\\\\Infrastructure\\\\Ui;/', $content)) { $result->addError("Namespace incorrecto. Debe ser: ROITheme\\Public\\{$pascalCaseName}\\Infrastructure\\Ui"); } // Validar clase final if (!preg_match('/final\s+class\s+' . preg_quote($pascalCaseName, '/') . 'Renderer/', $content)) { $result->addError("Clase debe ser: final class {$pascalCaseName}Renderer"); } } private function validateCSSGeneratorInjection(string $content, ValidationResult $result): void { // Verificar que constructor recibe CSSGeneratorInterface if (!preg_match('/public\s+function\s+__construct\([^)]*CSSGeneratorInterface\s+\$cssGenerator/s', $content)) { $result->addError("Constructor NO inyecta CSSGeneratorInterface (debe recibir interfaz, no clase concreta)"); } // Verificar propiedad privada if (!preg_match('/private\s+CSSGeneratorInterface\s+\$cssGenerator/', $content)) { $result->addError("Falta propiedad: private CSSGeneratorInterface \$cssGenerator"); } } private function validateRenderMethod(string $content, string $componentName, ValidationResult $result): void { // Verificar método render existe (aceptar Component object o array - Component es preferido) $hasComponentSignature = preg_match('/public\s+function\s+render\s*\(\s*Component\s+\$component\s*\)\s*:\s*string/', $content); $hasArraySignature = preg_match('/public\s+function\s+render\s*\(\s*array\s+\$data\s*\)\s*:\s*string/', $content); if (!$hasComponentSignature && !$hasArraySignature) { $result->addError("Falta método: public function render(Component \$component): string o render(array \$data): string"); } elseif ($hasArraySignature) { $result->addWarning("Método usa render(array \$data) - Considerar migrar a render(Component \$component) para type safety"); } // Componentes de inyeccion no requieren validacion de visibility $isInjectionComponent = in_array($componentName, self::INJECTION_COMPONENTS, true); if (!$isInjectionComponent) { // Verificar que valida is_enabled $hasArrayValidation = preg_match('/\$data\s*\[\s*[\'"]visibility[\'"]\s*\]\s*\[\s*[\'"]is_enabled[\'"]\s*\]/', $content); $hasComponentValidation = preg_match('/\$component->getVisibility\(\)->isEnabled\(\)/', $content); if (!$hasArrayValidation && !$hasComponentValidation) { $result->addWarning("Método render() debería validar visibilidad (is_enabled)"); } } } private function validateNoCSSHardcoded(string $content, ValidationResult $result): void { $violations = []; // Detectar style="..." (pero permitir algunos casos específicos del design system) if (preg_match_all('/style\s*=\s*["\']/', $content, $matches, PREG_OFFSET_CAPTURE)) { // Contar ocurrencias $count = count($matches[0]); // Si hay más de 2-3 (casos permitidos del design system), es violación if ($count > 3) { $violations[] = "Encontrado style=\"...\" inline ({$count} ocurrencias) - PROHIBIDO"; } elseif ($count > 0) { $result->addWarning("Encontrado {$count} uso(s) de style=\"...\" - Verificar que sea del design system aprobado"); } } // Detectar heredoc <<