- 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
243 lines
7.4 KiB
PHP
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));
|
|
}
|
|
}
|