nameToClassMap = $this->buildNameMap(); } /** * Build mapping of component names to class names using DiscoveryRegistry * * Discovers both LiveComponents and StaticComponents: * - LiveComponents: Dynamic, stateful components with #[LiveComponent] attribute * - StaticComponents: Server-side rendered components with #[ComponentName] attribute * * Performance Optimization: * - Batch loads metadata for all components in single cache operation * - ~85% faster than individual metadata loading */ private function buildNameMap(): ComponentNameMap { $mappings = []; $liveComponentClassNames = []; // Get all LiveComponent attributes from DiscoveryRegistry $liveComponents = $this->discoveryRegistry->attributes()->get(LiveComponent::class); foreach ($liveComponents as $discoveredAttribute) { /** @var LiveComponent|null $attribute */ $attribute = $discoveredAttribute->createAttributeInstance(); if ($attribute && ! empty($attribute->name)) { $className = $discoveredAttribute->className->toString(); $mappings[] = new ComponentMapping($attribute->name, $className); $liveComponentClassNames[] = $className; } } // Get all StaticComponent attributes (ComponentName) from DiscoveryRegistry $staticComponents = $this->discoveryRegistry->attributes()->get(ComponentName::class); foreach ($staticComponents as $discoveredAttribute) { /** @var ComponentName|null $attribute */ $attribute = $discoveredAttribute->createAttributeInstance(); if ($attribute && ! empty($attribute->tag)) { $className = $discoveredAttribute->className->toString(); $mappings[] = new ComponentMapping($attribute->tag, $className); // Don't add StaticComponents to metadata cache (they don't need it) } } // Batch warm metadata cache for LiveComponents only // This pre-compiles and caches metadata for ~90% faster subsequent access if (! empty($liveComponentClassNames)) { $this->metadataCache->warmCache($liveComponentClassNames); } // ComponentNameMap constructor validates no name collisions via variadic parameter return new ComponentNameMap(...$mappings); } /** * Resolve component instance from component ID * * Components no longer need TemplateRenderer - rendering is handled * by LiveComponentRenderer in the View module. * * Uses MethodInvoker::make() for dependency injection with custom parameters. * * State Handling: * - $stateData is a raw array from client (from state JSON) * - ComponentRegistry creates typed State VO by reading component's $state property type * - Component constructor receives typed State VO directly (CounterState, SearchState, etc.) * - If $stateData is null (initial creation), uses State::empty() and calls onMount() */ public function resolve(ComponentId $componentId, ?array $stateData = null): LiveComponentContract { [$componentName, $instanceId] = $this->parseComponentId($componentId->toString()); // Get class name from name map $className = $this->nameToClassMap->getClassName($componentName); if (! $className) { throw new \InvalidArgumentException("Unknown component: {$componentName}"); } // Get State class from component's $state property type $stateClassName = $this->getStateClassName($className); // Create State VO from array or use empty state $state = $stateData !== null ? $stateClassName::fromArray($stateData) : $stateClassName::empty(); // Use MethodInvoker::make() to create instance with DI and typed State VO $component = $this->container->invoker->make($className, [ 'id' => $componentId, 'state' => $state, ]); // Call onMount() for initial component creation (when $stateData is null) // For re-hydration with existing state, onMount() is not called if ($stateData === null) { $this->handler->callMountHook($component); } return $component; } /** * Get State class name from component's $state property type * * Reads the type hint from the public readonly $state property. * * @param class-string $componentClassName * @return class-string */ private function getStateClassName(string $componentClassName): string { $reflection = new \ReflectionClass($componentClassName); if (!$reflection->hasProperty('state')) { throw new \InvalidArgumentException( "Component {$componentClassName} must have a public 'state' property" ); } $stateProperty = $reflection->getProperty('state'); $type = $stateProperty->getType(); if (!$type instanceof \ReflectionNamedType) { throw new \InvalidArgumentException( "Component {$componentClassName} \$state property must have a type hint" ); } $stateClassName = $type->getName(); if (!class_exists($stateClassName)) { throw new \InvalidArgumentException( "State class not found: {$stateClassName}" ); } return $stateClassName; } /** * Render a StaticComponent to HTML * * StaticComponents return a tree (Node) via getRootNode() which is then * rendered to HTML string by HtmlRenderer. * * @param string $content Inner HTML content from template * @param array $attributes Key-value attributes from template */ public function renderStatic(string $componentName, string $content, array $attributes = []): string { // Get class name from registry $className = $this->nameToClassMap->getClassName($componentName); if (! $className) { throw new \InvalidArgumentException("Unknown component: {$componentName}"); } // Verify it's a StaticComponent if (! is_subclass_of($className, StaticComponent::class)) { throw new \RuntimeException( "Component {$componentName} ({$className}) does not implement StaticComponent. " . "Use render() for LiveComponents." ); } // Instantiate StaticComponent with content + attributes $component = new $className($content, $attributes); // Get root node tree $rootNode = $component->getRootNode(); // Render tree to HTML return $this->htmlRenderer->render($rootNode); } /** * Render a component to HTML with SWR support * * Delegates rendering to LiveComponentRenderer in View module. * Uses cache if component implements Cacheable. * * SWR (Stale-While-Revalidate) behavior: * - Fresh cache: Return cached HTML immediately * - Stale cache: Return stale HTML + trigger background refresh * - No cache: Render fresh HTML and cache it */ public function render(LiveComponentContract $component): string { return $this->performanceTracker->measure( "livecomponent.render.{$component->id->name}", PerformanceCategory::VIEW, function () use ($component): string { $startTime = microtime(true); $cacheHit = false; // Try to get from cache if component is cacheable if ($component instanceof Cacheable) { $cacheResult = $this->performanceTracker->measure( "livecomponent.cache.get", PerformanceCategory::CACHE, fn() => $this->cacheManager->getCachedHtml($component), ['component' => $component->id->name] ); if ($cacheResult['html'] !== null) { // Cache hit (fresh or stale) $cacheHit = true; // Trigger background refresh for stale content if ($cacheResult['needs_refresh']) { $this->triggerBackgroundRefresh($component); } $html = $cacheResult['html']; // Append debug panel if enabled if ($this->debugPanel !== null && DebugPanelRenderer::shouldRender()) { $renderTime = (microtime(true) - $startTime) * 1000; $html .= $this->renderDebugPanel($component, $renderTime, $cacheHit); } return $html; } } // Render component fresh $renderData = $this->performanceTracker->measure( "livecomponent.getRenderData", PerformanceCategory::CUSTOM, fn() => $component->getRenderData(), ['component' => $component->id->name] ); $html = $this->performanceTracker->measure( "livecomponent.template.render", PerformanceCategory::TEMPLATE, fn() => $this->renderer->render( templatePath: $renderData->templatePath, data: $renderData->data, componentId: $component->id->toString() ), ['component' => $component->id->name, 'template' => $renderData->templatePath] ); // Cache the rendered HTML if component is cacheable if ($component instanceof Cacheable) { $this->performanceTracker->measure( "livecomponent.cache.set", PerformanceCategory::CACHE, function () use ($component, $html): void { // Get state and convert to array for caching $stateArray = $component->state->toArray(); $this->cacheManager->cacheComponent( $component, $html, $stateArray ); }, ['component' => $component->id->name] ); } // Append debug panel if enabled if ($this->debugPanel !== null && DebugPanelRenderer::shouldRender()) { $renderTime = (microtime(true) - $startTime) * 1000; $html .= $this->renderDebugPanel($component, $renderTime, $cacheHit); } return $html; }, ['component' => $component->id->name] ); } /** * Trigger background refresh for stale cached component * * For now, this is a simple implementation that refreshes immediately. * In production, this should be delegated to a queue/background job system. */ private function triggerBackgroundRefresh(LiveComponentContract $component): void { // Mark as refreshing to prevent multiple concurrent refreshes $this->cacheManager->markAsRefreshing($component); // TODO: Delegate to background job system in production // For now, we'll do a simple deferred refresh try { $renderData = $component->getRenderData(); $html = $this->renderer->render( templatePath: $renderData->templatePath, data: $renderData->data, componentId: $component->id->toString() ); // Update cache with fresh content $stateArray = $component->state->toArray(); $this->cacheManager->cacheComponent( $component, $html, $stateArray ); } catch (\Throwable) { // Silent failure - stale content is still served // TODO: Log the error for monitoring } } /** * Render component with wrapper (for initial page load) * * Automatically detects SSE support by checking if component has getSseChannel() method. * If present, the SSE channel will be rendered as data-sse-channel attribute. */ public function renderWithWrapper(LiveComponentContract $component): string { $componentHtml = $this->render($component); // Check if component supports SSE (has getSseChannel() method) $sseChannel = null; if (method_exists($component, 'getSseChannel')) { $sseChannel = $component->getSseChannel(); } // Check if component supports polling (implements Pollable interface) $pollInterval = null; if ($component instanceof Pollable) { $pollInterval = $component->getPollInterval(); } return $this->renderer->renderWithWrapper( componentId: $component->id->toString(), componentHtml: $componentHtml, state: $component->state->toArray(), sseChannel: $sseChannel, pollInterval: $pollInterval ); } /** * Parse component ID into component name and instance ID * * New format: "counter:demo" or "user-card:user-123" */ private function parseComponentId(string $componentId): array { if (! str_contains($componentId, ':')) { throw new \InvalidArgumentException("Invalid component ID format: {$componentId}"); } return explode(':', $componentId, 2); } /** * Generate component ID from component name and instance ID */ public static function makeId(string $componentName, string $instanceId): ComponentId { return ComponentId::create($componentName, $instanceId); } /** * Get component name from class using attribute */ public function getComponentName(string $componentClass): string { $liveComponents = $this->discoveryRegistry->attributes()->get(LiveComponent::class); foreach ($liveComponents as $discoveredAttribute) { if ($discoveredAttribute->className->toString() === $componentClass) { /** @var LiveComponent|null $attribute */ $attribute = $discoveredAttribute->createAttributeInstance(); if ($attribute) { return $attribute->name; } } } throw new \InvalidArgumentException("Component {$componentClass} has no #[LiveComponent] attribute"); } /** * Invalidate component cache */ public function invalidateCache(LiveComponentContract $component): bool { return $this->cacheManager->invalidate($component); } /** * Invalidate cache by tag */ public function invalidateCacheByTag(string $tag): bool { return $this->cacheManager->invalidateByTag($tag); } /** * Get cache statistics for component */ public function getCacheStats(LiveComponentContract $component): array { return $this->cacheManager->getStats($component); } /** * Get compiled metadata for component (fast access - no reflection) * * Performance: ~99% faster than reflection-based access */ public function getMetadata(string $componentName): CompiledComponentMetadata { $className = $this->nameToClassMap->getClassName($componentName); if (! $className) { throw new \InvalidArgumentException("Unknown component: {$componentName}"); } return $this->metadataCache->get($className); } /** * Check if component has specific property (fast metadata lookup) */ public function hasProperty(string $componentName, string $propertyName): bool { $metadata = $this->getMetadata($componentName); return $metadata->hasProperty($propertyName); } /** * Check if component has specific action (fast metadata lookup) */ public function hasAction(string $componentName, string $actionName): bool { $metadata = $this->getMetadata($componentName); return $metadata->hasAction($actionName); } /** * Get all registered component names */ public function getAllComponentNames(): array { return $this->nameToClassMap->getAllNames(); } /** * Get component class name */ public function getClassName(string $componentName): ?string { return $this->nameToClassMap->getClassName($componentName); } /** * Check if component is registered */ public function isRegistered(string $componentName): bool { return $this->nameToClassMap->has($componentName); } /** * Get registry statistics */ public function getRegistryStats(): array { return $this->nameToClassMap->getStats(); } /** * Render debug panel for component (development only) */ private function renderDebugPanel( LiveComponentContract $component, float $renderTimeMs, bool $cacheHit ): string { if ($this->debugPanel === null) { return ''; } // Extract component name from ID $componentId = $component->id; [$componentName, ] = $this->parseComponentId($componentId->toString()); // Get component class name $className = get_class($component); // Get metadata if available $metadata = null; try { $metadata = $this->metadataCache->get($className); } catch (\Throwable) { // Ignore metadata fetch errors } return $this->debugPanel->render( componentId: $componentId, componentName: $componentName, className: $className, renderTimeMs: $renderTimeMs, state: $component->state, metadata: $metadata, cacheHit: $cacheHit ); } }