Migración completa a Clean Architecture con componentes funcionales
- Reorganización de estructura: Admin/, Public/, Shared/, Schemas/ - 12 componentes migrados: TopNotificationBar, Navbar, CtaLetsTalk, Hero, FeaturedImage, TableOfContents, CtaBoxSidebar, SocialShare, CtaPost, RelatedPost, ContactForm, Footer - Panel de administración con tabs Bootstrap 5 funcionales - Schemas JSON para configuración de componentes - Renderers dinámicos con CSSGeneratorService (cero CSS hardcodeado) - FormBuilders para UI admin con Design System consistente - Fix: Bootstrap JS cargado en header para tabs funcionales - Fix: buildTextInput maneja valores mixed (bool/string) - Eliminación de estructura legacy (src/, admin/, assets/css/componente-*) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
305
shared/Infrastructure/Validators/Phase05Validator.php
Normal file
305
shared/Infrastructure/Validators/Phase05Validator.php
Normal 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)';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user