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

306 lines
11 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 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)';
}
}