Files
roi-theme/Shared/Infrastructure/Services/CriticalCSSService.php
FrankZamora c0172467b3 fix(cls): Server-side device visibility + aspect-ratio for featured-image
- functions-addon.php: Validacion centralizada con wp_is_mobile()
  Componentes con show_on_mobile=false NO se renderizan en mobile
  Previene CLS de elementos ocultos con CSS

- FeaturedImageRenderer: Agrega aspect-ratio 16/9 para reservar espacio
  Imagen usa object-fit:cover con position:absolute
  Metodo generateCSS() ahora publico para CriticalCSSService

- CriticalCSSService: Agrega featured-image a CRITICAL_RENDERERS
  CSS se inyecta en <head> antes de que cargue contenido

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 10:43:22 -06:00

271 lines
7.6 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,
'hero' => \ROITheme\Public\Hero\Infrastructure\Ui\HeroRenderer::class,
'featured-image' => \ROITheme\Public\FeaturedImage\Infrastructure\Ui\FeaturedImageRenderer::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;
}
}