diff --git a/Public/Hero/Infrastructure/Ui/HeroRenderer.php b/Public/Hero/Infrastructure/Ui/HeroRenderer.php index 4eb5e664..dd22803f 100644 --- a/Public/Hero/Infrastructure/Ui/HeroRenderer.php +++ b/Public/Hero/Infrastructure/Ui/HeroRenderer.php @@ -5,7 +5,6 @@ namespace ROITheme\Public\Hero\Infrastructure\Ui; use ROITheme\Shared\Domain\Contracts\RendererInterface; use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface; -use ROITheme\Shared\Domain\Contracts\CriticalCSSCollectorInterface; use ROITheme\Shared\Domain\Entities\Component; /** @@ -25,17 +24,20 @@ use ROITheme\Shared\Domain\Entities\Component; * - Persistir datos * - Lógica de negocio * + * Cumple con: + * - DIP: Recibe CSSGeneratorInterface por constructor + * - SRP: Una responsabilidad (renderizar hero) + * - Clean Architecture: Infrastructure puede usar WordPress + * * @package ROITheme\Public\Hero\Infrastructure\Ui */ final class HeroRenderer implements RendererInterface { /** * @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS - * @param CriticalCSSCollectorInterface $criticalCollector Colector de CSS crítico */ public function __construct( - private CSSGeneratorInterface $cssGenerator, - private CriticalCSSCollectorInterface $criticalCollector + private CSSGeneratorInterface $cssGenerator ) {} public function render(Component $component): string @@ -50,11 +52,17 @@ final class HeroRenderer implements RendererInterface return ''; } - $css = $this->generateCSS($data); $html = $this->buildHTML($data); - // Siempre incluir CSS inline con el componente - // Nota: is_critical se reserva para futura implementación con output buffering + // Si is_critical=true, CSS ya fue inyectado en por CriticalCSSService + $isCritical = $data['visibility']['is_critical'] ?? false; + + if ($isCritical) { + return $html; // Solo HTML, sin CSS inline + } + + // CSS inline para componentes no críticos + $css = $this->generateCSS($data); return sprintf("\n%s", $css, $html); } @@ -86,7 +94,16 @@ final class HeroRenderer implements RendererInterface } } - private function generateCSS(array $data): string + /** + * Generar CSS usando CSSGeneratorService + * + * Este método es público para que CriticalCSSService pueda + * generar CSS crítico antes de wp_head sin duplicar lógica. + * + * @param array $data Datos del componente + * @return string CSS generado + */ + public function generateCSS(array $data): string { $colors = $data['colors'] ?? []; $typography = $data['typography'] ?? []; diff --git a/Public/Navbar/Infrastructure/Ui/NavbarRenderer.php b/Public/Navbar/Infrastructure/Ui/NavbarRenderer.php index e9b709cf..11cf590f 100644 --- a/Public/Navbar/Infrastructure/Ui/NavbarRenderer.php +++ b/Public/Navbar/Infrastructure/Ui/NavbarRenderer.php @@ -6,7 +6,6 @@ namespace ROITheme\Public\Navbar\Infrastructure\Ui; use ROITheme\Shared\Domain\Entities\Component; use ROITheme\Shared\Domain\Contracts\RendererInterface; use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface; -use ROITheme\Shared\Domain\Contracts\CriticalCSSCollectorInterface; use Walker_Nav_Menu; /** @@ -31,11 +30,9 @@ final class NavbarRenderer implements RendererInterface { /** * @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS - * @param CriticalCSSCollectorInterface $criticalCollector Colector de CSS crítico */ public function __construct( - private CSSGeneratorInterface $cssGenerator, - private CriticalCSSCollectorInterface $criticalCollector + private CSSGeneratorInterface $cssGenerator ) {} public function render(Component $component): string @@ -46,16 +43,18 @@ final class NavbarRenderer implements RendererInterface return ''; } - $css = $this->generateCSS($data); $html = $this->buildMenu($data); - // Siempre incluir CSS inline con el componente - // Nota: is_critical se reserva para futura implementación con output buffering - return sprintf( - "\n%s", - $css, - $html - ); + // Si is_critical=true, CSS ya fue inyectado en por CriticalCSSService + $isCritical = $data['visibility']['is_critical'] ?? false; + + if ($isCritical) { + return $html; // Solo HTML, sin CSS inline + } + + // CSS inline para componentes no críticos + $css = $this->generateCSS($data); + return sprintf("\n%s", $css, $html); } private function isEnabled(array $data): bool @@ -73,10 +72,13 @@ final class NavbarRenderer implements RendererInterface /** * Generar CSS usando CSSGeneratorService * + * Este método es público para que CriticalCSSService pueda + * generar CSS crítico antes de wp_head sin duplicar lógica. + * * @param array $data Datos del componente * @return string CSS generado */ - private function generateCSS(array $data): string + public function generateCSS(array $data): string { $css = ''; diff --git a/Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php b/Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php index 3aba6014..288995c8 100644 --- a/Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php +++ b/Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php @@ -5,7 +5,6 @@ namespace ROITheme\Public\TopNotificationBar\Infrastructure\Ui; use ROITheme\Shared\Domain\Contracts\RendererInterface; use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface; -use ROITheme\Shared\Domain\Contracts\CriticalCSSCollectorInterface; use ROITheme\Shared\Domain\Entities\Component; /** @@ -37,11 +36,9 @@ final class TopNotificationBarRenderer implements RendererInterface { /** * @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS - * @param CriticalCSSCollectorInterface $criticalCollector Colector de CSS crítico */ public function __construct( - private CSSGeneratorInterface $cssGenerator, - private CriticalCSSCollectorInterface $criticalCollector + private CSSGeneratorInterface $cssGenerator ) {} /** @@ -61,19 +58,19 @@ final class TopNotificationBarRenderer implements RendererInterface return ''; } - // Generar CSS usando CSSGeneratorService - $css = $this->generateCSS($data); - // Generar HTML $html = $this->buildHTML($data); - // Siempre incluir CSS inline con el componente - // Nota: is_critical se reserva para futura implementación con output buffering - return sprintf( - "\n%s", - $css, - $html - ); + // Si is_critical=true, CSS ya fue inyectado en por CriticalCSSService + $isCritical = $data['visibility']['is_critical'] ?? false; + + if ($isCritical) { + return $html; // Solo HTML, sin CSS inline + } + + // CSS inline para componentes no críticos + $css = $this->generateCSS($data); + return sprintf("\n%s", $css, $html); } /** @@ -165,10 +162,13 @@ final class TopNotificationBarRenderer implements RendererInterface /** * Generar CSS usando CSSGeneratorService * + * Este método es público para que CriticalCSSService pueda + * generar CSS crítico antes de wp_head sin duplicar lógica. + * * @param array $data Datos del componente * @return string CSS generado */ - private function generateCSS(array $data): string + public function generateCSS(array $data): string { $css = ''; diff --git a/Shared/Domain/Contracts/CriticalCSSCollectorInterface.php b/Shared/Domain/Contracts/CriticalCSSCollectorInterface.php deleted file mode 100644 index afd4ec94..00000000 --- a/Shared/Domain/Contracts/CriticalCSSCollectorInterface.php +++ /dev/null @@ -1,45 +0,0 @@ - [componentName => css] - */ - public function getAll(): array; - - /** - * Renderizar CSS crítico como tag ', - $css - ); - } - - /** - * {@inheritDoc} - */ - public function clear(): void - { - $this->criticalStyles = []; - } -} diff --git a/Shared/Infrastructure/Services/CriticalCSSService.php b/Shared/Infrastructure/Services/CriticalCSSService.php new file mode 100644 index 00000000..d6f3ecab --- /dev/null +++ b/Shared/Infrastructure/Services/CriticalCSSService.php @@ -0,0 +1,269 @@ + 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, + 'hero' => \ROITheme\Public\Hero\Infrastructure\Ui\HeroRenderer::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; + } +} diff --git a/Shared/Infrastructure/Wordpress/CriticalCSSHooksRegistrar.php b/Shared/Infrastructure/Wordpress/CriticalCSSHooksRegistrar.php index 7547dd4b..3b2f88a1 100644 --- a/Shared/Infrastructure/Wordpress/CriticalCSSHooksRegistrar.php +++ b/Shared/Infrastructure/Wordpress/CriticalCSSHooksRegistrar.php @@ -3,27 +3,32 @@ declare(strict_types=1); namespace ROITheme\Shared\Infrastructure\Wordpress; -use ROITheme\Shared\Domain\Contracts\CriticalCSSCollectorInterface; +use ROITheme\Shared\Infrastructure\Services\CriticalCSSService; /** * Registra hook wp_head para inyectar CSS crítico * * RESPONSABILIDAD: - * - Registrar hook wp_head - * - Delegar renderizado a CriticalCSSCollector + * - Registrar hook wp_head (priority 1) + * - Delegar renderizado a CriticalCSSService + * + * FLUJO: + * 1. wp_head (priority 1) → renderCriticalCSS() + * 2. CriticalCSSService consulta BD por componentes is_critical=true + * 3. Genera CSS usando Renderers y lo inyecta en + * 4. Los Renderers detectan is_critical y omiten CSS inline * * PATRÓN: - * - DIP: Recibe interface, no clase concreta - * - SRP: Solo registra hook, no contiene lógica de CSS + * - SRP: Solo registra hook, delega lógica a CriticalCSSService * - * UBICACIÓN: Infrastructure/Wordpress (según 00.02 líneas 307-311) + * UBICACIÓN: Infrastructure/Wordpress * * @package ROITheme\Shared\Infrastructure\Wordpress */ final class CriticalCSSHooksRegistrar { public function __construct( - private readonly CriticalCSSCollectorInterface $collector + private readonly CriticalCSSService $criticalCSSService ) {} /** @@ -37,10 +42,14 @@ final class CriticalCSSHooksRegistrar /** * Callback para wp_head + * + * Ejecuta CriticalCSSService que: + * - Consulta BD por componentes con is_critical=true + * - Genera CSS usando los Renderers + * - Output: */ public function renderCriticalCSS(): void { - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo $this->collector->render(); + $this->criticalCSSService->render(); } } diff --git a/functions-addon.php b/functions-addon.php index 070a3a0d..1771de7c 100644 --- a/functions-addon.php +++ b/functions-addon.php @@ -115,25 +115,26 @@ function roi_get_navbar_setting(string $group, string $attribute, $default = nul } // ============================================================================= -// CRITICAL CSS COLLECTOR SINGLETON +// CRITICAL CSS SERVICE SINGLETON // ============================================================================= /** - * Obtiene la instancia singleton del CriticalCSSCollector + * Obtiene la instancia singleton del CriticalCSSService * - * Patrón Singleton implementado via función para mantener una única instancia - * que será compartida por todos los Renderers y el HooksRegistrar + * Este servicio consulta la BD para componentes con is_critical=true + * y genera su CSS en wp_head ANTES de que los componentes rendericen. * - * @return \ROITheme\Shared\Domain\Contracts\CriticalCSSCollectorInterface + * @return \ROITheme\Shared\Infrastructure\Services\CriticalCSSService */ -function roi_get_critical_css_collector(): \ROITheme\Shared\Domain\Contracts\CriticalCSSCollectorInterface { - static $collector = null; +function roi_get_critical_css_service(): \ROITheme\Shared\Infrastructure\Services\CriticalCSSService { + global $wpdb; + static $cssGenerator = null; - if ($collector === null) { - $collector = new \ROITheme\Shared\Infrastructure\Services\CriticalCSSCollector(); + if ($cssGenerator === null) { + $cssGenerator = new \ROITheme\Shared\Infrastructure\Services\CSSGeneratorService(); } - return $collector; + return \ROITheme\Shared\Infrastructure\Services\CriticalCSSService::getInstance($wpdb, $cssGenerator); } // ============================================================================= @@ -205,23 +206,21 @@ function roi_render_component(string $componentName): string { // Obtener renderer específico para el componente $renderer = null; - // Crear instancia del CSSGeneratorService (reutilizable para todos los renderers que lo necesiten) + // Crear instancia del CSSGeneratorService (reutilizable para todos los renderers) $cssGenerator = new \ROITheme\Shared\Infrastructure\Services\CSSGeneratorService(); - // Obtener instancia singleton del CriticalCSSCollector - $criticalCollector = roi_get_critical_css_collector(); - switch ($componentName) { // Componentes con soporte de CSS Crítico (above-the-fold) + // Nota: Si is_critical=true, el CSS ya fue inyectado en por CriticalCSSService case 'top-notification-bar': - $renderer = new \ROITheme\Public\TopNotificationBar\Infrastructure\Ui\TopNotificationBarRenderer($cssGenerator, $criticalCollector); + $renderer = new \ROITheme\Public\TopNotificationBar\Infrastructure\Ui\TopNotificationBarRenderer($cssGenerator); break; case 'navbar': - $renderer = new \ROITheme\Public\Navbar\Infrastructure\Ui\NavbarRenderer($cssGenerator, $criticalCollector); + $renderer = new \ROITheme\Public\Navbar\Infrastructure\Ui\NavbarRenderer($cssGenerator); break; case 'hero': error_log("ROI Theme DEBUG: Creating HeroRenderer"); - $renderer = new \ROITheme\Public\Hero\Infrastructure\Ui\HeroRenderer($cssGenerator, $criticalCollector); + $renderer = new \ROITheme\Public\Hero\Infrastructure\Ui\HeroRenderer($cssGenerator); error_log("ROI Theme DEBUG: HeroRenderer created successfully"); break; @@ -283,13 +282,16 @@ function roi_render_component(string $componentName): string { /** * Registra el hook para inyectar CSS crítico en * - * IMPORTANTE: El HooksRegistrar usa la misma instancia singleton del collector - * que usan los Renderers, garantizando que el CSS recolectado se inyecte - * correctamente en wp_head con prioridad 1 (muy temprano). + * FLUJO: + * 1. wp_head (priority 1) → CriticalCSSService::render() + * 2. CriticalCSSService consulta BD por componentes con is_critical=true + * 3. Genera CSS usando los métodos públicos generateCSS() de los Renderers + * 4. Output: + * 5. Cuando los Renderers ejecutan, detectan is_critical y omiten CSS inline */ add_action('after_setup_theme', function() { - $criticalCollector = roi_get_critical_css_collector(); - $hooksRegistrar = new \ROITheme\Shared\Infrastructure\Wordpress\CriticalCSSHooksRegistrar($criticalCollector); + $criticalCSSService = roi_get_critical_css_service(); + $hooksRegistrar = new \ROITheme\Shared\Infrastructure\Wordpress\CriticalCSSHooksRegistrar($criticalCSSService); $hooksRegistrar->register(); });