addInfo("Validando estructura de carpetas para: {$componentName}"); // Buscar el módulo en Admin/ o Public/ $modulePath = $this->findModulePath($componentName, $themePath); if ($modulePath === null) { $result->addError("Módulo no encontrado en Admin/ ni Public/"); return $result; } $context = basename(dirname($modulePath)); $result->addInfo("Módulo encontrado en: {$context}/{$componentName}/"); // Validar nombre del módulo (debe coincidir con PascalCase esperado) $this->validateModuleName($componentName, $modulePath, $result); // Validar capas obligatorias $this->validateRequiredLayers($modulePath, $result); // Validar contenido de cada capa $this->validateLayerContents($modulePath, $result); // Validar profundidad de carpetas $this->validateDepth($modulePath, $result); // Buscar carpetas prohibidas $this->findForbiddenFolders($modulePath, $result); return $result; } private function findModulePath(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 validateModuleName(string $componentName, string $modulePath, ValidationResult $result): void { // Convertir a PascalCase esperado $pascalCaseExpected = str_replace('-', '', ucwords($componentName, '-')); $actualFolderName = basename($modulePath); // El nombre de la carpeta debe ser PascalCase (PSR-4 standard) if ($actualFolderName !== $pascalCaseExpected) { $result->addError("Nombre de carpeta '{$actualFolderName}' no coincide con PascalCase esperado '{$pascalCaseExpected}'"); } else { $result->addInfo("✓ Nombre de carpeta en PascalCase: {$actualFolderName}"); } } private function validateRequiredLayers(string $modulePath, ValidationResult $result): void { // SHARED KERNEL PATTERN: // Los módulos individuales solo requieren Infrastructure/ // Domain/ y Application/ están a nivel de contexto (Admin/, Public/) // Infrastructure/ es OBLIGATORIA $infrastructurePath = $modulePath . '/Infrastructure'; if (!is_dir($infrastructurePath)) { $result->addError("Falta capa obligatoria: Infrastructure/"); } else { $result->addInfo("✓ Capa Infrastructure/ existe"); } // Domain/ y Application/ son OPCIONALES (Shared Kernel a nivel contexto) foreach (['Domain', 'Application'] as $optionalLayer) { $layerPath = $modulePath . '/' . $optionalLayer; if (is_dir($layerPath)) { $result->addInfo("✓ Capa {$optionalLayer}/ existe (específica del módulo)"); } } $result->addInfo("ℹ️ Arquitectura Shared Kernel: Domain/ y Application/ a nivel contexto (Admin/, Public/)"); } private function validateLayerContents(string $modulePath, ValidationResult $result): void { // Validar Domain/ $this->validateLayerContent( $modulePath . '/Domain', 'Domain', self::ALLOWED_IN_DOMAIN, $result ); // Validar Application/ $this->validateLayerContent( $modulePath . '/Application', 'Application', self::ALLOWED_IN_APPLICATION, $result ); // Validar Infrastructure/ $this->validateLayerContent( $modulePath . '/Infrastructure', 'Infrastructure', self::ALLOWED_IN_INFRASTRUCTURE, $result ); } private function validateLayerContent(string $layerPath, string $layerName, array $allowedFolders, ValidationResult $result): void { if (!is_dir($layerPath)) { return; // Ya reportado en validateRequiredLayers } $items = scandir($layerPath); $folders = array_filter($items, function($item) use ($layerPath) { return $item !== '.' && $item !== '..' && is_dir($layerPath . '/' . $item); }); foreach ($folders as $folder) { // Validar que la carpeta está en la lista permitida (PascalCase) if (!in_array($folder, $allowedFolders, true)) { $result->addError("Carpeta NO permitida en {$layerName}/: '{$folder}' (permitidas: " . implode(', ', $allowedFolders) . ")"); } } // Validaciones especiales para infrastructure/ui/ if ($layerName === 'infrastructure' && in_array('ui', $folders, true)) { $this->validateUIStructure($layerPath . '/ui', $result); } // Validaciones especiales para infrastructure/api/ if ($layerName === 'infrastructure' && in_array('api', $folders, true)) { $this->validateAPIStructure($layerPath . '/api', $result); } } private function validateUIStructure(string $uiPath, ValidationResult $result): void { if (!is_dir($uiPath)) { return; } $items = scandir($uiPath); $folders = array_filter($items, function($item) use ($uiPath) { return $item !== '.' && $item !== '..' && is_dir($uiPath . '/' . $item); }); $allowedInUI = ['assets', 'templates', 'views']; foreach ($folders as $folder) { if (!in_array($folder, $allowedInUI, true)) { $result->addWarning("Carpeta en ui/: '{$folder}' (esperadas: " . implode(', ', $allowedInUI) . ")"); } } // Si existe assets/, validar que tiene css/ y/o js/ if (in_array('assets', $folders, true)) { $assetsPath = $uiPath . '/assets'; $assetsItems = scandir($assetsPath); $assetsFolders = array_filter($assetsItems, function($item) use ($assetsPath) { return $item !== '.' && $item !== '..' && is_dir($assetsPath . '/' . $item); }); $allowedInAssets = ['css', 'js', 'images', 'fonts']; foreach ($assetsFolders as $folder) { if (!in_array($folder, $allowedInAssets, true)) { $result->addWarning("Carpeta en ui/assets/: '{$folder}' (esperadas: " . implode(', ', $allowedInAssets) . ")"); } } } } private function validateAPIStructure(string $apiPath, ValidationResult $result): void { if (!is_dir($apiPath)) { return; } $items = scandir($apiPath); $folders = array_filter($items, function($item) use ($apiPath) { return $item !== '.' && $item !== '..' && is_dir($apiPath . '/' . $item); }); $allowedInAPI = ['wordpress']; foreach ($folders as $folder) { if (!in_array($folder, $allowedInAPI, true)) { $result->addWarning("Carpeta en api/: '{$folder}' (esperada: wordpress)"); } } } private function validateDepth(string $modulePath, ValidationResult $result): void { // Máximo 5 niveles desde módulo: module/layer/category/subcategory/file.php $maxDepth = 5; $deepPaths = []; $this->scanDirectoryDepth($modulePath, $modulePath, 0, $maxDepth, $deepPaths); if (!empty($deepPaths)) { foreach ($deepPaths as $path => $depth) { $result->addWarning("Ruta muy profunda ({$depth} niveles): {$path}"); } } } private function scanDirectoryDepth(string $basePath, string $currentPath, int $currentDepth, int $maxDepth, array &$deepPaths): void { if ($currentDepth > $maxDepth) { $relativePath = str_replace($basePath . '/', '', $currentPath); $deepPaths[$relativePath] = $currentDepth; return; } if (!is_dir($currentPath)) { return; } $items = scandir($currentPath); foreach ($items as $item) { if ($item === '.' || $item === '..') { continue; } $itemPath = $currentPath . '/' . $item; if (is_dir($itemPath)) { $this->scanDirectoryDepth($basePath, $itemPath, $currentDepth + 1, $maxDepth, $deepPaths); } } } private function findForbiddenFolders(string $modulePath, ValidationResult $result): void { $foundForbidden = []; $this->scanForForbiddenNames($modulePath, $modulePath, $foundForbidden); if (!empty($foundForbidden)) { foreach ($foundForbidden as $path) { $relativePath = str_replace($modulePath . '/', '', $path); $folderName = basename($path); $result->addError("❌ Carpeta PROHIBIDA encontrada: {$relativePath}/ ('{$folderName}' NO es arquitectónicamente válido)"); } } else { $result->addInfo("✓ Sin carpetas prohibidas (helpers, utils, etc.)"); } } private function scanForForbiddenNames(string $basePath, string $currentPath, array &$foundForbidden): void { if (!is_dir($currentPath)) { return; } $items = scandir($currentPath); foreach ($items as $item) { if ($item === '.' || $item === '..') { continue; } $itemPath = $currentPath . '/' . $item; if (is_dir($itemPath)) { // Verificar si el nombre de la carpeta está prohibido if (in_array(strtolower($item), self::FORBIDDEN_NAMES, true)) { $foundForbidden[] = $itemPath; } // Recursivo $this->scanForForbiddenNames($basePath, $itemPath, $foundForbidden); } } } public function getPhaseNumber(): int|string { return 'structure'; } public function getPhaseDescription(): string { return 'Folder Structure (Clean Architecture)'; } }