From a93a086ee4c24c44b31eafc674027a65ddcb27ac Mon Sep 17 00:00:00 2001 From: Michael Schiemer Date: Mon, 3 Nov 2025 22:38:06 +0100 Subject: [PATCH] refactor(di): add analysis components for dependency parsing and resolution - Introduce `CodeParser` to extract dependencies from `container->get()` calls and `return new` statements. - Add `DependencyPathAnalyzer` for recursive analysis of dependency paths with cycle detection. - Implement `InitializerFinder` to locate initializers based on naming conventions. - Include `InterfaceResolver` to determine interface implementations using introspection and initializers. - Add `NamespaceResolver` for resolving class names from use statements and namespaces. - Introduce `ReturnTypeAnalyzer` for method and closure return type analysis. --- src/Framework/DI/Analysis/CodeParser.php | 254 ++++++ .../DI/Analysis/DependencyPathAnalyzer.php | 202 +++++ .../DI/Analysis/InitializerFinder.php | 125 +++ .../DI/Analysis/InterfaceResolver.php | 122 +++ .../DI/Analysis/NamespaceResolver.php | 96 +++ .../DI/Analysis/ReturnTypeAnalyzer.php | 110 +++ .../DI/InitializerDependencyAnalyzer.php | 756 ++---------------- src/Framework/View/LiveComponentRenderer.php | 13 +- .../View/TemplateLoaderInitializer.php | 39 + .../View/TemplateProcessorInitializer.php | 72 ++ .../View/TemplateRendererInitializer.php | 82 +- .../LiveComponents/CsrfIntegrationTest.php | 87 +- 12 files changed, 1120 insertions(+), 838 deletions(-) create mode 100644 src/Framework/DI/Analysis/CodeParser.php create mode 100644 src/Framework/DI/Analysis/DependencyPathAnalyzer.php create mode 100644 src/Framework/DI/Analysis/InitializerFinder.php create mode 100644 src/Framework/DI/Analysis/InterfaceResolver.php create mode 100644 src/Framework/DI/Analysis/NamespaceResolver.php create mode 100644 src/Framework/DI/Analysis/ReturnTypeAnalyzer.php create mode 100644 src/Framework/View/TemplateLoaderInitializer.php create mode 100644 src/Framework/View/TemplateProcessorInitializer.php diff --git a/src/Framework/DI/Analysis/CodeParser.php b/src/Framework/DI/Analysis/CodeParser.php new file mode 100644 index 00000000..e4c18cf7 --- /dev/null +++ b/src/Framework/DI/Analysis/CodeParser.php @@ -0,0 +1,254 @@ +get() Aufrufe und return new Statements + */ +final readonly class CodeParser +{ + /** + * Parse container->get() Aufrufe aus einer Method + * + * @return array{dependencies: array, hasCalls: bool} + */ + public function parseContainerGetCalls(\ReflectionMethod $method): array + { + try { + $fileName = $method->getFileName(); + if ($fileName === false || !file_exists($fileName)) { + return ['dependencies' => [], 'hasCalls' => false]; + } + + $fileContent = file_get_contents($fileName); + if ($fileContent === false) { + return ['dependencies' => [], 'hasCalls' => false]; + } + + $startLine = $method->getStartLine(); + $endLine = $method->getEndLine(); + + if ($startLine === false || $endLine === false) { + return ['dependencies' => [], 'hasCalls' => false]; + } + + // Extrahiere nur die Method + $lines = explode("\n", $fileContent); + $methodLines = array_slice($lines, $startLine - 1, $endLine - $startLine + 1); + $methodCode = implode("\n", $methodLines); + + // Finde container->get(...) Aufrufe + // Pattern: $container->get(ClassName::class) oder $this->container->get(ClassName::class) + // Unterstützt auch ::class Notation + $pattern = '/(?:\$container|\$this->container)->get\(([^,\)]+::class|[^,\)]+)\)/'; + preg_match_all($pattern, $methodCode, $matches); + + $dependencies = []; + if (!empty($matches[1])) { + foreach ($matches[1] as $match) { + $match = trim($match); + + // Entferne ::class falls vorhanden + $className = str_replace('::class', '', $match); + $className = trim($className, '\'"'); + + // Prüfe ob es ein gültiger Klassenname ist (beginnt mit \ oder Namespace) + if (preg_match('/^\\\\?[A-Z][A-Za-z0-9\\\\]*$/', $className)) { + try { + // Normalisiere (füge \ am Anfang hinzu wenn nicht vorhanden) + if (!str_starts_with($className, '\\')) { + // Versuche vollständigen Namespace zu finden (z.B. über use statements) + // Note: NamespaceResolver wird später injiziert + $fullClassName = $this->resolveClassNameFromMethod($className, $fileContent, $method); + if ($fullClassName !== null) { + $dependencies[] = ClassName::create($fullClassName); + } else { + // Fallback: Verwende wie angegeben + $dependencies[] = ClassName::create($className); + } + } else { + $dependencies[] = ClassName::create($className); + } + } catch (\InvalidArgumentException) { + // Ungültiger Klassenname - überspringe + continue; + } + } + } + + // Entferne Duplikate + $dependencies = $this->uniqueClassNames($dependencies); + } + + return [ + 'dependencies' => $dependencies, + 'hasCalls' => !empty($matches[0]), + ]; + } catch (\Throwable) { + return ['dependencies' => [], 'hasCalls' => false]; + } + } + + /** + * Extrahiere die tatsächlich zurückgegebene Klasse aus dem Code + * (z.B. "return new Engine(...)" → ClassName) + */ + public function extractReturnClass(\ReflectionMethod $method): ?ClassName + { + try { + $fileName = $method->getFileName(); + if ($fileName === false || !file_exists($fileName)) { + return null; + } + + $fileContent = file_get_contents($fileName); + if ($fileContent === false) { + return null; + } + + $startLine = $method->getStartLine(); + $endLine = $method->getEndLine(); + + if ($startLine === false || $endLine === false) { + return null; + } + + // Extrahiere nur den Method-Code + $lines = explode("\n", $fileContent); + $methodLines = array_slice($lines, $startLine - 1, $endLine - $startLine + 1); + $methodCode = implode("\n", $methodLines); + + // Suche nach "return new ClassName(" oder "return new ClassName;" + // Pattern muss auch Named Parameters unterstützen: "return new Engine(...)" + $pattern = '/return\s+new\s+([A-Z][A-Za-z0-9\\\\]+)\s*[\(;]/'; + if (preg_match($pattern, $methodCode, $matches)) { + $className = $matches[1]; + + // Resolve vollständigen Namespace + $fullClassName = $this->resolveClassNameFromMethod($className, $fileContent, $method); + if ($fullClassName !== null) { + try { + return ClassName::create($fullClassName); + } catch (\InvalidArgumentException) { + return null; + } + } + + // Fallback: Versuche direkt + try { + return ClassName::create($className); + } catch (\InvalidArgumentException) { + return null; + } + } + + // Fallback: Suche auch nach Named Parameters Syntax: "return new Engine(...)" + // mit Named Parameters: loader: ..., processor: ... + $pattern2 = '/return\s+new\s+([A-Z][A-Za-z0-9\\\\]+)\s*\(/'; + if (preg_match($pattern2, $methodCode, $matches)) { + $className = $matches[1]; + + // Resolve vollständigen Namespace + $fullClassName = $this->resolveClassNameFromMethod($className, $fileContent, $method); + if ($fullClassName !== null) { + try { + return ClassName::create($fullClassName); + } catch (\InvalidArgumentException) { + return null; + } + } + + // Fallback: Versuche direkt + try { + return ClassName::create($className); + } catch (\InvalidArgumentException) { + return null; + } + } + + return null; + } catch (\Throwable) { + return null; + } + } + + /** + * Versuche vollständigen Klassenname aus use statements zu resolven + * + * @param string $shortName Kurzer Klassenname (z.B. "Engine") + * @param string $fileContent Vollständiger Dateiinhalt + * @param \ReflectionMethod $method Method-Kontext + * @return string|null Vollständiger Klassenname oder null + */ + private function resolveClassNameFromMethod(string $shortName, string $fileContent, \ReflectionMethod $method): ?string + { + try { + // Finde use statements im File + preg_match_all('/^use\s+([^;]+);/m', $fileContent, $useMatches); + + // Suche nach exakter Übereinstimmung oder Alias + foreach ($useMatches[1] as $useStatement) { + $useStatement = trim($useStatement); + + // Prüfe auf Alias (use Full\Class\Name as Alias) + if (preg_match('/^(.+?)\s+as\s+(\w+)$/', $useStatement, $aliasMatch)) { + $fullClassName = $aliasMatch[1]; + $alias = $aliasMatch[2]; + if ($alias === $shortName) { + return $fullClassName; + } + } else { + // Prüfe ob letzter Teil übereinstimmt + $parts = explode('\\', $useStatement); + $lastPart = end($parts); + if ($lastPart === $shortName) { + return $useStatement; + } + } + } + + // Fallback: Prüfe ob Klasse im gleichen Namespace ist + // Extrahiere Namespace aus der Datei + if (preg_match('/^namespace\s+([^;]+);/m', $fileContent, $namespaceMatch)) { + $namespace = trim($namespaceMatch[1]); + $fullClassName = $namespace . '\\' . $shortName; + if (class_exists($fullClassName)) { + return $fullClassName; + } + } + + return null; + } catch (\Throwable) { + return null; + } + } + + /** + * Entferne Duplikate aus ClassName Array + * + * @param array $classNames + * @return array + */ + private function uniqueClassNames(array $classNames): array + { + $seen = []; + $unique = []; + + foreach ($classNames as $className) { + $key = $className->toString(); + if (!isset($seen[$key])) { + $seen[$key] = true; + $unique[] = $className; + } + } + + return array_values($unique); + } +} + diff --git a/src/Framework/DI/Analysis/DependencyPathAnalyzer.php b/src/Framework/DI/Analysis/DependencyPathAnalyzer.php new file mode 100644 index 00000000..bf2a34e7 --- /dev/null +++ b/src/Framework/DI/Analysis/DependencyPathAnalyzer.php @@ -0,0 +1,202 @@ + $visited Bereits besuchte Klassen (für Cycle-Detection) + * @param array $currentPath Aktueller Pfad + * @param int $depth Aktuelle Rekursionstiefe + * @return array|null Vollständiger Pfad [Dep1, Dep2, ..., Interface] oder null + */ + public function findPathToInterface( + ClassName $dependencyClass, + ClassName $targetInterface, + array $visited = [], + array $currentPath = [], + int $depth = 0 + ): ?array { + // Max. Rekursionstiefe erreicht + if ($depth >= self::MAX_RECURSION_DEPTH) { + return null; + } + + // Cycle-Detection: Vermeide Endlosschleifen + foreach ($visited as $visitedClass) { + if ($visitedClass->equals($dependencyClass)) { + return null; + } + } + + // Prüfe ob diese Klasse direkt das Interface benötigt + if ($this->dependencyNeedsInterface($dependencyClass, $targetInterface)) { + return array_merge($currentPath, [$dependencyClass, $targetInterface]); + } + + // Rekursiv: Analysiere Dependencies dieser Klasse + $dependencies = $this->getClassDependencies($dependencyClass); + + if (empty($dependencies)) { + return null; + } + + $newVisited = array_merge($visited, [$dependencyClass]); + $newPath = array_merge($currentPath, [$dependencyClass]); + + foreach ($dependencies as $subDependency) { + // Überspringe Container selbst (würde alle Dependencies auflisten) + if ($subDependency->toString() === Container::class || $subDependency->toString() === 'App\Framework\DI\Container') { + continue; + } + + $path = $this->findPathToInterface( + $subDependency, + $targetInterface, + $newVisited, + $newPath, + $depth + 1 + ); + + if ($path !== null) { + return $path; + } + } + + return null; + } + + /** + * Hole Dependencies einer Klasse (Constructor-Parameter) + * + * @return array + */ + public function getClassDependencies(ClassName $className): array + { + try { + if (!$className->exists()) { + return []; + } + + $reflection = new \ReflectionClass($className->toString()); + + // Wenn es ein Interface ist, versuche die Implementierung zu finden + if ($reflection->isInterface()) { + $implClass = $this->interfaceResolver->findImplementation($className); + if ($implClass !== null && $implClass->exists()) { + $reflection = new \ReflectionClass($implClass->toString()); + } else { + // Kann keine Dependencies für Interfaces ohne Implementierung finden + return []; + } + } + + $constructor = $reflection->getConstructor(); + + if ($constructor === null) { + return []; + } + + $dependencies = []; + foreach ($constructor->getParameters() as $parameter) { + $type = $parameter->getType(); + + if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) { + try { + $depClassName = ClassName::create($type->getName()); + $dependencies[] = $depClassName; + } catch (\InvalidArgumentException) { + // Ungültiger Klassenname - überspringe + continue; + } + } + } + + return $dependencies; + } catch (\Throwable) { + return []; + } + } + + /** + * Prüfe ob eine Klasse ein Interface benötigt (über Reflection) + */ + private function dependencyNeedsInterface(ClassName $dependencyClass, ClassName $interface): bool + { + try { + if (!$dependencyClass->exists()) { + return false; + } + + $reflection = new \ReflectionClass($dependencyClass->toString()); + $constructor = $reflection->getConstructor(); + + if ($constructor === null) { + return false; + } + + // Prüfe alle Constructor-Parameter + foreach ($constructor->getParameters() as $parameter) { + $type = $parameter->getType(); + + if ($type === null) { + continue; + } + + // Direkter NamedType + if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) { + if ($type->getName() === $interface->toString()) { + return true; + } + } + + // Union Types (PHP 8.0+) + if ($type instanceof \ReflectionUnionType) { + foreach ($type->getTypes() as $subType) { + if ($subType instanceof \ReflectionNamedType && !$subType->isBuiltin()) { + if ($subType->getName() === $interface->toString()) { + return true; + } + } + } + } + + // Intersection Types (PHP 8.1+) + if ($type instanceof \ReflectionIntersectionType) { + foreach ($type->getTypes() as $subType) { + if ($subType instanceof \ReflectionNamedType && !$subType->isBuiltin()) { + if ($subType->getName() === $interface->toString()) { + return true; + } + } + } + } + } + + return false; + } catch (\Throwable) { + return false; + } + } +} + diff --git a/src/Framework/DI/Analysis/InitializerFinder.php b/src/Framework/DI/Analysis/InitializerFinder.php new file mode 100644 index 00000000..1297180a --- /dev/null +++ b/src/Framework/DI/Analysis/InitializerFinder.php @@ -0,0 +1,125 @@ +getShortName(); + + // Wenn Interface kein "Interface" Suffix hat, füge einfach "Initializer" hinzu + if (str_ends_with($interfaceName, 'Interface')) { + $suggestedName = str_replace('Interface', '', $interfaceName) . 'Initializer'; + } else { + // TemplateRenderer -> TemplateRendererInitializer + $suggestedName = $interfaceName . 'Initializer'; + } + + $namespace = $interface->getNamespaceObject(); + + // Strategie 1: Suche im gleichen Namespace (falls Interface nicht in Contracts ist) + $suggestedClass = ClassName::fromNamespace($namespace, $suggestedName); + if ($suggestedClass->exists()) { + return $suggestedClass; + } + + // Strategie 2: Entferne "Contracts" aus dem Namespace + if (str_ends_with($namespace->toString(), '\\Contracts')) { + $parentNamespaceStr = substr($namespace->toString(), 0, -10); // Entferne '\Contracts' + $parentNamespace = PhpNamespace::fromString($parentNamespaceStr); + $suggestedClass = ClassName::fromNamespace($parentNamespace, $suggestedName); + if ($suggestedClass->exists()) { + return $suggestedClass; + } + } + + // Strategie 3: Suche im übergeordneten Namespace (einen Level höher) + $parentNamespace = $namespace->parent(); + if ($parentNamespace !== null) { + $suggestedClass = ClassName::fromNamespace($parentNamespace, $suggestedName); + if ($suggestedClass->exists()) { + return $suggestedClass; + } + } + + // Strategie 4: Suche mit vollständigem App\Framework\ Präfix + $interfaceParts = explode('\\', $interface->toString()); + if (count($interfaceParts) >= 3 && $interfaceParts[0] === 'App' && $interfaceParts[1] === 'Framework') { + // Entferne 'Contracts' falls vorhanden + $filteredParts = array_filter($interfaceParts, fn($part) => $part !== 'Contracts'); + $filteredParts = array_values($filteredParts); + + // Baue Namespace ohne Interface-Name, aber mit Initializer-Name + $baseNamespace = implode('\\', array_slice($filteredParts, 0, -1)); + $baseNamespaceObj = PhpNamespace::fromString($baseNamespace); + $suggestedClass = ClassName::fromNamespace($baseNamespaceObj, $suggestedName); + if ($suggestedClass->exists()) { + return $suggestedClass; + } + } + + // Strategie 5: Fallback - direkter App\Framework\ Präfix + $suggestedClassShort = ClassName::create('App\\Framework\\' . $suggestedName); + if ($suggestedClassShort->exists()) { + return $suggestedClassShort; + } + + return null; + } + + /** + * Findet Initializer für eine Klasse + * + * Namenskonvention: TemplateRenderer → TemplateRendererInitializer + * + * @param ClassName $class Klassen-Name + * @return ClassName|null Initializer-Klasse oder null + */ + public function findForClass(ClassName $class): ?ClassName + { + $className = $class->getShortName(); + $suggestedName = $className . 'Initializer'; + $namespace = $class->getNamespaceObject(); + + // Strategie 1: Suche im gleichen Namespace + $suggestedClass = ClassName::fromNamespace($namespace, $suggestedName); + if ($suggestedClass->exists()) { + return $suggestedClass; + } + + // Strategie 2: Suche im übergeordneten Namespace + $parentNamespace = $namespace->parent(); + if ($parentNamespace !== null) { + $suggestedClass = ClassName::fromNamespace($parentNamespace, $suggestedName); + if ($suggestedClass->exists()) { + return $suggestedClass; + } + } + + // Strategie 3: Fallback - direkter App\Framework\ Präfix + $suggestedClassShort = ClassName::create('App\\Framework\\' . $suggestedName); + if ($suggestedClassShort->exists()) { + return $suggestedClassShort; + } + + return null; + } +} + diff --git a/src/Framework/DI/Analysis/InterfaceResolver.php b/src/Framework/DI/Analysis/InterfaceResolver.php new file mode 100644 index 00000000..5db50c2a --- /dev/null +++ b/src/Framework/DI/Analysis/InterfaceResolver.php @@ -0,0 +1,122 @@ +containerIntrospector !== null) { + try { + $binding = $this->containerIntrospector->getBinding($interface->toString()); + if ($binding !== null) { + $impl = $this->resolveFromBinding($binding, $interface); + if ($impl !== null) { + return $impl; + } + } + } catch (\Throwable) { + // Container-Zugriff fehlgeschlagen - ignoriere und versuche Fallback + } + } + + // 2. Versuche Namenskonvention: Interface -> DefaultInterfaceName + $interfaceName = $interface->getShortName(); + $namespace = $interface->getNamespaceObject(); + + // Versuche "Default" + InterfaceName ohne "Interface" + $implName = str_replace('Interface', '', $interfaceName); + $defaultImplName = 'Default' . $implName; + + // Suche im gleichen Namespace + $defaultImpl = ClassName::fromNamespace($namespace, $defaultImplName); + if ($defaultImpl->exists()) { + return $defaultImpl; + } + + // Suche im übergeordneten Namespace + $parentNamespace = $namespace->parent(); + if ($parentNamespace !== null) { + $defaultImpl = ClassName::fromNamespace($parentNamespace, $defaultImplName); + if ($defaultImpl->exists()) { + return $defaultImpl; + } + } + + return null; + } + + /** + * Resolved Implementierung aus einem Container-Binding + * + * @param callable|string|object $binding Binding vom Container + * @param ClassName $interface Interface das aufgelöst werden soll + * @return ClassName|null + */ + private function resolveFromBinding(callable|string|object $binding, ClassName $interface): ?ClassName + { + // Wenn Binding ein String ist (Klassenname), verwende diesen + if (is_string($binding)) { + try { + $className = ClassName::create($binding); + if ($className->exists()) { + return $className; + } + } catch (\InvalidArgumentException) { + return null; + } + } + + // Wenn Binding ein Objekt ist, verwende dessen Klasse + // ABER: Überspringe Closure, da wir den Return-Type analysieren müssen + if (is_object($binding) && !($binding instanceof \Closure)) { + try { + return ClassName::fromObject($binding); + } catch (\InvalidArgumentException) { + return null; + } + } + + // Wenn Binding ein Closure ist, versuche Return-Type zu analysieren + if ($binding instanceof \Closure) { + $returnType = $this->returnTypeAnalyzer->getClosureReturnType($binding); + if ($returnType !== null && $returnType->exists()) { + return $returnType; + } + + // Fallback: Wenn Closure keinen Return-Type hat, suche Initializer-Klasse + // und analysiere deren __invoke() Return-Type + $initializerClass = $this->initializerFinder->findForInterface($interface); + if ($initializerClass !== null && $initializerClass->exists()) { + $invokeReturnType = $this->returnTypeAnalyzer->getInitializerReturnType($initializerClass); + if ($invokeReturnType !== null && $invokeReturnType->exists()) { + return $invokeReturnType; + } + } + } + + return null; + } +} + diff --git a/src/Framework/DI/Analysis/NamespaceResolver.php b/src/Framework/DI/Analysis/NamespaceResolver.php new file mode 100644 index 00000000..836d4419 --- /dev/null +++ b/src/Framework/DI/Analysis/NamespaceResolver.php @@ -0,0 +1,96 @@ +getFileName(); + if ($fileName === false || !file_exists($fileName)) { + return null; + } + + $fileContent = file_get_contents($fileName); + if ($fileContent === false) { + return null; + } + + // 1. Versuche über use statements zu finden + $resolved = $this->resolveFromUseStatements($shortName, $fileContent); + if ($resolved !== null) { + return $resolved; + } + + // 2. Fallback: Prüfe ob Klasse im gleichen Namespace ist + $namespace = PhpNamespace::fromClass($context->getName()); + if (!$namespace->isGlobal()) { + $fullClassName = $namespace->toString() . '\\' . $shortName; + if (class_exists($fullClassName)) { + return ClassName::create($fullClassName); + } + } + + return null; + } catch (\Throwable) { + return null; + } + } + + /** + * Resolved Klassenname aus use statements + * + * @param string $shortName Kurzer Klassenname + * @param string $fileContent Vollständiger Dateiinhalt + * @return ClassName|null + */ + private function resolveFromUseStatements(string $shortName, string $fileContent): ?ClassName + { + try { + // Finde use statements im File + preg_match_all('/^use\s+([^;]+);/m', $fileContent, $useMatches); + + // Suche nach exakter Übereinstimmung oder Alias + foreach ($useMatches[1] as $useStatement) { + $useStatement = trim($useStatement); + + // Prüfe auf Alias (use Full\Class\Name as Alias) + if (preg_match('/^(.+?)\s+as\s+(\w+)$/', $useStatement, $aliasMatch)) { + $fullClassName = $aliasMatch[1]; + $alias = $aliasMatch[2]; + if ($alias === $shortName) { + return ClassName::create($fullClassName); + } + } else { + // Prüfe ob letzter Teil übereinstimmt + $parts = explode('\\', $useStatement); + $lastPart = end($parts); + if ($lastPart === $shortName) { + return ClassName::create($useStatement); + } + } + } + + return null; + } catch (\Throwable) { + return null; + } + } +} + diff --git a/src/Framework/DI/Analysis/ReturnTypeAnalyzer.php b/src/Framework/DI/Analysis/ReturnTypeAnalyzer.php new file mode 100644 index 00000000..b1d8a007 --- /dev/null +++ b/src/Framework/DI/Analysis/ReturnTypeAnalyzer.php @@ -0,0 +1,110 @@ +getReturnType(); + + if ($returnType instanceof \ReflectionNamedType && !$returnType->isBuiltin()) { + return ClassName::create($returnType->getName()); + } + + return null; + } catch (\Throwable) { + return null; + } + } + + /** + * Analysiert Return-Type einer Methode + */ + public function getMethodReturnType(\ReflectionMethod $method): ?ClassName + { + try { + $returnType = $method->getReturnType(); + + if ($returnType instanceof \ReflectionNamedType && !$returnType->isBuiltin()) { + return ClassName::create($returnType->getName()); + } + + return null; + } catch (\Throwable) { + return null; + } + } + + /** + * Analysiert Initializer __invoke() Return-Type + * + * Versucht sowohl den deklarierten Return-Type als auch die tatsächlich zurückgegebene Klasse + * aus dem Code zu extrahieren (z.B. "return new Engine(...)") + */ + public function getInitializerReturnType(ClassName $initializerClass): ?ClassName + { + try { + if (!$initializerClass->exists()) { + return null; + } + + $reflection = new \ReflectionClass($initializerClass->toString()); + + // Versuche __invoke() Methode zu finden + if (!$reflection->hasMethod('__invoke')) { + return null; + } + + $invokeMethod = $reflection->getMethod('__invoke'); + + // 1. Versuche deklarierten Return-Type + $returnType = $invokeMethod->getReturnType(); + if ($returnType instanceof \ReflectionNamedType && !$returnType->isBuiltin()) { + $declaredReturnType = ClassName::create($returnType->getName()); + + // Wenn Return-Type ein Interface ist, versuche die tatsächliche Klasse aus dem Code zu finden + if ($declaredReturnType->exists()) { + $reflectionClass = new \ReflectionClass($declaredReturnType->toString()); + if ($reflectionClass->isInterface() || $reflectionClass->isAbstract()) { + $actualClass = $this->codeParser->extractReturnClass($invokeMethod); + if ($actualClass !== null) { + return $actualClass; + } + } + } + + return $declaredReturnType; + } + + // 2. Versuche tatsächliche Klasse aus dem Code zu extrahieren + $actualClass = $this->codeParser->extractReturnClass($invokeMethod); + if ($actualClass !== null) { + return $actualClass; + } + + return null; + } catch (\Throwable) { + return null; + } + } +} + diff --git a/src/Framework/DI/InitializerDependencyAnalyzer.php b/src/Framework/DI/InitializerDependencyAnalyzer.php index fbc5d69e..926dcfff 100644 --- a/src/Framework/DI/InitializerDependencyAnalyzer.php +++ b/src/Framework/DI/InitializerDependencyAnalyzer.php @@ -4,6 +4,13 @@ declare(strict_types=1); namespace App\Framework\DI; +use App\Framework\Core\ValueObjects\ClassName; +use App\Framework\DI\Analysis\CodeParser; +use App\Framework\DI\Analysis\DependencyPathAnalyzer; +use App\Framework\DI\Analysis\InitializerFinder; +use App\Framework\DI\Analysis\InterfaceResolver; +use App\Framework\DI\Analysis\ReturnTypeAnalyzer; + /** * Analysiert Dependencies von Initializern * @@ -13,12 +20,37 @@ namespace App\Framework\DI; */ final readonly class InitializerDependencyAnalyzer { - private const MAX_RECURSION_DEPTH = 4; - + private CodeParser $codeParser; + private InitializerFinder $initializerFinder; + private ReturnTypeAnalyzer $returnTypeAnalyzer; + private ?InterfaceResolver $interfaceResolver; + private ?DependencyPathAnalyzer $dependencyPathAnalyzer; + public function __construct( private ?Container $container = null ) { + $this->codeParser = new CodeParser(); + $this->initializerFinder = new InitializerFinder(); + $this->returnTypeAnalyzer = new ReturnTypeAnalyzer($this->codeParser, $this->initializerFinder); + + // Initialisiere ContainerIntrospector wenn Container verfügbar + $containerIntrospector = null; + if ($this->container !== null && property_exists($this->container, 'introspector')) { + $containerIntrospector = $this->container->introspector; + } + + $this->interfaceResolver = new InterfaceResolver( + $containerIntrospector, + $this->initializerFinder, + $this->returnTypeAnalyzer + ); + + $this->dependencyPathAnalyzer = new DependencyPathAnalyzer( + $this->interfaceResolver, + $this->codeParser + ); } + /** * Analysiere Dependencies eines Initializers * @@ -47,14 +79,14 @@ final readonly class InitializerDependencyAnalyzer // 1. Analysiere Constructor-Dependencies $constructor = $reflection->getConstructor(); $hasContainerInConstructor = false; - + if ($constructor !== null) { foreach ($constructor->getParameters() as $parameter) { $type = $parameter->getType(); if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) { $typeName = $type->getName(); $constructorDeps[] = $typeName; - + // Prüfe ob Container verfügbar ist if ($typeName === Container::class || $typeName === 'App\Framework\DI\Container') { $hasContainerInConstructor = true; @@ -67,10 +99,10 @@ final readonly class InitializerDependencyAnalyzer $containerGetDeps = []; $hasContainerGetCalls = false; $hasContainerInInvoke = false; - + try { $invokeMethod = $reflection->getMethod('__invoke'); - + // Prüfe ob Container als Parameter in __invoke() verfügbar ist foreach ($invokeMethod->getParameters() as $parameter) { $type = $parameter->getType(); @@ -79,21 +111,25 @@ final readonly class InitializerDependencyAnalyzer if ($typeName === Container::class || $typeName === 'App\Framework\DI\Container') { $hasContainerInInvoke = true; } - + // Füge auch zu constructorDeps hinzu wenn nicht schon vorhanden if (!in_array($typeName, $constructorDeps, true)) { $constructorDeps[] = $typeName; } } } - + // Nur nach container->get() suchen wenn Container verfügbar ist $hasContainerAvailable = $hasContainerInConstructor || $hasContainerInInvoke; - + if ($hasContainerAvailable) { // Parse Code der __invoke() Methode um container->get() Aufrufe zu finden - $parsedDeps = $this->parseContainerGetCalls($invokeMethod); - $containerGetDeps = $parsedDeps['dependencies']; + $parsedDeps = $this->codeParser->parseContainerGetCalls($invokeMethod); + // Konvertiere ClassName[] zurück zu string[] + $containerGetDeps = array_map( + fn(ClassName $className) => $className->toString(), + $parsedDeps['dependencies'] + ); $hasContainerGetCalls = $parsedDeps['hasCalls']; } } catch (\ReflectionException) { @@ -117,129 +153,6 @@ final readonly class InitializerDependencyAnalyzer } } - /** - * Parse container->get() Aufrufe aus einer Method - * - * @return array{dependencies: array, hasCalls: bool} - */ - private function parseContainerGetCalls(\ReflectionMethod $method): array - { - try { - $fileName = $method->getFileName(); - if ($fileName === false || !file_exists($fileName)) { - return ['dependencies' => [], 'hasCalls' => false]; - } - - $fileContent = file_get_contents($fileName); - if ($fileContent === false) { - return ['dependencies' => [], 'hasCalls' => false]; - } - - $startLine = $method->getStartLine(); - $endLine = $method->getEndLine(); - - if ($startLine === false || $endLine === false) { - return ['dependencies' => [], 'hasCalls' => false]; - } - - // Extrahiere nur die Method - $lines = explode("\n", $fileContent); - $methodLines = array_slice($lines, $startLine - 1, $endLine - $startLine + 1); - $methodCode = implode("\n", $methodLines); - - // Finde container->get(...) Aufrufe - // Pattern: $container->get(ClassName::class) oder $this->container->get(ClassName::class) - // Unterstützt auch ::class Notation - $pattern = '/(?:\$container|\$this->container)->get\(([^,\)]+::class|[^,\)]+)\)/'; - preg_match_all($pattern, $methodCode, $matches); - - $dependencies = []; - if (!empty($matches[1])) { - foreach ($matches[1] as $match) { - $match = trim($match); - - // Entferne ::class falls vorhanden - $className = str_replace('::class', '', $match); - $className = trim($className, '\'"'); - - // Prüfe ob es ein gültiger Klassenname ist (beginnt mit \ oder Namespace) - if (preg_match('/^\\\\?[A-Z][A-Za-z0-9\\\\]*$/', $className)) { - // Normalisiere (füge \ am Anfang hinzu wenn nicht vorhanden) - if (!str_starts_with($className, '\\')) { - // Versuche vollständigen Namespace zu finden (z.B. über use statements) - $fullClassName = $this->resolveClassNameFromMethod($className, $fileContent, $method); - if ($fullClassName !== null) { - $dependencies[] = $fullClassName; - } else { - // Fallback: Verwende wie angegeben - $dependencies[] = $className; - } - } else { - $dependencies[] = $className; - } - } - } - - // Entferne Duplikate - $dependencies = array_unique($dependencies); - $dependencies = array_values($dependencies); - } - - return [ - 'dependencies' => $dependencies, - 'hasCalls' => !empty($matches[0]), - ]; - } catch (\Throwable) { - return ['dependencies' => [], 'hasCalls' => false]; - } - } - - /** - * Versuche vollständigen Klassenname aus use statements zu resolven - */ - private function resolveClassNameFromMethod(string $shortName, string $fileContent, \ReflectionMethod $method): ?string - { - try { - // Finde use statements im File - preg_match_all('/^use\s+([^;]+);/m', $fileContent, $useMatches); - - // Suche nach exakter Übereinstimmung oder Alias - foreach ($useMatches[1] as $useStatement) { - $useStatement = trim($useStatement); - - // Prüfe auf Alias (use Full\Class\Name as Alias) - if (preg_match('/^(.+?)\s+as\s+(\w+)$/', $useStatement, $aliasMatch)) { - $fullClassName = $aliasMatch[1]; - $alias = $aliasMatch[2]; - if ($alias === $shortName) { - return $fullClassName; - } - } else { - // Prüfe ob letzter Teil übereinstimmt - $parts = explode('\\', $useStatement); - $lastPart = end($parts); - if ($lastPart === $shortName) { - return $useStatement; - } - } - } - - // Fallback: Prüfe ob Klasse im gleichen Namespace ist - // Extrahiere Namespace aus der Datei - if (preg_match('/^namespace\s+([^;]+);/m', $fileContent, $namespaceMatch)) { - $namespace = trim($namespaceMatch[1]); - $fullClassName = $namespace . '\\' . $shortName; - if (class_exists($fullClassName)) { - return $fullClassName; - } - } - - return null; - } catch (\Throwable) { - return null; - } - } - /** * Finde rekursiv den Pfad von einer Dependency bis zum Interface * @@ -257,576 +170,37 @@ final readonly class InitializerDependencyAnalyzer array $currentPath = [], int $depth = 0 ): ?array { - // Max. Rekursionstiefe erreicht - if ($depth >= self::MAX_RECURSION_DEPTH) { + if ($this->dependencyPathAnalyzer === null) { return null; } - // Cycle-Detection: Vermeide Endlosschleifen - if (in_array($dependencyClass, $visited, true)) { - return null; - } + try { + $dependencyClassName = ClassName::create($dependencyClass); + $targetInterfaceName = ClassName::create($targetInterface); - // Prüfe ob diese Klasse direkt das Interface benötigt - if ($this->dependencyNeedsInterface($dependencyClass, $targetInterface)) { - return array_merge($currentPath, [$dependencyClass, $targetInterface]); - } + // Konvertiere visited und currentPath zu ClassName[] + $visitedClassNames = array_map(fn(string $class) => ClassName::create($class), $visited); + $currentPathClassNames = array_map(fn(string $class) => ClassName::create($class), $currentPath); - // Rekursiv: Analysiere Dependencies dieser Klasse - $dependencies = $this->getClassDependencies($dependencyClass); - - if (empty($dependencies)) { - return null; - } - - $newVisited = array_merge($visited, [$dependencyClass]); - $newPath = array_merge($currentPath, [$dependencyClass]); - - foreach ($dependencies as $subDependency) { - // Überspringe Container selbst (würde alle Dependencies auflisten) - if ($subDependency === Container::class || $subDependency === 'App\Framework\DI\Container') { - continue; - } - - $path = $this->findDependencyPathToInterface( - $subDependency, - $targetInterface, - $newVisited, - $newPath, - $depth + 1 + $path = $this->dependencyPathAnalyzer->findPathToInterface( + $dependencyClassName, + $targetInterfaceName, + $visitedClassNames, + $currentPathClassNames, + $depth ); - if ($path !== null) { - return $path; - } - } - - return null; - } - - /** - * Prüfe ob eine Klasse ein Interface benötigt (über Reflection) - */ - private function dependencyNeedsInterface(string $dependencyClass, string $interface): bool - { - try { - if (!class_exists($dependencyClass)) { - return false; - } - - $reflection = new \ReflectionClass($dependencyClass); - $constructor = $reflection->getConstructor(); - - if ($constructor === null) { - return false; - } - - // Prüfe alle Constructor-Parameter - foreach ($constructor->getParameters() as $parameter) { - $type = $parameter->getType(); - - if ($type === null) { - continue; - } - - // Direkter NamedType - if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) { - if ($type->getName() === $interface) { - return true; - } - } - - // Union Types (PHP 8.0+) - if ($type instanceof \ReflectionUnionType) { - foreach ($type->getTypes() as $subType) { - if ($subType instanceof \ReflectionNamedType && !$subType->isBuiltin()) { - if ($subType->getName() === $interface) { - return true; - } - } - } - } - - // Intersection Types (PHP 8.1+) - if ($type instanceof \ReflectionIntersectionType) { - foreach ($type->getTypes() as $subType) { - if ($subType instanceof \ReflectionNamedType && !$subType->isBuiltin()) { - if ($subType->getName() === $interface) { - return true; - } - } - } - } - } - - return false; - } catch (\Throwable) { - return false; - } - } - - /** - * Hole Dependencies einer Klasse (Constructor-Parameter + Array-Elemente) - * - * @return array - */ - private function getClassDependencies(string $className): array - { - try { - if (!class_exists($className) && !interface_exists($className)) { - return []; - } - - $reflection = new \ReflectionClass($className); - - // Wenn es ein Interface ist, versuche die Implementierung zu finden - if ($reflection->isInterface()) { - // Suche nach bekannten Implementierungen (basierend auf Namenskonvention) - $implClass = $this->findInterfaceImplementation($className); - if ($implClass !== null && class_exists($implClass)) { - $reflection = new \ReflectionClass($implClass); - } else { - // Kann keine Dependencies für Interfaces ohne Implementierung finden - return []; - } - } - - $constructor = $reflection->getConstructor(); - - if ($constructor === null) { - return []; - } - - $dependencies = []; - foreach ($constructor->getParameters() as $parameter) { - $type = $parameter->getType(); - - if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) { - $dependencies[] = $type->getName(); - } - } - - // Zusätzlich: Prüfe ob Parameter Arrays sind, die Klassen-Namen enthalten könnten - // (z.B. TemplateProcessor hat $astTransformers als Array von Klassen-Namen) - foreach ($constructor->getParameters() as $parameter) { - $type = $parameter->getType(); - if ($type instanceof \ReflectionNamedType && $type->getName() === 'array') { - // Versuche Klassen-Namen aus dem Array zu extrahieren (via Code-Parsing) - $arrayDeps = $this->extractClassNamesFromArrayParameter($reflection, $parameter); - $dependencies = array_merge($dependencies, $arrayDeps); - } - } - - return array_unique($dependencies); - } catch (\Throwable) { - return []; - } - } - - /** - * Extrahiere Klassen-Namen aus Array-Parametern (z.B. $astTransformers = [XComponentTransformer::class]) - * - * Sucht sowohl im Constructor als auch in Initializern (__invoke) - */ - private function extractClassNamesFromArrayParameter(\ReflectionClass $class, \ReflectionParameter $parameter): array - { - try { - $fileName = $class->getFileName(); - if ($fileName === false || !file_exists($fileName)) { - return []; - } - - $fileContent = file_get_contents($fileName); - if ($fileContent === false) { - return []; - } - - $paramName = $parameter->getName(); - $classes = []; - - // 1. Suche im Constructor - $constructor = $class->getConstructor(); - if ($constructor !== null) { - $startLine = $constructor->getStartLine(); - $endLine = $constructor->getEndLine(); - - if ($startLine !== false && $endLine !== false) { - $lines = explode("\n", $fileContent); - $constructorLines = array_slice($lines, $startLine - 1, $endLine - $startLine + 1); - $constructorCode = implode("\n", $constructorLines); - - // Finde Array-Initialisierungen für diesen Parameter - // Pattern: [ClassName::class, ...] oder ['ClassName', ...] - $pattern = '/\$' . preg_quote($paramName, '/') . '\s*=\s*\[(.*?)\]/s'; - if (preg_match($pattern, $constructorCode, $matches)) { - $arrayClasses = $this->extractClassesFromArrayContent($matches[1], $fileContent, $constructor); - $classes = array_merge($classes, $arrayClasses); - } - } - } - - // 2. Suche auch in __invoke() (für Initializer) - try { - $invokeMethod = $class->getMethod('__invoke'); - $startLine = $invokeMethod->getStartLine(); - $endLine = $invokeMethod->getEndLine(); - - if ($startLine !== false && $endLine !== false) { - $lines = explode("\n", $fileContent); - $invokeLines = array_slice($lines, $startLine - 1, $endLine - $startLine + 1); - $invokeCode = implode("\n", $invokeLines); - - // Finde Array-Initialisierungen die an diesen Parameter übergeben werden - // Pattern: [$paramName] = [...] oder new Class([...]) - // Oder: $paramName = [...] - $pattern = '/\$' . preg_quote($paramName, '/') . '\s*=\s*\[(.*?)\]/s'; - if (preg_match($pattern, $invokeCode, $matches)) { - $arrayClasses = $this->extractClassesFromArrayContent($matches[1], $fileContent, $invokeMethod); - $classes = array_merge($classes, $arrayClasses); - } - - // Suche auch nach direkten Array-Definitionen die an den Parameter übergeben werden - // z.B. new TemplateProcessor(astTransformers: [XComponentTransformer::class, ...]) - $pattern = '/\b' . preg_quote($paramName, '/') . '\s*:\s*\[(.*?)\]/s'; - if (preg_match($pattern, $invokeCode, $matches)) { - $arrayClasses = $this->extractClassesFromArrayContent($matches[1], $fileContent, $invokeMethod); - $classes = array_merge($classes, $arrayClasses); - } - } - } catch (\ReflectionException) { - // __invoke() existiert nicht - das ist okay - } - - return array_unique($classes); - } catch (\Throwable) { - return []; - } - } - - /** - * Extrahiere Klassen-Namen aus Array-Content-String - */ - private function extractClassesFromArrayContent(string $arrayContent, string $fileContent, \ReflectionMethod $method): array - { - // Finde alle Klassen-Namen (::class oder als String) - $classPattern = '/([A-Z][A-Za-z0-9\\\\]+)(::class|\')/'; - preg_match_all($classPattern, $arrayContent, $classMatches); - - $classes = []; - if (!empty($classMatches[1])) { - foreach ($classMatches[1] as $classMatch) { - // Normalisiere (füge \ am Anfang hinzu wenn nicht vorhanden) - if (!str_starts_with($classMatch, '\\') && !str_starts_with($classMatch, 'App\\')) { - // Versuche vollständigen Namespace zu finden - $fullClassName = $this->resolveClassNameFromMethod($classMatch, $fileContent, $method); - if ($fullClassName !== null) { - $classes[] = $fullClassName; - } elseif (class_exists($classMatch)) { - $classes[] = $classMatch; - } - } elseif (class_exists($classMatch)) { - $classes[] = $classMatch; - } - } - } - - return $classes; - } - - /** - * Versuche Implementierung eines Interfaces zu finden - * - * Prüft zuerst Container-Bindings, dann Namenskonventionen - */ - private function findInterfaceImplementation(string $interface): ?string - { - // 1. Versuche über Container-Bindings (falls Container verfügbar) - if ($this->container !== null) { - try { - // Prüfe ob Interface gebunden ist - if ($this->container->has($interface)) { - $binding = $this->getBindingForInterface($interface); - if ($binding !== null) { - // Wenn Binding ein String ist (Klassenname), verwende diesen - if (is_string($binding) && class_exists($binding)) { - return $binding; - } - - // Wenn Binding ein Objekt ist, verwende dessen Klasse - // ABER: Überspringe Closure, da wir den Return-Type analysieren müssen - if (is_object($binding) && !($binding instanceof \Closure)) { - return $binding::class; - } - - // Wenn Binding ein Closure ist, versuche Return-Type zu analysieren - if ($binding instanceof \Closure) { - $returnType = $this->getClosureReturnType($binding); - if ($returnType !== null && class_exists($returnType)) { - return $returnType; - } - - // Fallback: Wenn Closure keinen Return-Type hat, suche Initializer-Klasse - // und analysiere deren __invoke() Return-Type - $initializerClass = $this->findInitializerForInterface($interface); - if ($initializerClass !== null && class_exists($initializerClass)) { - $invokeReturnType = $this->getInitializerInvokeReturnType($initializerClass); - if ($invokeReturnType !== null && class_exists($invokeReturnType)) { - return $invokeReturnType; - } - } - } - } - } - } catch (\Throwable) { - // Container-Zugriff fehlgeschlagen - ignoriere und versuche Fallback - } - } - - // 2. Versuche Namenskonvention: Interface -> DefaultInterfaceName - $interfaceName = basename(str_replace('\\', '/', $interface)); - $namespace = substr($interface, 0, strrpos($interface, '\\')); - - // Versuche "Default" + InterfaceName ohne "Interface" - $implName = str_replace('Interface', '', $interfaceName); - $defaultImpl = $namespace . '\\Default' . $implName; - if (class_exists($defaultImpl)) { - return $defaultImpl; - } - - // Versuche einfach InterfaceName ohne "Interface" - $simpleImpl = $namespace . '\\' . $implName; - if (class_exists($simpleImpl)) { - return $simpleImpl; - } - - return null; - } - - /** - * Hole Binding für ein Interface aus dem Container - */ - private function getBindingForInterface(string $interface): callable|string|object|null - { - if ($this->container === null) { - return null; - } - - try { - // Versuche über Introspector (sicherer, keine Instanziierung) - // Introspector ist eine readonly Property im Container - if (property_exists($this->container, 'introspector')) { - $introspector = $this->container->introspector; - return $introspector->getBinding($interface); - } - - // Fallback: Versuche direkt über BindingRegistry (wenn verfügbar) - // Das ist nicht ideal, aber besser als nichts - return null; - } catch (\Throwable) { - return null; - } - } - - /** - * Versuche Return-Type eines Closures zu extrahieren - */ - private function getClosureReturnType(\Closure $closure): ?string - { - try { - $reflection = new \ReflectionFunction($closure); - $returnType = $reflection->getReturnType(); - - if ($returnType instanceof \ReflectionNamedType && !$returnType->isBuiltin()) { - return $returnType->getName(); - } - - return null; - } catch (\Throwable) { - return null; - } - } - - /** - * Finde Initializer-Klasse für ein Interface (basierend auf Namenskonvention) - */ - private function findInitializerForInterface(string $interface): ?string - { - // Namenskonvention: - // - ComponentRegistryInterface -> ComponentRegistryInitializer - // - TemplateRenderer -> TemplateRendererInitializer (Interface ohne "Interface" Suffix) - $interfaceName = basename(str_replace('\\', '/', $interface)); - - // Wenn Interface kein "Interface" Suffix hat, füge einfach "Initializer" hinzu - if (str_ends_with($interfaceName, 'Interface')) { - $suggestedName = str_replace('Interface', '', $interfaceName) . 'Initializer'; - } else { - // TemplateRenderer -> TemplateRendererInitializer - $suggestedName = $interfaceName . 'Initializer'; - } - - $namespace = substr($interface, 0, strrpos($interface, '\\')); - - // Strategie 1: Suche im gleichen Namespace (falls Interface nicht in Contracts ist) - $suggestedClass = $namespace . '\\' . $suggestedName; - if (class_exists($suggestedClass)) { - return $suggestedClass; - } - - // Strategie 2: Entferne "Contracts" aus dem Namespace - if (str_ends_with($namespace, '\\Contracts')) { - $parentNamespace = substr($namespace, 0, -10); // Entferne '\Contracts' - $suggestedClass = $parentNamespace . '\\' . $suggestedName; - if (class_exists($suggestedClass)) { - return $suggestedClass; - } - } - - // Strategie 3: Suche im übergeordneten Namespace (einen Level höher) - if (strrpos($namespace, '\\') !== false) { - $parentNamespace = substr($namespace, 0, strrpos($namespace, '\\')); - $suggestedClass = $parentNamespace . '\\' . $suggestedName; - if (class_exists($suggestedClass)) { - return $suggestedClass; - } - } - - // Strategie 4: Suche mit vollständigem App\Framework\ Präfix - $interfaceParts = explode('\\', $interface); - if (count($interfaceParts) >= 3 && $interfaceParts[0] === 'App' && $interfaceParts[1] === 'Framework') { - // Entferne 'Contracts' falls vorhanden - $filteredParts = array_filter($interfaceParts, fn($part) => $part !== 'Contracts'); - $filteredParts = array_values($filteredParts); - - // Baue Namespace ohne Interface-Name, aber mit Initializer-Name - $baseNamespace = implode('\\', array_slice($filteredParts, 0, -1)); - $suggestedClass = $baseNamespace . '\\' . $suggestedName; - if (class_exists($suggestedClass)) { - return $suggestedClass; - } - } - - // Strategie 5: Fallback - direkter App\Framework\ Präfix - $suggestedClassShort = 'App\\Framework\\' . $suggestedName; - if (class_exists($suggestedClassShort)) { - return $suggestedClassShort; - } - - return null; - } - - /** - * Analysiere Return-Type der __invoke() Methode eines Initializers - * - * Versucht sowohl den deklarierten Return-Type als auch die tatsächlich zurückgegebene Klasse - * aus dem Code zu extrahieren (z.B. "return new Engine(...)") - */ - private function getInitializerInvokeReturnType(string $initializerClass): ?string - { - try { - if (!class_exists($initializerClass)) { + if ($path === null) { return null; } - $reflection = new \ReflectionClass($initializerClass); - - // Versuche __invoke() Methode zu finden - if (!$reflection->hasMethod('__invoke')) { - return null; - } - - $invokeMethod = $reflection->getMethod('__invoke'); - - // 1. Versuche deklarierten Return-Type - $returnType = $invokeMethod->getReturnType(); - if ($returnType instanceof \ReflectionNamedType && !$returnType->isBuiltin()) { - $declaredReturnType = $returnType->getName(); - - // Wenn Return-Type ein Interface ist, versuche die tatsächliche Klasse aus dem Code zu finden - if (interface_exists($declaredReturnType) || (class_exists($declaredReturnType) && (new \ReflectionClass($declaredReturnType))->isAbstract())) { - $actualClass = $this->extractActualReturnClassFromInvoke($invokeMethod); - if ($actualClass !== null) { - return $actualClass; - } - } - - return $declaredReturnType; - } - - // 2. Versuche tatsächliche Klasse aus dem Code zu extrahieren - $actualClass = $this->extractActualReturnClassFromInvoke($invokeMethod); - if ($actualClass !== null) { - return $actualClass; - } - - return null; - } catch (\Throwable) { - return null; - } - } - - /** - * Extrahiere die tatsächlich zurückgegebene Klasse aus dem __invoke() Code - * (z.B. "return new Engine(...)" → "Engine") - */ - private function extractActualReturnClassFromInvoke(\ReflectionMethod $invokeMethod): ?string - { - try { - $fileName = $invokeMethod->getFileName(); - if ($fileName === false || !file_exists($fileName)) { - return null; - } - - $fileContent = file_get_contents($fileName); - if ($fileContent === false) { - return null; - } - - $startLine = $invokeMethod->getStartLine(); - $endLine = $invokeMethod->getEndLine(); - - if ($startLine === false || $endLine === false) { - return null; - } - - // Extrahiere nur den __invoke() Method-Code - $lines = explode("\n", $fileContent); - $methodLines = array_slice($lines, $startLine - 1, $endLine - $startLine + 1); - $methodCode = implode("\n", $methodLines); - - // Suche nach "return new ClassName(" oder "return new ClassName;" - // Pattern muss auch Named Parameters unterstützen: "return new Engine(...)" - $pattern = '/return\s+new\s+([A-Z][A-Za-z0-9\\\\]+)\s*[\(;]/'; - if (preg_match($pattern, $methodCode, $matches)) { - $className = $matches[1]; - - // Resolve vollständigen Namespace - $fullClassName = $this->resolveClassNameFromMethod($className, $fileContent, $invokeMethod); - if ($fullClassName !== null && class_exists($fullClassName)) { - return $fullClassName; - } elseif (class_exists($className)) { - return $className; - } - } - - // Fallback: Suche auch nach Named Parameters Syntax: "return new Engine(...)" - // mit Named Parameters: loader: ..., processor: ... - $pattern2 = '/return\s+new\s+([A-Z][A-Za-z0-9\\\\]+)\s*\(/'; - if (preg_match($pattern2, $methodCode, $matches)) { - $className = $matches[1]; - - // Resolve vollständigen Namespace - $fullClassName = $this->resolveClassNameFromMethod($className, $fileContent, $invokeMethod); - if ($fullClassName !== null && class_exists($fullClassName)) { - return $fullClassName; - } elseif (class_exists($className)) { - return $className; - } - } - + // Konvertiere ClassName[] zurück zu string[] + return array_map(fn(ClassName $className) => $className->toString(), $path); + } catch (\InvalidArgumentException) { + // Ungültiger Klassenname return null; } catch (\Throwable) { return null; } } } - diff --git a/src/Framework/View/LiveComponentRenderer.php b/src/Framework/View/LiveComponentRenderer.php index 86b31e35..e07c8bc7 100644 --- a/src/Framework/View/LiveComponentRenderer.php +++ b/src/Framework/View/LiveComponentRenderer.php @@ -6,6 +6,8 @@ namespace App\Framework\View; use App\Framework\Http\Session\SessionInterface; use App\Framework\Meta\MetaData; +use App\Framework\View\Loading\TemplateLoader; +use App\Framework\View\TemplateProcessor; /** * Renders LiveComponent templates @@ -16,7 +18,8 @@ use App\Framework\Meta\MetaData; final readonly class LiveComponentRenderer { public function __construct( - private TemplateRenderer $templateRenderer, + private TemplateLoader $templateLoader, + private TemplateProcessor $templateProcessor, private SessionInterface $session ) { } @@ -46,7 +49,13 @@ final readonly class LiveComponentRenderer processingMode: ProcessingMode::COMPONENT ); - return $this->templateRenderer->renderPartial($context); + $templateHtml = $this->templateLoader->load( + template: $templatePath, + controllerClass: null, + context: $context + ); + + return $this->templateProcessor->render($context, $templateHtml, component: true); } /** diff --git a/src/Framework/View/TemplateLoaderInitializer.php b/src/Framework/View/TemplateLoaderInitializer.php new file mode 100644 index 00000000..5ac6859d --- /dev/null +++ b/src/Framework/View/TemplateLoaderInitializer.php @@ -0,0 +1,39 @@ +discoveryRegistry, + cacheEnabled: self::CACHE_ENABLED + ); + + $this->container->singleton(TemplateLoader::class, $loader); + + return $loader; + } +} + diff --git a/src/Framework/View/TemplateProcessorInitializer.php b/src/Framework/View/TemplateProcessorInitializer.php new file mode 100644 index 00000000..f21c7d51 --- /dev/null +++ b/src/Framework/View/TemplateProcessorInitializer.php @@ -0,0 +1,72 @@ +enable(); + } + + $processor = new TemplateProcessor( + astTransformers: $astTransformers, + stringProcessors: $stringProcessors, + container: $this->container, + chainOptimizer: $chainOptimizer, + compiledTemplateCache: $compiledTemplateCache, + performanceTracker: $performanceTracker + ); + + $this->container->singleton(TemplateProcessor::class, $processor); + + return $processor; + } +} + diff --git a/src/Framework/View/TemplateRendererInitializer.php b/src/Framework/View/TemplateRendererInitializer.php index a6a61195..8be6720c 100644 --- a/src/Framework/View/TemplateRendererInitializer.php +++ b/src/Framework/View/TemplateRendererInitializer.php @@ -5,102 +5,30 @@ declare(strict_types=1); namespace App\Framework\View; use App\Framework\Cache\Cache; -use App\Framework\Core\PathProvider; use App\Framework\DI\DefaultContainer; use App\Framework\DI\Initializer; -use App\Framework\Discovery\Results\DiscoveryRegistry; use App\Framework\Performance\PerformanceService; -use App\Framework\View\Dom\Transformer\AssetInjectorTransformer; -use App\Framework\View\Dom\Transformer\CommentStripTransformer; -use App\Framework\View\Dom\Transformer\ForTransformer; -use App\Framework\View\Dom\Transformer\HoneypotTransformer; -use App\Framework\View\Dom\Transformer\IfTransformer; -use App\Framework\View\Dom\Transformer\LayoutTagTransformer; -use App\Framework\View\Dom\Transformer\MetaManipulatorTransformer; -use App\Framework\View\Dom\Transformer\WhitespaceCleanupTransformer; -use App\Framework\View\Dom\Transformer\XComponentTransformer; use App\Framework\View\Loading\TemplateLoader; -use App\Framework\View\Processors\PlaceholderReplacer; -use App\Framework\View\Processors\VoidElementsSelfClosingProcessor; final readonly class TemplateRendererInitializer { public function __construct( private DefaultContainer $container, - private DiscoveryRegistry $results, ) {} #[Initializer] public function __invoke(): TemplateRenderer { - // AST Transformers (new approach) - Modern template processing - $astTransformers = [ - // Core transformers (order matters!) - LayoutTagTransformer::class, // Process tags FIRST (before other processing) - XComponentTransformer::class, // Process components (LiveComponents + HtmlComponents) - ForTransformer::class, // Process foreach loops and elements (BEFORE if/placeholders) - IfTransformer::class, // Conditional rendering (if/condition attributes) - MetaManipulatorTransformer::class, // Set meta tags from context - AssetInjectorTransformer::class, // Inject Vite assets (CSS/JS) - HoneypotTransformer::class, // Add honeypot spam protection to forms - CommentStripTransformer::class, // Remove HTML comments - WhitespaceCleanupTransformer::class, // Remove empty text nodes - ]; - - // TODO: Migrate remaining DOM processors to AST transformers: - // - ComponentProcessor (for tags) - COMPLEX, keep in DOM for now - // - TableProcessor (for table rendering) - OPTIONAL - // - FormProcessor (for form handling) - OPTIONAL - - $strings = [ - PlaceholderReplacer::class, // PlaceholderReplacer handles simple {{ }} replacements - VoidElementsSelfClosingProcessor::class, - ]; - - /** @var Cache $cache */ - $cache = $this->container->get(Cache::class); - - // Performance-Optimierungen (optional) - $chainOptimizer = new ProcessorChainOptimizer($cache); - $compiledTemplateCache = new CompiledTemplateCache($cache); - - // Performance Tracker nur in Development/Profiling - $performanceTracker = null; - if (getenv('ENABLE_TEMPLATE_PROFILING') === 'true') { - $performanceTracker = new ProcessorPerformanceTracker(); - $performanceTracker->enable(); - } - - $templateProcessor = new TemplateProcessor( - astTransformers: $astTransformers, - stringProcessors: $strings, - container: $this->container, - chainOptimizer: $chainOptimizer, - compiledTemplateCache: $compiledTemplateCache, - performanceTracker: $performanceTracker - ); - - $this->container->singleton(TemplateProcessor::class, $templateProcessor); - - /** @var PathProvider $pathProvider */ - $pathProvider = $this->container->get(PathProvider::class); - /** @var Cache $cache */ - $cache = $this->container->get(Cache::class); + $templateProcessor = $this->container->get(TemplateProcessor::class); + $loader = $this->container->get(TemplateLoader::class); /** @var PerformanceService $performanceService */ $performanceService = $this->container->get(PerformanceService::class); - // Define caching state centrally - $cacheEnabled = false; // Keep caching disabled while debugging template processing + /** @var Cache $cache */ + $cache = $this->container->get(Cache::class); - $loader = new TemplateLoader( - pathProvider: $pathProvider, - cache: $cache, - discoveryRegistry: $this->results, - cacheEnabled: $cacheEnabled // Pass cache state to loader - ); - - $this->container->singleton(TemplateLoader::class, $loader); + $cacheEnabled = false; return new Engine( loader: $loader, diff --git a/tests/Feature/Framework/LiveComponents/CsrfIntegrationTest.php b/tests/Feature/Framework/LiveComponents/CsrfIntegrationTest.php index bc95ec3e..dc2d2267 100644 --- a/tests/Feature/Framework/LiveComponents/CsrfIntegrationTest.php +++ b/tests/Feature/Framework/LiveComponents/CsrfIntegrationTest.php @@ -2,7 +2,9 @@ declare(strict_types=1); -use App\Framework\Http\Session\SessionInterface; +use App\Framework\DateTime\SystemClock; +use App\Framework\Http\Session\Session; +use App\Framework\Http\Session\SessionId; use App\Framework\LiveComponents\ComponentEventDispatcher; use App\Framework\LiveComponents\Contracts\LiveComponentContract; use App\Framework\LiveComponents\LiveComponentHandler; @@ -10,74 +12,20 @@ use App\Framework\LiveComponents\ValueObjects\ActionParameters; use App\Framework\LiveComponents\ValueObjects\ComponentAction; use App\Framework\LiveComponents\ValueObjects\ComponentData; use App\Framework\LiveComponents\ValueObjects\ComponentId; +use App\Framework\Random\SecureRandomGenerator; use App\Framework\Security\CsrfToken; +use App\Framework\Security\CsrfTokenGenerator; use App\Framework\View\LiveComponentRenderer; -use App\Framework\View\TemplateRenderer; +use App\Framework\View\Loading\TemplateLoader; +use App\Framework\View\TemplateProcessor; beforeEach(function () { - // Create mock SessionInterface for CSRF testing - $this->session = new class () implements SessionInterface { - private array $tokens = []; - - public function __get(string $name): mixed - { - if ($name === 'csrf') { - return new class ($this) { - public function __construct(private $session) - { - } - - public function generateToken(string $formId): CsrfToken - { - $token = CsrfToken::generate(); - $this->session->tokens[$formId] = $token->toString(); - - return $token; - } - - public function validateToken(string $formId, CsrfToken $token): bool - { - return isset($this->session->tokens[$formId]) - && hash_equals($this->session->tokens[$formId], $token->toString()); - } - }; - } - - return null; - } - - public function get(string $key, mixed $default = null): mixed - { - return $default; - } - - public function set(string $key, mixed $value): void - { - } - - public function has(string $key): bool - { - return false; - } - - public function remove(string $key): void - { - } - - public function regenerate(): bool - { - return true; - } - - public function destroy(): void - { - } - - public function getId(): string - { - return 'test-session-id'; - } - }; + $this->session = Session::fromArray( + SessionId::fromString(str_repeat('a', 32)), + new SystemClock(), + new CsrfTokenGenerator(new SecureRandomGenerator()), + [] + ); $this->eventDispatcher = new ComponentEventDispatcher(); $this->handler = new LiveComponentHandler($this->eventDispatcher, $this->session); @@ -86,7 +34,8 @@ beforeEach(function () { describe('CSRF Token Generation', function () { it('generates unique CSRF token for each component instance', function () { $renderer = new LiveComponentRenderer( - $this->createMock(TemplateRenderer::class), + $this->createMock(TemplateLoader::class), + $this->createMock(TemplateProcessor::class), $this->session ); @@ -126,7 +75,8 @@ describe('CSRF Token Generation', function () { it('includes CSRF token in rendered wrapper HTML', function () { $renderer = new LiveComponentRenderer( - $this->createMock(TemplateRenderer::class), + $this->createMock(TemplateLoader::class), + $this->createMock(TemplateProcessor::class), $this->session ); @@ -465,7 +415,8 @@ describe('End-to-End CSRF Flow', function () { it('completes full CSRF flow from render to action execution', function () { // Step 1: Render component with CSRF token $renderer = new LiveComponentRenderer( - $this->createMock(TemplateRenderer::class), + $this->createMock(TemplateLoader::class), + $this->createMock(TemplateProcessor::class), $this->session );