Files
michaelschiemer/src/Framework/View/LiveComponentRenderer.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

135 lines
4.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\View;
use App\Framework\Http\Session\SessionInterface;
use App\Framework\Meta\MetaData;
/**
* Renders LiveComponent templates
*
* Separates rendering concerns from LiveComponent business logic.
* LiveComponents should only handle state and actions, not template rendering.
*/
final readonly class LiveComponentRenderer
{
public function __construct(
private TemplateRenderer $templateRenderer,
private SessionInterface $session
) {
}
/**
* Render a LiveComponent template with data
*
* @param string $templatePath Template path (without extension)
* @param array $data Template data
* @param string $componentId Full component ID (e.g., "counter:demo")
* @return string Rendered HTML
*/
public function render(string $templatePath, array $data, string $componentId): string
{
// Merge component ID into data for templates
$templateData = array_merge($data, [
'componentId' => $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(
'<div %s>%s</div>',
$attributeString,
$componentHtml
);
}
}