via wp_head (priority 1) * * FLUJO: * 1. wp_head (priority 1) → render() * 2. Consulta BD: componentes con visibility.is_critical = true * 3. Para cada componente: obtiene datos y llama Renderer->generateCSS() * 4. Output: * * IMPORTANTE: * - Este servicio se ejecuta ANTES de que los componentes rendericen * - Los Renderers detectan is_critical y omiten CSS inline (ya está en ) * * @package ROITheme\Shared\Infrastructure\Services */ final class CriticalCSSService { /** * Instancia singleton */ private static ?self $instance = null; /** * Tabla de configuraciones */ private string $tableName; /** * Cache de instancias de renderers * @var array */ private array $rendererInstances = []; /** * Mapa de componentes críticos y sus clases Renderer * @var array */ private const CRITICAL_RENDERERS = [ 'top-notification-bar' => \ROITheme\Public\TopNotificationBar\Infrastructure\Ui\TopNotificationBarRenderer::class, 'navbar' => \ROITheme\Public\Navbar\Infrastructure\Ui\NavbarRenderer::class, 'cta-lets-talk' => \ROITheme\Public\CtaLetsTalk\Infrastructure\Ui\CtaLetsTalkRenderer::class, 'hero' => \ROITheme\Public\Hero\Infrastructure\Ui\HeroRenderer::class, 'featured-image' => \ROITheme\Public\FeaturedImage\Infrastructure\Ui\FeaturedImageRenderer::class, 'table-of-contents' => \ROITheme\Public\TableOfContents\Infrastructure\Ui\TableOfContentsRenderer::class ]; /** * Constructor privado (singleton) */ private function __construct( private \wpdb $wpdb, private CSSGeneratorInterface $cssGenerator ) { $this->tableName = $this->wpdb->prefix . 'roi_theme_component_settings'; } /** * Obtiene la instancia singleton * * @param \wpdb $wpdb Instancia de WordPress Database * @param CSSGeneratorInterface $cssGenerator Generador de CSS * @return self */ public static function getInstance(\wpdb $wpdb, CSSGeneratorInterface $cssGenerator): self { if (self::$instance === null) { self::$instance = new self($wpdb, $cssGenerator); } return self::$instance; } /** * Renderiza CSS crítico en wp_head * * Este método se llama desde wp_head con priority 1 (muy temprano). * Genera y outputea el CSS de todos los componentes marcados como críticos. * * @return void */ public function render(): void { $criticalComponents = $this->getCriticalComponents(); if (empty($criticalComponents)) { return; } $allCSS = []; foreach ($criticalComponents as $componentName) { $css = $this->generateComponentCSS($componentName); if (!empty($css)) { $allCSS[] = "/* {$componentName} */\n" . $css; } } if (empty($allCSS)) { return; } $combinedCSS = implode("\n\n", $allCSS); printf( '' . "\n", $combinedCSS ); } /** * Obtiene lista de componentes con is_critical=true * * Consulta la BD para encontrar qué componentes tienen * visibility.is_critical = 1 (true) * * @return array Nombres de componentes críticos */ private function getCriticalComponents(): array { $sql = $this->wpdb->prepare( "SELECT DISTINCT component_name FROM {$this->tableName} WHERE group_name = %s AND attribute_name = %s AND attribute_value = %s", 'visibility', 'is_critical', '1' ); $rows = $this->wpdb->get_col($sql); return $rows ?: []; } /** * Genera CSS para un componente específico * * @param string $componentName Nombre del componente (kebab-case) * @return string CSS generado o string vacío */ private function generateComponentCSS(string $componentName): string { // Verificar que el componente tenga Renderer definido if (!isset(self::CRITICAL_RENDERERS[$componentName])) { return ''; } // Obtener datos del componente desde BD $data = $this->getComponentData($componentName); if (empty($data)) { return ''; } // Verificar que esté habilitado if (!($data['visibility']['is_enabled'] ?? false)) { return ''; } // Obtener o crear instancia del Renderer $renderer = $this->getRendererInstance($componentName); if ($renderer === null) { return ''; } // Llamar al método público generateCSS() del Renderer return $renderer->generateCSS($data); } /** * Obtiene datos del componente desde BD * * @param string $componentName Nombre del componente * @return array> Datos agrupados */ private function getComponentData(string $componentName): array { $sql = $this->wpdb->prepare( "SELECT group_name, attribute_name, attribute_value FROM {$this->tableName} WHERE component_name = %s ORDER BY group_name, attribute_name", $componentName ); $rows = $this->wpdb->get_results($sql, ARRAY_A); if (empty($rows)) { return []; } // Agrupar por grupo $settings = []; foreach ($rows as $row) { $groupName = $row['group_name']; $attributeName = $row['attribute_name']; $value = $this->unserializeValue($row['attribute_value']); if (!isset($settings[$groupName])) { $settings[$groupName] = []; } $settings[$groupName][$attributeName] = $value; } return $settings; } /** * Obtiene o crea instancia del Renderer * * @param string $componentName Nombre del componente * @return object|null Instancia del Renderer o null */ private function getRendererInstance(string $componentName): ?object { // Usar cache si ya existe if (isset($this->rendererInstances[$componentName])) { return $this->rendererInstances[$componentName]; } $rendererClass = self::CRITICAL_RENDERERS[$componentName] ?? null; if ($rendererClass === null || !class_exists($rendererClass)) { return null; } // Crear instancia inyectando CSSGeneratorInterface $this->rendererInstances[$componentName] = new $rendererClass($this->cssGenerator); return $this->rendererInstances[$componentName]; } /** * Deserializa un valor desde la BD * * @param string $value Valor serializado * @return mixed Valor deserializado */ private function unserializeValue(string $value): mixed { // Intentar decodificar JSON if (str_starts_with($value, '{') || str_starts_with($value, '[')) { $decoded = json_decode($value, true); if (json_last_error() === JSON_ERROR_NONE) { return $decoded; } } // Convertir booleanos if ($value === '1' || $value === '0') { return $value === '1'; } return $value; } }