- Agregar campo is_critical a schemas table-of-contents.json y cta-lets-talk.json - Cambiar generateCSS() de private a public en TableOfContentsRenderer y CtaLetsTalkRenderer - Registrar table-of-contents y cta-lets-talk en CRITICAL_RENDERERS - Ahora 6 componentes inyectan CSS crítico inline en <head> Componentes críticos: - top-notification-bar - navbar - cta-lets-talk (NUEVO) - hero - featured-image - table-of-contents (NUEVO) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
273 lines
7.8 KiB
PHP
273 lines
7.8 KiB
PHP
<?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,
|
|
'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(
|
|
'<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;
|
|
}
|
|
}
|