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,305 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Validators;
/**
* Validador de Fase 05: Validación General SOLID
*
* Escanea TODOS los archivos .php del componente y valida:
* - Domain Purity (sin WordPress, sin echo, sin HTML)
* - Application Purity (sin WordPress)
* - CERO CSS hardcodeado
* - Dependency Injection
* - SRP (Single Responsibility - tamaño de archivos)
* - ISP (Interface Segregation - tamaño de interfaces)
* - Encapsulación (propiedades private/protected)
* - NO instanciación directa en Domain/Application
*/
final class Phase05Validator implements PhaseValidatorInterface
{
private const WORDPRESS_FUNCTIONS = [
'global \$wpdb',
'add_action',
'add_filter',
'get_option',
'update_option',
'wp_enqueue_',
'register_post_type',
'add_shortcode',
'\$_POST',
'\$_GET',
'\$_SESSION',
'\$_COOKIE',
];
private const CSS_PATTERNS = [
'style\s*=\s*["\']', // style="..."
'<<<STYLE', // HEREDOC con STYLE
'<<<CSS', // HEREDOC con CSS
'onmouseover\s*=', // inline JS events
'onmouseout\s*=',
'onclick\s*=',
'onload\s*=',
'onchange\s*=',
];
private int $filesScanned = 0;
public function validate(string $componentName, string $themePath): ValidationResult
{
$result = new ValidationResult();
$result->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)';
}
}