Files
michaelschiemer/src/Framework/View/Processors/FrameworkComponentProcessor.php
Michael Schiemer 5050c7d73a docs: consolidate documentation into organized structure
- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
2025-10-05 11:05:04 +02:00

243 lines
7.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Template\Processing\DomProcessor;
use App\Framework\View\Attributes\ComponentName;
use App\Framework\View\DomComponentService;
use App\Framework\View\DomWrapper;
use App\Framework\View\RenderContext;
use App\Framework\View\ValueObjects\ComponentMetadata;
use App\Framework\View\ValueObjects\HtmlElement;
use Dom\HTMLElement as DomHTMLElement;
use ReflectionClass;
use ReflectionMethod;
use ReflectionNamedType;
final class FrameworkComponentProcessor implements DomProcessor
{
/** @var array<string, ComponentMetadata> */
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<string, string>
*/
private function getElementAttributes(DomHTMLElement $element): array
{
$attributes = [];
foreach ($element->attributes as $attr) {
$attributes[$attr->name] = $attr->value;
}
return $attributes;
}
/**
* @param array<string, string> $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<string, string> $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<string, string> $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<string, ReflectionMethod>
*/
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<string, ReflectionMethod>
*/
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));
}
}