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>
This commit is contained in:
FrankZamora
2025-11-26 22:53:34 -06:00
parent a2548ab5c2
commit 90863cd8f5
92 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,333 @@
<?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)';
}
}