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

300 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Validators;
/**
* Validador de Conflictos CSS
*
* Detecta archivos CSS en Assets/ que podrían sobrescribir
* estilos generados dinámicamente por Renderers.
*
* Valida que:
* - NO existan archivos CSS hardcodeados para componentes con Renderer dinámico
* - NO haya reglas !important que sobrescriban estilos dinámicos
* - NO se encolen CSS externos que conflictúen con Renderers
*
* @since 1.0.0
*/
final class CSSConflictValidator implements PhaseValidatorInterface
{
/**
* Mapeo de componentes con Renderer dinámico a sus posibles archivos CSS conflictivos
*/
private const DYNAMIC_RENDERER_COMPONENTS = [
'top-notification-bar' => [
'css_files' => ['componente-top-bar.css', 'top-notification-bar.css'],
'css_classes' => ['.top-notification-bar', '.top-bar'],
],
'navbar' => [
'css_files' => ['componente-navbar.css', 'navbar.css'],
'css_classes' => ['.navbar', '.nav-link', '.navbar-brand', '.dropdown-menu'],
],
'cta-lets-talk' => [
'css_files' => ['componente-boton-lets-talk.css', 'cta-lets-talk.css'],
'css_classes' => ['.btn-lets-talk', '.cta-lets-talk'],
],
];
public function validate(string $componentName, string $themePath): ValidationResult
{
$result = new ValidationResult();
$result->addInfo("Validando conflictos CSS para: {$componentName}");
// Solo validar componentes con Renderer dinámico conocidos
if (!isset(self::DYNAMIC_RENDERER_COMPONENTS[$componentName])) {
$result->addInfo("Componente no tiene Renderer dinámico registrado - Omitiendo validación CSS");
return $result;
}
$componentConfig = self::DYNAMIC_RENDERER_COMPONENTS[$componentName];
// 1. Verificar que existe un Renderer dinámico
$hasRenderer = $this->hasRendererFile($componentName, $themePath);
if (!$hasRenderer) {
$result->addInfo("No se encontró Renderer dinámico - Validación CSS no aplica");
return $result;
}
$result->addInfo("✓ Renderer dinámico detectado");
// 2. Buscar archivos CSS conflictivos en Assets/css/
$cssData = $this->validateAssetsCSSFiles($componentName, $componentConfig, $themePath, $result);
// 3. Validar enqueue-scripts.php y determinar errores vs warnings
$this->validateEnqueueScripts($componentName, $componentConfig, $themePath, $result, $cssData);
return $result;
}
/**
* Verifica si existe un Renderer para el componente
*/
private function hasRendererFile(string $componentName, string $themePath): bool
{
$pascalCaseName = str_replace('-', '', ucwords($componentName, '-'));
$publicPath = $themePath . '/Public/' . $pascalCaseName . '/Infrastructure/Ui/' . $pascalCaseName . 'Renderer.php';
if (file_exists($publicPath)) {
return true;
}
$adminPath = $themePath . '/Admin/' . $pascalCaseName . '/Infrastructure/Ui/' . $pascalCaseName . 'Renderer.php';
if (file_exists($adminPath)) {
return true;
}
return false;
}
/**
* Valida archivos CSS en Assets/css/
*
* @return array{files: string[], important: array<string, int>} Archivos encontrados y violaciones
*/
private function validateAssetsCSSFiles(
string $componentName,
array $componentConfig,
string $themePath,
ValidationResult $result
): array {
$assetsPath = $themePath . '/Assets/css';
$cssFilesFound = [];
$importantViolations = [];
if (!is_dir($assetsPath)) {
$result->addInfo("No existe carpeta Assets/css/");
return ['files' => [], 'important' => []];
}
foreach ($componentConfig['css_files'] as $cssFileName) {
$cssFilePath = $assetsPath . '/' . $cssFileName;
if (file_exists($cssFilePath)) {
$cssFilesFound[] = $cssFileName;
$content = file_get_contents($cssFilePath);
// Buscar reglas !important
$importantCount = $this->countImportantRules($content);
if ($importantCount > 0) {
$importantViolations[$cssFileName] = $importantCount;
}
// Buscar clases CSS del componente
$conflictingClasses = $this->findConflictingClasses($content, $componentConfig['css_classes']);
if (!empty($conflictingClasses)) {
$result->addWarning(
"Archivo '{$cssFileName}' define clases del componente: " .
implode(', ', $conflictingClasses)
);
}
}
}
// Reportar archivos encontrados
if (!empty($cssFilesFound)) {
$result->addWarning(
"Archivos CSS hardcodeados encontrados en Assets/css/: " .
implode(', ', $cssFilesFound)
);
$result->addInfo(
"⚠️ Estos archivos podrían sobrescribir estilos generados dinámicamente por el Renderer"
);
} else {
$result->addInfo("✓ Sin archivos CSS hardcodeados conflictivos en Assets/css/");
}
// Stats
$result->setStat('Archivos CSS conflictivos', count($cssFilesFound));
$result->setStat('Reglas !important', array_sum($importantViolations));
return ['files' => $cssFilesFound, 'important' => $importantViolations];
}
/**
* Cuenta reglas !important en contenido CSS
*/
private function countImportantRules(string $content): int
{
preg_match_all('/!important/i', $content, $matches);
return count($matches[0]);
}
/**
* Encuentra clases CSS conflictivas en el contenido
*/
private function findConflictingClasses(string $content, array $cssClasses): array
{
$found = [];
foreach ($cssClasses as $className) {
// Escapar el punto para regex
$pattern = '/' . preg_quote($className, '/') . '\s*[{,]/';
if (preg_match($pattern, $content)) {
$found[] = $className;
}
}
return $found;
}
/**
* Valida Inc/enqueue-scripts.php para detectar CSS encolados
*
* @param array{files: string[], important: array<string, int>} $cssData Datos de archivos CSS encontrados
*/
private function validateEnqueueScripts(
string $componentName,
array $componentConfig,
string $themePath,
ValidationResult $result,
array $cssData
): void {
$enqueueScriptsPath = $themePath . '/Inc/enqueue-scripts.php';
if (!file_exists($enqueueScriptsPath)) {
$result->addWarning("No se encontró Inc/enqueue-scripts.php");
return;
}
$content = file_get_contents($enqueueScriptsPath);
$enqueuedFiles = [];
$commentedFiles = [];
foreach ($componentConfig['css_files'] as $cssFileName) {
// Buscar si el archivo está siendo encolado (multiline - wp_enqueue_style puede tener saltos de línea)
$pattern = '/wp_enqueue_style\s*\([^;]*' . preg_quote($cssFileName, '/') . '[^;]*\);/s';
if (preg_match($pattern, $content, $match)) {
// Verificar si está comentado
$matchPos = strpos($content, $match[0]);
$lineStart = strrpos(substr($content, 0, $matchPos), "\n") + 1;
$lineContent = substr($content, $lineStart, $matchPos - $lineStart);
// Verificar si la línea está en un bloque comentado /* */
$beforeMatch = substr($content, 0, $matchPos);
$lastCommentOpen = strrpos($beforeMatch, '/*');
$lastCommentClose = strrpos($beforeMatch, '*/');
$isCommented = false;
if ($lastCommentOpen !== false) {
if ($lastCommentClose === false || $lastCommentOpen > $lastCommentClose) {
$isCommented = true;
}
}
// También verificar comentario de línea //
if (strpos($lineContent, '//') !== false) {
$isCommented = true;
}
if ($isCommented) {
$commentedFiles[] = $cssFileName;
} else {
$enqueuedFiles[] = $cssFileName;
}
}
}
// Reportar resultados de enqueue
if (!empty($enqueuedFiles)) {
$result->addError(
"❌ CRÍTICO: CSS conflictivo ACTIVO en enqueue-scripts.php: " .
implode(', ', $enqueuedFiles)
);
$result->addInfo(
"SOLUCIÓN: Comentar wp_enqueue_style() para estos archivos ya que el Renderer genera CSS dinámico"
);
}
if (!empty($commentedFiles)) {
$result->addInfo(
"✓ CSS correctamente deshabilitado en enqueue-scripts.php: " .
implode(', ', $commentedFiles)
);
}
if (empty($enqueuedFiles) && empty($commentedFiles)) {
$result->addInfo("✓ Sin conflictos de enqueue detectados");
}
// Reportar !important violations basado en estado de enqueue
$importantViolations = $cssData['important'] ?? [];
if (!empty($importantViolations)) {
foreach ($importantViolations as $file => $count) {
if (in_array($file, $enqueuedFiles, true)) {
// CSS activo con !important → ERROR CRÍTICO
$result->addError(
"❌ CRÍTICO: '{$file}' ACTIVO con {$count} regla(s) !important que SOBRESCRIBEN estilos dinámicos"
);
} elseif (in_array($file, $commentedFiles, true)) {
// CSS deshabilitado con !important → WARNING (considerar eliminar archivo)
$result->addWarning(
"'{$file}' deshabilitado pero tiene {$count} regla(s) !important - Considerar eliminar archivo"
);
} else {
// Archivo existe pero no está en enqueue-scripts.php → WARNING
$result->addWarning(
"'{$file}' no está en enqueue-scripts.php pero tiene {$count} regla(s) !important"
);
}
}
}
}
public function getPhaseNumber(): int|string
{
return 'css';
}
public function getPhaseDescription(): string
{
return 'CSS Conflicts (Assets vs Dynamic Renderers)';
}
}