From d0c36b9245ae3d40018805ae57bc29e0b2ec2d2a Mon Sep 17 00:00:00 2001 From: Michael Schiemer Date: Mon, 3 Nov 2025 21:56:27 +0100 Subject: [PATCH] refactor(di): enhance `InitializerDependencyAnalyzer` with fallback and initializer-based return type analysis - Add fallback mechanism to resolve return types for closures without explicit return types. - Introduce methods for discovering initializer classes based on naming conventions and interface analysis. - Implement functionality to analyze the `__invoke()` method's return types, including actual return class extraction. - Improve dependency resolution with comprehensive initializer discovery strategies. --- .../DI/InitializerDependencyAnalyzer.php | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) diff --git a/src/Framework/DI/InitializerDependencyAnalyzer.php b/src/Framework/DI/InitializerDependencyAnalyzer.php index 3b8a1863..06641aa9 100644 --- a/src/Framework/DI/InitializerDependencyAnalyzer.php +++ b/src/Framework/DI/InitializerDependencyAnalyzer.php @@ -554,6 +554,20 @@ final readonly class InitializerDependencyAnalyzer 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) { + // getInitializerInvokeReturnType() extrahiert bereits die tatsächliche Klasse + // aus dem Code (z.B. "return new Engine(...)"), also können wir sie direkt verwenden + if (class_exists($invokeReturnType)) { + return $invokeReturnType; + } + } + } } } } @@ -625,5 +639,172 @@ final readonly class InitializerDependencyAnalyzer 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)) { + 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 = '/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; + } + } + + return null; + } catch (\Throwable) { + return null; + } + } }