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();
});