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>
300 lines
11 KiB
PHP
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)';
|
|
}
|
|
}
|