*/ private array $registry = []; public function __construct( private readonly DiscoveryRegistry $discoveryRegistry, private readonly DomComponentService $componentService ) { $this->discoverComponents(); } public function process(DomWrapper $dom, RenderContext $context): DomWrapper { // Process each registered component type foreach ($this->registry as $tagName => $metadata) { $elements = $dom->getElementsByTagName("x-{$tagName}"); $elements->forEach(function ($element) use ($dom, $tagName, $metadata) { $this->processElement($dom, $element, $tagName, $metadata); }); } return $dom; } private function processElement( DomWrapper $dom, DomHTMLElement $element, string $tagName, ComponentMetadata $metadata ): void { // Get content and attributes $content = $element->textContent; $attributes = $this->getElementAttributes($element); // Render component $rendered = $this->renderComponent($metadata, $content, $attributes); // Replace element with rendered HTML using DomComponentService $this->componentService->replaceComponent($dom, $element, $rendered); } /** * @return array */ private function getElementAttributes(DomHTMLElement $element): array { $attributes = []; foreach ($element->attributes as $attr) { $attributes[$attr->name] = $attr->value; } return $attributes; } /** * @param array $attributes */ private function renderComponent( ComponentMetadata $metadata, string $content, array $attributes ): string { $variant = $attributes['variant'] ?? null; // Create component instance $component = $this->instantiateComponent($metadata, $variant, $content, $attributes); // Apply modifiers $component = $this->applyModifiers($component, $attributes, $metadata); return (string) $component; } /** * @param array $attributes */ private function instantiateComponent( ComponentMetadata $metadata, ?string $variant, string $content, array $attributes ): HtmlElement { // Try factory method first if ($variant !== null && $metadata->hasFactory($variant)) { $factory = $metadata->getFactory($variant); return $factory->invoke(null, $content); } // Try default 'create' factory if ($metadata->hasFactory('create')) { $factory = $metadata->getFactory('create'); return $factory->invoke(null, $content); } // Fall back to constructor $constructor = $metadata->reflection->getConstructor(); if ($constructor === null) { throw new \RuntimeException("Component {$metadata->class} has no constructor or factory methods"); } // Simple approach: pass content as first parameter return $metadata->reflection->newInstance($content); } /** * @param array $attributes */ private function applyModifiers( HtmlElement $component, array $attributes, ComponentMetadata $metadata ): HtmlElement { foreach ($attributes as $name => $value) { if ($name === 'variant') { continue; // Already handled in instantiation } if ($metadata->hasModifier($name)) { $modifier = $metadata->getModifier($name); $params = $modifier->getParameters(); // If modifier has no parameters, call without arguments if (empty($params)) { $component = $modifier->invoke($component); } else { // Pass the attribute value $component = $modifier->invoke($component, $value); } } } return $component; } private function discoverComponents(): void { $componentAttributes = $this->discoveryRegistry->attributes->get(ComponentName::class); foreach ($componentAttributes as $attribute) { $className = $attribute->className->getFullyQualified(); $reflection = new ReflectionClass($className); /** @var ComponentName|null $componentName */ $componentName = $attribute->createAttributeInstance(); if ($componentName === null) { continue; } $tagName = $componentName->tag; $this->registry[$tagName] = new ComponentMetadata( class: $className, factories: $this->findFactoryMethods($reflection), modifiers: $this->findModifierMethods($reflection), reflection: $reflection ); } } /** * @return array */ private function findFactoryMethods(ReflectionClass $reflection): array { $factories = []; foreach ($reflection->getMethods(ReflectionMethod::IS_STATIC | ReflectionMethod::IS_PUBLIC) as $method) { $returnType = $method->getReturnType(); if (! $returnType instanceof ReflectionNamedType) { continue; } if ($returnType->getName() === 'self' || $returnType->getName() === $reflection->getName()) { $factories[$method->getName()] = $method; } } return $factories; } /** * @return array */ private function findModifierMethods(ReflectionClass $reflection): array { $modifiers = []; foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { if ($method->isStatic() || $method->isConstructor()) { continue; } $returnType = $method->getReturnType(); if (! $returnType instanceof ReflectionNamedType) { continue; } if ($returnType->getName() === 'self' || $returnType->getName() === $reflection->getName()) { // Convert method name to kebab-case for HTML attributes $attributeName = $this->methodNameToAttribute($method->getName()); $modifiers[$attributeName] = $method; } } return $modifiers; } private function methodNameToAttribute(string $methodName): string { // Remove 'with' prefix if present $name = preg_replace('/^with/', '', $methodName); // Convert to kebab-case return strtolower(preg_replace('/([a-z])([A-Z])/', '$1-$2', $name)); } }