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

273 lines
11 KiB
PHP
Raw 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 03: Renderers (DB→HTML/CSS)
*
* Valida que:
* - Renderer existe en ubicación correcta
* - Namespace y clase correctos
* - Inyecta CSSGeneratorInterface
* - CERO CSS hardcodeado (CRÍTICO)
* - Tiene métodos obligatorios (render, getVisibilityClasses)
* - Usa escaping correcto
* - NO usa WordPress de BD
*/
final class Phase03Validator implements PhaseValidatorInterface
{
/**
* Componentes especiales que NO requieren CSSGeneratorInterface ni getVisibilityClasses
*
* Estos son componentes de inyeccion (no visuales) que:
* - NO renderizan HTML visual
* - NO generan CSS dinamico (inyectan CSS del usuario tal cual)
* - NO necesitan clases de visibilidad responsive
* - Inyectan codigo en hooks (wp_head, wp_footer)
*/
private const INJECTION_COMPONENTS = ['theme-settings'];
public function validate(string $componentName, string $themePath): ValidationResult
{
$result = new ValidationResult();
$result->addInfo("Validando Renderer para: {$componentName}");
// Determinar contexto (admin o public) - intentar ambos
$rendererPath = $this->findRendererPath($componentName, $themePath);
if ($rendererPath === null) {
$result->addError("Renderer no encontrado en Public/ ni Admin/");
$pascalCaseName = str_replace('-', '', ucwords($componentName, '-'));
$result->addInfo("Ubicación esperada: Public/{$pascalCaseName}/Infrastructure/Ui/*Renderer.php");
return $result;
}
$result->addInfo("Archivo encontrado: {$rendererPath}");
// Leer contenido del archivo
$content = file_get_contents($rendererPath);
// Validaciones
$this->validateNamespaceAndClass($content, $componentName, $result);
// Validaciones especiales para componentes de inyeccion
$isInjectionComponent = in_array($componentName, self::INJECTION_COMPONENTS, true);
if ($isInjectionComponent) {
$result->addInfo("✓ Componente de inyección - validaciones CSS/visibility omitidas");
} else {
$this->validateCSSGeneratorInjection($content, $result);
$this->validateNoCSSHardcoded($content, $result); // CRÍTICO
$this->validateGetVisibilityClassesMethod($content, $result);
}
$this->validateRenderMethod($content, $componentName, $result);
$this->validateEscaping($content, $result);
$this->validateNoDirectDatabaseAccess($content, $result);
// Estadísticas
$fileSize = filesize($rendererPath);
$lineCount = substr_count($content, "\n") + 1;
$result->setStat('Archivo', basename($rendererPath));
$result->setStat('Líneas', $lineCount);
$result->setStat('Tamaño', $fileSize . ' bytes');
if ($lineCount > 500) {
$result->addWarning("Archivo excede 500 líneas ({$lineCount}) - considerar refactorizar.");
} elseif ($lineCount > 300) {
$result->addInfo(" Archivo tiene {$lineCount} líneas (recomendado: <300, aceptable: <500)");
}
return $result;
}
private function findRendererPath(string $componentName, string $themePath): ?string
{
// Convertir kebab-case a PascalCase (para carpetas y archivos)
$pascalCaseName = str_replace('-', '', ucwords($componentName, '-'));
// Intentar en Public/ con PascalCase (PSR-4 standard)
$publicPath = $themePath . '/Public/' . $pascalCaseName . '/Infrastructure/Ui/' . $pascalCaseName . 'Renderer.php';
if (file_exists($publicPath)) {
return $publicPath;
}
// Intentar en Admin/ con PascalCase (PSR-4 standard)
$adminPath = $themePath . '/Admin/' . $pascalCaseName . '/Infrastructure/Ui/' . $pascalCaseName . 'Renderer.php';
if (file_exists($adminPath)) {
return $adminPath;
}
return null;
}
private function validateNamespaceAndClass(string $content, string $componentName, ValidationResult $result): void
{
$pascalCaseName = str_replace('-', '', ucwords($componentName, '-'));
// Validar namespace (Ui en PascalCase, primera letra mayúscula)
if (!preg_match('/namespace\s+ROITheme\\\\(Public|Admin)\\\\' . preg_quote($pascalCaseName, '/') . '\\\\Infrastructure\\\\Ui;/', $content)) {
$result->addError("Namespace incorrecto. Debe ser: ROITheme\\Public\\{$pascalCaseName}\\Infrastructure\\Ui");
}
// Validar clase final
if (!preg_match('/final\s+class\s+' . preg_quote($pascalCaseName, '/') . 'Renderer/', $content)) {
$result->addError("Clase debe ser: final class {$pascalCaseName}Renderer");
}
}
private function validateCSSGeneratorInjection(string $content, ValidationResult $result): void
{
// Verificar que constructor recibe CSSGeneratorInterface
if (!preg_match('/public\s+function\s+__construct\([^)]*CSSGeneratorInterface\s+\$cssGenerator/s', $content)) {
$result->addError("Constructor NO inyecta CSSGeneratorInterface (debe recibir interfaz, no clase concreta)");
}
// Verificar propiedad privada
if (!preg_match('/private\s+CSSGeneratorInterface\s+\$cssGenerator/', $content)) {
$result->addError("Falta propiedad: private CSSGeneratorInterface \$cssGenerator");
}
}
private function validateRenderMethod(string $content, string $componentName, ValidationResult $result): void
{
// Verificar método render existe (aceptar Component object o array - Component es preferido)
$hasComponentSignature = preg_match('/public\s+function\s+render\s*\(\s*Component\s+\$component\s*\)\s*:\s*string/', $content);
$hasArraySignature = preg_match('/public\s+function\s+render\s*\(\s*array\s+\$data\s*\)\s*:\s*string/', $content);
if (!$hasComponentSignature && !$hasArraySignature) {
$result->addError("Falta método: public function render(Component \$component): string o render(array \$data): string");
} elseif ($hasArraySignature) {
$result->addWarning("Método usa render(array \$data) - Considerar migrar a render(Component \$component) para type safety");
}
// Componentes de inyeccion no requieren validacion de visibility
$isInjectionComponent = in_array($componentName, self::INJECTION_COMPONENTS, true);
if (!$isInjectionComponent) {
// Verificar que valida is_enabled
$hasArrayValidation = preg_match('/\$data\s*\[\s*[\'"]visibility[\'"]\s*\]\s*\[\s*[\'"]is_enabled[\'"]\s*\]/', $content);
$hasComponentValidation = preg_match('/\$component->getVisibility\(\)->isEnabled\(\)/', $content);
if (!$hasArrayValidation && !$hasComponentValidation) {
$result->addWarning("Método render() debería validar visibilidad (is_enabled)");
}
}
}
private function validateNoCSSHardcoded(string $content, ValidationResult $result): void
{
$violations = [];
// Detectar style="..." (pero permitir algunos casos específicos del design system)
if (preg_match_all('/style\s*=\s*["\']/', $content, $matches, PREG_OFFSET_CAPTURE)) {
// Contar ocurrencias
$count = count($matches[0]);
// Si hay más de 2-3 (casos permitidos del design system), es violación
if ($count > 3) {
$violations[] = "Encontrado style=\"...\" inline ({$count} ocurrencias) - PROHIBIDO";
} elseif ($count > 0) {
$result->addWarning("Encontrado {$count} uso(s) de style=\"...\" - Verificar que sea del design system aprobado");
}
}
// Detectar heredoc <<<STYLE o <<<CSS
if (preg_match('/<<<(STYLE|CSS)/', $content)) {
$violations[] = "Encontrado heredoc <<<STYLE o <<<CSS - PROHIBIDO";
}
// Detectar eventos inline (onclick, onmouseover, etc.)
$inlineEvents = ['onclick', 'onmouseover', 'onmouseout', 'onload', 'onchange', 'onsubmit'];
foreach ($inlineEvents as $event) {
if (preg_match('/' . $event . '\s*=/', $content)) {
$violations[] = "Encontrado evento inline '{$event}=\"...\"' - PROHIBIDO";
}
}
// Verificar que usa $this->cssGenerator->generate()
if (!preg_match('/\$this->cssGenerator->generate\s*\(/', $content)) {
$violations[] = "NO usa \$this->cssGenerator->generate() - CSS debe generarse vía servicio";
}
if (!empty($violations)) {
foreach ($violations as $violation) {
$result->addError("❌ CRÍTICO - CSS HARDCODEADO: {$violation}");
}
} else {
$result->addInfo("✓ CERO CSS hardcodeado detectado");
}
}
private function validateGetVisibilityClassesMethod(string $content, ValidationResult $result): void
{
// Verificar firma del método
if (!preg_match('/private\s+function\s+getVisibilityClasses\s*\(\s*bool\s+\$desktop\s*,\s*bool\s+\$mobile\s*\)\s*:\s*\?string/', $content)) {
$result->addError("Falta método: private function getVisibilityClasses(bool \$desktop, bool \$mobile): ?string");
return;
}
// Verificar implementación de tabla Bootstrap
$requiredPatterns = [
'/d-none d-lg-block/' => "Patrón 'd-none d-lg-block' (desktop only)",
'/d-lg-none/' => "Patrón 'd-lg-none' (mobile only)",
];
foreach ($requiredPatterns as $pattern => $description) {
if (!preg_match($pattern, $content)) {
$result->addWarning("getVisibilityClasses() puede no implementar correctamente: {$description}");
}
}
}
private function validateEscaping(string $content, ValidationResult $result): void
{
$escapingUsed = false;
if (preg_match('/esc_html\s*\(/', $content)) {
$escapingUsed = true;
}
if (preg_match('/esc_attr\s*\(/', $content)) {
$escapingUsed = true;
}
if (preg_match('/esc_url\s*\(/', $content)) {
$escapingUsed = true;
}
if (!$escapingUsed) {
$result->addWarning("No se detectó uso de esc_html(), esc_attr() o esc_url() - Verificar escaping");
}
}
private function validateNoDirectDatabaseAccess(string $content, ValidationResult $result): void
{
// Verificar que NO usa global $wpdb
if (preg_match('/global\s+\$wpdb/', $content)) {
$result->addError("Renderer usa 'global \$wpdb' - Acceso a BD debe estar en Repository, NO en Renderer");
}
// Verificar que NO usa funciones directas de BD
$dbFunctions = ['get_option', 'update_option', 'get_post_meta', 'update_post_meta'];
foreach ($dbFunctions as $func) {
if (preg_match('/\b' . $func . '\s*\(/', $content)) {
$result->addWarning("Renderer usa '{$func}()' - Considerar mover a Repository");
}
}
}
public function getPhaseNumber(): int|string
{
return 3;
}
public function getPhaseDescription(): string
{
return 'Renderers (DB→HTML/CSS)';
}
}