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:
333
Shared/Infrastructure/Validators/FolderStructureValidator.php
Normal file
333
Shared/Infrastructure/Validators/FolderStructureValidator.php
Normal 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)';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user