- 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.
135 lines
4.8 KiB
PHP
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
|
|
);
|
|
}
|
|
}
|