feat: implement is_critical CSS injection via CriticalCSSService
- Created CriticalCSSService (singleton) that queries BD directly in wp_head - Service generates CSS BEFORE components render (priority 1) - Renderers check is_critical flag and skip inline CSS if true - Made generateCSS() public in Renderers for CriticalCSSService to use - Removed CriticalCSSCollector pattern (timing issue with WordPress) Flow: 1. wp_head (priority 1) → CriticalCSSService::render() 2. Service queries BD for components with visibility.is_critical=true 3. Generates CSS using Renderer->generateCSS() methods 4. Outputs: <style id="roi-critical-css">...</style> 5. When Renderers execute, they detect is_critical and omit CSS inline 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,6 @@ namespace ROITheme\Public\Hero\Infrastructure\Ui;
|
|||||||
|
|
||||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||||
use ROITheme\Shared\Domain\Contracts\CriticalCSSCollectorInterface;
|
|
||||||
use ROITheme\Shared\Domain\Entities\Component;
|
use ROITheme\Shared\Domain\Entities\Component;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,17 +24,20 @@ use ROITheme\Shared\Domain\Entities\Component;
|
|||||||
* - Persistir datos
|
* - Persistir datos
|
||||||
* - Lógica de negocio
|
* - 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
|
* @package ROITheme\Public\Hero\Infrastructure\Ui
|
||||||
*/
|
*/
|
||||||
final class HeroRenderer implements RendererInterface
|
final class HeroRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
||||||
* @param CriticalCSSCollectorInterface $criticalCollector Colector de CSS crítico
|
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private CSSGeneratorInterface $cssGenerator,
|
private CSSGeneratorInterface $cssGenerator
|
||||||
private CriticalCSSCollectorInterface $criticalCollector
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function render(Component $component): string
|
public function render(Component $component): string
|
||||||
@@ -50,11 +52,17 @@ final class HeroRenderer implements RendererInterface
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$css = $this->generateCSS($data);
|
|
||||||
$html = $this->buildHTML($data);
|
$html = $this->buildHTML($data);
|
||||||
|
|
||||||
// Siempre incluir CSS inline con el componente
|
// Si is_critical=true, CSS ya fue inyectado en <head> por CriticalCSSService
|
||||||
// Nota: is_critical se reserva para futura implementación con output buffering
|
$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("<style>%s</style>\n%s", $css, $html);
|
return sprintf("<style>%s</style>\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'] ?? [];
|
$colors = $data['colors'] ?? [];
|
||||||
$typography = $data['typography'] ?? [];
|
$typography = $data['typography'] ?? [];
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ namespace ROITheme\Public\Navbar\Infrastructure\Ui;
|
|||||||
use ROITheme\Shared\Domain\Entities\Component;
|
use ROITheme\Shared\Domain\Entities\Component;
|
||||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||||
use ROITheme\Shared\Domain\Contracts\CriticalCSSCollectorInterface;
|
|
||||||
use Walker_Nav_Menu;
|
use Walker_Nav_Menu;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,11 +30,9 @@ final class NavbarRenderer implements RendererInterface
|
|||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
||||||
* @param CriticalCSSCollectorInterface $criticalCollector Colector de CSS crítico
|
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private CSSGeneratorInterface $cssGenerator,
|
private CSSGeneratorInterface $cssGenerator
|
||||||
private CriticalCSSCollectorInterface $criticalCollector
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function render(Component $component): string
|
public function render(Component $component): string
|
||||||
@@ -46,16 +43,18 @@ final class NavbarRenderer implements RendererInterface
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$css = $this->generateCSS($data);
|
|
||||||
$html = $this->buildMenu($data);
|
$html = $this->buildMenu($data);
|
||||||
|
|
||||||
// Siempre incluir CSS inline con el componente
|
// Si is_critical=true, CSS ya fue inyectado en <head> por CriticalCSSService
|
||||||
// Nota: is_critical se reserva para futura implementación con output buffering
|
$isCritical = $data['visibility']['is_critical'] ?? false;
|
||||||
return sprintf(
|
|
||||||
"<style>%s</style>\n%s",
|
if ($isCritical) {
|
||||||
$css,
|
return $html; // Solo HTML, sin CSS inline
|
||||||
$html
|
}
|
||||||
);
|
|
||||||
|
// CSS inline para componentes no críticos
|
||||||
|
$css = $this->generateCSS($data);
|
||||||
|
return sprintf("<style>%s</style>\n%s", $css, $html);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isEnabled(array $data): bool
|
private function isEnabled(array $data): bool
|
||||||
@@ -73,10 +72,13 @@ final class NavbarRenderer implements RendererInterface
|
|||||||
/**
|
/**
|
||||||
* Generar CSS usando CSSGeneratorService
|
* 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
|
* @param array $data Datos del componente
|
||||||
* @return string CSS generado
|
* @return string CSS generado
|
||||||
*/
|
*/
|
||||||
private function generateCSS(array $data): string
|
public function generateCSS(array $data): string
|
||||||
{
|
{
|
||||||
$css = '';
|
$css = '';
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ namespace ROITheme\Public\TopNotificationBar\Infrastructure\Ui;
|
|||||||
|
|
||||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||||
use ROITheme\Shared\Domain\Contracts\CriticalCSSCollectorInterface;
|
|
||||||
use ROITheme\Shared\Domain\Entities\Component;
|
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 CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
||||||
* @param CriticalCSSCollectorInterface $criticalCollector Colector de CSS crítico
|
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private CSSGeneratorInterface $cssGenerator,
|
private CSSGeneratorInterface $cssGenerator
|
||||||
private CriticalCSSCollectorInterface $criticalCollector
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,19 +58,19 @@ final class TopNotificationBarRenderer implements RendererInterface
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generar CSS usando CSSGeneratorService
|
|
||||||
$css = $this->generateCSS($data);
|
|
||||||
|
|
||||||
// Generar HTML
|
// Generar HTML
|
||||||
$html = $this->buildHTML($data);
|
$html = $this->buildHTML($data);
|
||||||
|
|
||||||
// Siempre incluir CSS inline con el componente
|
// Si is_critical=true, CSS ya fue inyectado en <head> por CriticalCSSService
|
||||||
// Nota: is_critical se reserva para futura implementación con output buffering
|
$isCritical = $data['visibility']['is_critical'] ?? false;
|
||||||
return sprintf(
|
|
||||||
"<style>%s</style>\n%s",
|
if ($isCritical) {
|
||||||
$css,
|
return $html; // Solo HTML, sin CSS inline
|
||||||
$html
|
}
|
||||||
);
|
|
||||||
|
// CSS inline para componentes no críticos
|
||||||
|
$css = $this->generateCSS($data);
|
||||||
|
return sprintf("<style>%s</style>\n%s", $css, $html);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -165,10 +162,13 @@ final class TopNotificationBarRenderer implements RendererInterface
|
|||||||
/**
|
/**
|
||||||
* Generar CSS usando CSSGeneratorService
|
* 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
|
* @param array $data Datos del componente
|
||||||
* @return string CSS generado
|
* @return string CSS generado
|
||||||
*/
|
*/
|
||||||
private function generateCSS(array $data): string
|
public function generateCSS(array $data): string
|
||||||
{
|
{
|
||||||
$css = '';
|
$css = '';
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace ROITheme\Shared\Domain\Contracts;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface para recolectar CSS crítico de componentes above-the-fold
|
|
||||||
*
|
|
||||||
* RESPONSABILIDAD:
|
|
||||||
* - Definir contrato para recolección de CSS crítico
|
|
||||||
* - Permitir inyección en Renderers (DIP)
|
|
||||||
*
|
|
||||||
* UBICACIÓN: Domain (según 00.02 líneas 239-252)
|
|
||||||
*
|
|
||||||
* @package ROITheme\Shared\Domain\Contracts
|
|
||||||
*/
|
|
||||||
interface CriticalCSSCollectorInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Agregar CSS de un componente a la colección crítica
|
|
||||||
*
|
|
||||||
* @param string $componentName Identificador del componente (kebab-case)
|
|
||||||
* @param string $css CSS generado del componente
|
|
||||||
*/
|
|
||||||
public function add(string $componentName, string $css): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtener todo el CSS crítico recolectado
|
|
||||||
*
|
|
||||||
* @return array<string, string> [componentName => css]
|
|
||||||
*/
|
|
||||||
public function getAll(): array;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renderizar CSS crítico como tag <style>
|
|
||||||
*
|
|
||||||
* @return string HTML del tag <style> o string vacío si no hay CSS
|
|
||||||
*/
|
|
||||||
public function render(): string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Limpiar colección (útil para tests)
|
|
||||||
*/
|
|
||||||
public function clear(): void;
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace ROITheme\Shared\Infrastructure\Services;
|
|
||||||
|
|
||||||
use ROITheme\Shared\Domain\Contracts\CriticalCSSCollectorInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementación del colector de CSS crítico
|
|
||||||
*
|
|
||||||
* RESPONSABILIDAD:
|
|
||||||
* - Recolectar CSS de componentes above-the-fold
|
|
||||||
* - Renderizar como <style> inline en <head>
|
|
||||||
*
|
|
||||||
* PATRÓN:
|
|
||||||
* - Singleton via DIContainer (NO estático)
|
|
||||||
* - Cumple DIP: Renderers reciben interface, no clase concreta
|
|
||||||
*
|
|
||||||
* UBICACIÓN: Infrastructure/Services (según 00.02)
|
|
||||||
*
|
|
||||||
* @package ROITheme\Shared\Infrastructure\Services
|
|
||||||
*/
|
|
||||||
final class CriticalCSSCollector implements CriticalCSSCollectorInterface
|
|
||||||
{
|
|
||||||
/** @var array<string, string> */
|
|
||||||
private array $criticalStyles = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public function add(string $componentName, string $css): void
|
|
||||||
{
|
|
||||||
$this->criticalStyles[$componentName] = $css;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public function getAll(): array
|
|
||||||
{
|
|
||||||
return $this->criticalStyles;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public function render(): string
|
|
||||||
{
|
|
||||||
if (empty($this->criticalStyles)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$css = implode("\n", $this->criticalStyles);
|
|
||||||
return sprintf(
|
|
||||||
'<style id="roi-critical-css">%s</style>',
|
|
||||||
$css
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public function clear(): void
|
|
||||||
{
|
|
||||||
$this->criticalStyles = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
269
Shared/Infrastructure/Services/CriticalCSSService.php
Normal file
269
Shared/Infrastructure/Services/CriticalCSSService.php
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Infrastructure\Services;
|
||||||
|
|
||||||
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CriticalCSSService
|
||||||
|
*
|
||||||
|
* Genera CSS crítico para componentes above-the-fold (LCP).
|
||||||
|
*
|
||||||
|
* RESPONSABILIDAD:
|
||||||
|
* - Consultar BD para obtener componentes con is_critical=true
|
||||||
|
* - Generar CSS usando los Renderers públicos (sin duplicar lógica)
|
||||||
|
* - Inyectar CSS en <head> 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: <style id="roi-critical-css">...</style>
|
||||||
|
*
|
||||||
|
* IMPORTANTE:
|
||||||
|
* - Este servicio se ejecuta ANTES de que los componentes rendericen
|
||||||
|
* - Los Renderers detectan is_critical y omiten CSS inline (ya está en <head>)
|
||||||
|
*
|
||||||
|
* @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<string, object>
|
||||||
|
*/
|
||||||
|
private array $rendererInstances = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapa de componentes críticos y sus clases Renderer
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
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(
|
||||||
|
'<style id="roi-critical-css">%s</style>' . "\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<string> 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<string, array<string, mixed>> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,27 +3,32 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ROITheme\Shared\Infrastructure\Wordpress;
|
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
|
* Registra hook wp_head para inyectar CSS crítico
|
||||||
*
|
*
|
||||||
* RESPONSABILIDAD:
|
* RESPONSABILIDAD:
|
||||||
* - Registrar hook wp_head
|
* - Registrar hook wp_head (priority 1)
|
||||||
* - Delegar renderizado a CriticalCSSCollector
|
* - 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 <head>
|
||||||
|
* 4. Los Renderers detectan is_critical y omiten CSS inline
|
||||||
*
|
*
|
||||||
* PATRÓN:
|
* PATRÓN:
|
||||||
* - DIP: Recibe interface, no clase concreta
|
* - SRP: Solo registra hook, delega lógica a CriticalCSSService
|
||||||
* - SRP: Solo registra hook, no contiene lógica de CSS
|
|
||||||
*
|
*
|
||||||
* UBICACIÓN: Infrastructure/Wordpress (según 00.02 líneas 307-311)
|
* UBICACIÓN: Infrastructure/Wordpress
|
||||||
*
|
*
|
||||||
* @package ROITheme\Shared\Infrastructure\Wordpress
|
* @package ROITheme\Shared\Infrastructure\Wordpress
|
||||||
*/
|
*/
|
||||||
final class CriticalCSSHooksRegistrar
|
final class CriticalCSSHooksRegistrar
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly CriticalCSSCollectorInterface $collector
|
private readonly CriticalCSSService $criticalCSSService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,10 +42,14 @@ final class CriticalCSSHooksRegistrar
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback para wp_head
|
* Callback para wp_head
|
||||||
|
*
|
||||||
|
* Ejecuta CriticalCSSService que:
|
||||||
|
* - Consulta BD por componentes con is_critical=true
|
||||||
|
* - Genera CSS usando los Renderers
|
||||||
|
* - Output: <style id="roi-critical-css">...</style>
|
||||||
*/
|
*/
|
||||||
public function renderCriticalCSS(): void
|
public function renderCriticalCSS(): void
|
||||||
{
|
{
|
||||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
$this->criticalCSSService->render();
|
||||||
echo $this->collector->render();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
* Este servicio consulta la BD para componentes con is_critical=true
|
||||||
* que será compartida por todos los Renderers y el HooksRegistrar
|
* 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 {
|
function roi_get_critical_css_service(): \ROITheme\Shared\Infrastructure\Services\CriticalCSSService {
|
||||||
static $collector = null;
|
global $wpdb;
|
||||||
|
static $cssGenerator = null;
|
||||||
|
|
||||||
if ($collector === null) {
|
if ($cssGenerator === null) {
|
||||||
$collector = new \ROITheme\Shared\Infrastructure\Services\CriticalCSSCollector();
|
$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
|
// Obtener renderer específico para el componente
|
||||||
$renderer = null;
|
$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();
|
$cssGenerator = new \ROITheme\Shared\Infrastructure\Services\CSSGeneratorService();
|
||||||
|
|
||||||
// Obtener instancia singleton del CriticalCSSCollector
|
|
||||||
$criticalCollector = roi_get_critical_css_collector();
|
|
||||||
|
|
||||||
switch ($componentName) {
|
switch ($componentName) {
|
||||||
// Componentes con soporte de CSS Crítico (above-the-fold)
|
// Componentes con soporte de CSS Crítico (above-the-fold)
|
||||||
|
// Nota: Si is_critical=true, el CSS ya fue inyectado en <head> por CriticalCSSService
|
||||||
case 'top-notification-bar':
|
case 'top-notification-bar':
|
||||||
$renderer = new \ROITheme\Public\TopNotificationBar\Infrastructure\Ui\TopNotificationBarRenderer($cssGenerator, $criticalCollector);
|
$renderer = new \ROITheme\Public\TopNotificationBar\Infrastructure\Ui\TopNotificationBarRenderer($cssGenerator);
|
||||||
break;
|
break;
|
||||||
case 'navbar':
|
case 'navbar':
|
||||||
$renderer = new \ROITheme\Public\Navbar\Infrastructure\Ui\NavbarRenderer($cssGenerator, $criticalCollector);
|
$renderer = new \ROITheme\Public\Navbar\Infrastructure\Ui\NavbarRenderer($cssGenerator);
|
||||||
break;
|
break;
|
||||||
case 'hero':
|
case 'hero':
|
||||||
error_log("ROI Theme DEBUG: Creating HeroRenderer");
|
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");
|
error_log("ROI Theme DEBUG: HeroRenderer created successfully");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -283,13 +282,16 @@ function roi_render_component(string $componentName): string {
|
|||||||
/**
|
/**
|
||||||
* Registra el hook para inyectar CSS crítico en <head>
|
* Registra el hook para inyectar CSS crítico en <head>
|
||||||
*
|
*
|
||||||
* IMPORTANTE: El HooksRegistrar usa la misma instancia singleton del collector
|
* FLUJO:
|
||||||
* que usan los Renderers, garantizando que el CSS recolectado se inyecte
|
* 1. wp_head (priority 1) → CriticalCSSService::render()
|
||||||
* correctamente en wp_head con prioridad 1 (muy temprano).
|
* 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: <style id="roi-critical-css">...</style>
|
||||||
|
* 5. Cuando los Renderers ejecutan, detectan is_critical y omiten CSS inline
|
||||||
*/
|
*/
|
||||||
add_action('after_setup_theme', function() {
|
add_action('after_setup_theme', function() {
|
||||||
$criticalCollector = roi_get_critical_css_collector();
|
$criticalCSSService = roi_get_critical_css_service();
|
||||||
$hooksRegistrar = new \ROITheme\Shared\Infrastructure\Wordpress\CriticalCSSHooksRegistrar($criticalCollector);
|
$hooksRegistrar = new \ROITheme\Shared\Infrastructure\Wordpress\CriticalCSSHooksRegistrar($criticalCSSService);
|
||||||
$hooksRegistrar->register();
|
$hooksRegistrar->register();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user