$componentId, ]); // Create component render context // Use COMPONENT mode to enable template logic (ForProcessor, IfProcessor, etc.) // but skip layout/meta/asset processors $context = new RenderContext( template: $templatePath, metaData: new MetaData(''), data: $templateData, processingMode: ProcessingMode::COMPONENT ); return $this->templateRenderer->renderPartial($context); } /** * Render component wrapper HTML with state, CSRF protection, SSE support, and polling * * Generates a component-specific CSRF token for secure action execution. * Each component instance gets its own token for isolation. * * If the component supports SSE (has getSseChannel() method), the SSE channel * will be rendered as data-sse-channel attribute for automatic real-time updates. * * If the component implements Pollable interface, the poll interval (in milliseconds) * will be rendered as data-poll-interval attribute for automatic polling. * * @param string $componentId Full component ID (e.g., "counter:demo") * @param string $componentHtml Rendered component HTML * @param array $state Component state data * @param string|null $sseChannel Optional SSE channel for real-time updates * @param int|null $pollInterval Optional poll interval in milliseconds * @return string Complete component HTML with wrapper */ public function renderWithWrapper( string $componentId, string $componentHtml, array $state, ?string $sseChannel = null, ?int $pollInterval = null ): string { // Extract component name from ID (format: "counter:demo") $componentName = explode(':', $componentId)[0] ?? 'unknown'; // Generate component-specific CSRF token // Use component ID as form ID for per-component isolation $formId = 'livecomponent:' . $componentId; $csrfToken = $this->session->csrf->generateToken($formId); $stateJson = json_encode([ 'id' => $componentId, 'component' => $componentName, 'data' => $state, 'version' => 1, ]); // Build attributes $attributes = [ 'data-live-component' => $componentId, 'data-state' => $stateJson, 'data-csrf-token' => $csrfToken->toString(), ]; // Add SSE channel if provided if ($sseChannel !== null) { $attributes['data-sse-channel'] = $sseChannel; } // Add poll interval if provided if ($pollInterval !== null) { $attributes['data-poll-interval'] = (string) $pollInterval; } // Build attribute string // IMPORTANT: data-state is already JSON and will be parsed by HTMLDocument // which handles escaping correctly. We DON'T htmlspecialchars the JSON // because it gets parsed and re-serialized by DOM, which handles escaping. $attributeString = implode(' ', array_map( function ($key, $value) { // For data-state, we use the JSON directly without additional escaping // The DOM parser will handle proper escaping when the HTML is parsed if ($key === 'data-state') { return sprintf('%s=\'%s\'', $key, $value); } // For other attributes, use standard escaping return sprintf('%s="%s"', $key, htmlspecialchars($value, ENT_QUOTES, 'UTF-8')); }, array_keys($attributes), $attributes )); return sprintf( '
%s
', $attributeString, $componentHtml ); } }