addInfo("Validación SOLID general para: {$componentName}"); // Buscar directorio del componente $componentPath = $this->findComponentPath($componentName, $themePath); if ($componentPath === null) { $result->addError("Componente no encontrado en Admin/ ni Public/"); return $result; } $result->addInfo("Escaneando: {$componentPath}"); // Escanear todos los archivos .php $this->filesScanned = 0; $this->scanDirectory($componentPath, $result); // Estadísticas $result->setStat('Archivos escaneados', $this->filesScanned); return $result; } private function findComponentPath(string $componentName, string $themePath): ?string { // Convertir kebab-case a PascalCase (PSR-4 standard) $pascalCaseName = str_replace('-', '', ucwords($componentName, '-')); // Intentar en Public/ con PascalCase $publicPath = $themePath . '/Public/' . $pascalCaseName; if (is_dir($publicPath)) { return $publicPath; } // Intentar en Admin/ con PascalCase $adminPath = $themePath . '/Admin/' . $pascalCaseName; if (is_dir($adminPath)) { return $adminPath; } return null; } private function scanDirectory(string $dir, ValidationResult $result): void { $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $file) { if ($file->isFile() && $file->getExtension() === 'php') { $this->validateFile($file->getPathname(), $result); } } } private function validateFile(string $filePath, ValidationResult $result): void { $this->filesScanned++; $content = file_get_contents($filePath); $fileName = basename($filePath); // Detectar capa (Domain, Application, Infrastructure) $layer = $this->detectLayer($filePath); // REGLA 1: Domain NO puede tener WordPress if ($layer === 'domain') { $this->checkDomainPurity($content, $fileName, $result); } // REGLA 2: Application NO puede tener WordPress if ($layer === 'application') { $this->checkApplicationPurity($content, $fileName, $result); } // REGLA 3: CERO CSS hardcodeado en PHP (general) $this->checkNoHardcodedCSS($content, $fileName, $result); // REGLA 4: DIP - Constructores deben recibir interfaces if ($layer === 'infrastructure') { $this->checkDependencyInjection($content, $fileName, $result); } // REGLA 5: SRP - Archivos no deben exceder 300 líneas $this->checkFileLength($content, $fileName, $result); // REGLA 6: ISP - Interfaces no deben ser gordas if (strpos($filePath, 'Interface.php') !== false) { $this->checkInterfaceSize($content, $fileName, $result); } // REGLA 7: Propiedades deben ser private/protected $this->checkEncapsulation($content, $fileName, $result); // REGLA 8: NO debe haber new ConcreteClass() en Domain/Application if ($layer === 'domain' || $layer === 'application') { $this->checkNoDirectInstantiation($content, $fileName, $result); } } private function detectLayer(string $filePath): ?string { if (stripos($filePath, DIRECTORY_SEPARATOR . 'domain' . DIRECTORY_SEPARATOR) !== false) { return 'domain'; } if (stripos($filePath, DIRECTORY_SEPARATOR . 'application' . DIRECTORY_SEPARATOR) !== false) { return 'application'; } if (stripos($filePath, DIRECTORY_SEPARATOR . 'infrastructure' . DIRECTORY_SEPARATOR) !== false) { return 'infrastructure'; } return null; } private function checkDomainPurity(string $content, string $file, ValidationResult $result): void { foreach (self::WORDPRESS_FUNCTIONS as $wpFunction) { if (preg_match('/' . $wpFunction . '/i', $content)) { $result->addError("Domain tiene código WordPress '{$wpFunction}': {$file}"); } } // Domain NO debe tener echo/print if (preg_match('/\b(echo|print|print_r|var_dump)\s+/', $content)) { $result->addError("Domain tiene output directo (echo/print/var_dump): {$file}"); } // Domain NO debe tener HTML if (preg_match('/<(div|span|p|a|ul|li|table|form|input|button|h[1-6])\s/i', $content)) { $result->addError("Domain tiene HTML hardcodeado: {$file}"); } } private function checkApplicationPurity(string $content, string $file, ValidationResult $result): void { foreach (self::WORDPRESS_FUNCTIONS as $wpFunction) { if (preg_match('/' . $wpFunction . '/i', $content)) { $result->addError("Application tiene código WordPress '{$wpFunction}': {$file}"); } } } private function checkNoHardcodedCSS(string $content, string $file, ValidationResult $result): void { // Skip si es un archivo de assets o FormBuilder (ya validado en Phase04) if (strpos($file, 'assets') !== false || strpos($file, 'FormBuilder') !== false) { return; } $violations = 0; foreach (self::CSS_PATTERNS as $pattern) { if (preg_match('/' . $pattern . '/i', $content)) { $violations++; } } if ($violations > 0) { $result->addError("CSS hardcodeado encontrado ({$violations} patrones): {$file}"); } } private function checkDependencyInjection(string $content, string $file, ValidationResult $result): void { // Buscar constructores if (preg_match('/__construct\s*\([^)]*\)/', $content, $matches)) { $constructor = $matches[0]; // Verificar si recibe parámetros if (strpos($constructor, '$') !== false) { // Buscar parámetros que son clases concretas (Service, Repository, Manager) if (preg_match('/private\s+([A-Z][a-zA-Z]+)(Service|Repository|Manager)\s+\$/', $constructor)) { // Verificar si NO termina en Interface if (!preg_match('/interface\s+\$/', $constructor)) { $result->addWarning("Constructor recibe clase concreta en lugar de interface: {$file}"); } } } } // Verificar si tiene new dentro del código (excepto DTOs, VOs, Entities) if (preg_match('/new\s+([A-Z][a-zA-Z]+)(Service|Repository|Manager|Controller|Builder)\s*\(/i', $content)) { $result->addError("Instanciación directa de clase de infraestructura (debe usar DI): {$file}"); } } private function checkFileLength(string $content, string $file, ValidationResult $result): void { $lines = substr_count($content, "\n") + 1; if ($lines > 500) { $result->addWarning("Archivo excede 500 líneas ({$lines}) - considerar refactorizar: {$file}"); } elseif ($lines > 300) { $result->addInfo("ℹ️ Archivo tiene {$lines} líneas (recomendado: <300, aceptable: <500): {$file}"); } } private function checkInterfaceSize(string $content, string $file, ValidationResult $result): void { // Contar métodos públicos en la interface $methodCount = preg_match_all('/public\s+function\s+\w+\s*\(/i', $content); if ($methodCount > 10) { $result->addError("Interface tiene {$methodCount} métodos (máx 10) - viola ISP: {$file}"); } elseif ($methodCount > 5) { $result->addWarning("Interface tiene {$methodCount} métodos (recomendado: 3-5) - considerar dividir: {$file}"); } } private function checkEncapsulation(string $content, string $file, ValidationResult $result): void { // Buscar propiedades public (excluir const y function) if (preg_match('/\bpublic\s+(?!const|function|static\s+function)\$\w+/i', $content)) { $result->addWarning("Propiedades públicas encontradas - usar private/protected: {$file}"); } } private function checkNoDirectInstantiation(string $content, string $file, ValidationResult $result): void { // Permitir new de: DTOs, Value Objects, Entities, Exceptions $allowedPatterns = [ 'DTO', 'Request', 'Response', 'Exception', 'ComponentName', 'ComponentConfiguration', 'ComponentVisibility', 'Color', 'Url', 'Email', 'ComponentId', 'MenuItem', 'self', 'static', 'parent', 'stdClass', 'DateTime' ]; if (preg_match_all('/new\s+([A-Z][a-zA-Z]+)\s*\(/i', $content, $matches)) { foreach ($matches[1] as $className) { $isAllowed = false; foreach ($allowedPatterns as $pattern) { if (strpos($className, $pattern) !== false || $className === $pattern) { $isAllowed = true; break; } } if (!$isAllowed) { $result->addWarning("Instanciación directa de '{$className}' en Domain/Application - considerar inyección: {$file}"); } } } } public function getPhaseNumber(): int|string { return 5; } public function getPhaseDescription(): string { return 'General SOLID (todos los archivos)'; } }