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

334 lines
12 KiB
PHP
Raw Permalink 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 Estructura de Carpetas Clean Architecture (PSR-4 PascalCase)
*
* Valida que:
* - Solo existen contextos permitidos (Admin/, Public/, Shared/)
* - Nombres de módulos en PascalCase (PSR-4 standard)
* - Shared Kernel: Infrastructure/ obligatoria, Domain/Application/ a nivel contexto
* - Carpetas dentro de capas son solo las arquitectónicamente permitidas
* - NO hay carpetas inventadas (Helpers/, Utils/, Lib/, etc.)
* - Niveles de profundidad correctos
*/
final class FolderStructureValidator implements PhaseValidatorInterface
{
private const ALLOWED_CONTEXTS = ['Admin', 'Public', 'Shared'];
private const REQUIRED_LAYERS = ['Domain', 'Application', 'Infrastructure'];
private const ALLOWED_IN_DOMAIN = ['Contracts', 'ValueObjects', 'Exceptions'];
private const ALLOWED_IN_APPLICATION = ['UseCases', 'Dtos', 'Contracts', 'Services'];
private const ALLOWED_IN_INFRASTRUCTURE = ['Persistence', 'Api', 'Ui', 'Services', 'WordPress', 'Traits', 'Di', 'Scripts', 'Validators'];
private const FORBIDDEN_NAMES = ['Helpers', 'Utils', 'Utilities', 'Lib', 'Libs', 'Core', 'Common', 'Base', 'Models'];
public function validate(string $componentName, string $themePath): ValidationResult
{
$result = new ValidationResult();
$result->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)';
}
}